diff --git a/CHANGELOG.md b/CHANGELOG.md index e719a95..b3e8418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # CHANGELOG https://keepachangelog.com/en/1.0.0/ +## [1.31.1] - 2026-01-25 +- `MultipleFromBodyParameters`: Support Minimal Web APIs (lambda expressions) + ## [1.31.0] - 2026-01-24 - `LoggerMessageAttribute`: This analyzer suggests converting regular logging calls to the source-generated logging pattern. diff --git a/README.md b/README.md index 0e4bb8d..1af9e5d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ or add a reference yourself: ```xml - + ``` diff --git a/SharpSource/SharpSource.Package/SharpSource.Package.csproj b/SharpSource/SharpSource.Package/SharpSource.Package.csproj index 704ebcd..0412d15 100644 --- a/SharpSource/SharpSource.Package/SharpSource.Package.csproj +++ b/SharpSource/SharpSource.Package/SharpSource.Package.csproj @@ -9,7 +9,7 @@ SharpSource - 1.31.0 + 1.31.1 Jeroen Vannevel https://github.com/Vannevelj/SharpSource/blob/master/LICENSE.md https://github.com/Vannevelj/SharpSource diff --git a/SharpSource/SharpSource.Test/ComparingStringsWithoutStringComparisonTests.cs b/SharpSource/SharpSource.Test/ComparingStringsWithoutStringComparisonTests.cs index 13a87c0..8ead44f 100644 --- a/SharpSource/SharpSource.Test/ComparingStringsWithoutStringComparisonTests.cs +++ b/SharpSource/SharpSource.Test/ComparingStringsWithoutStringComparisonTests.cs @@ -482,7 +482,6 @@ public async Task ComparingStringsWithoutStringComparison_PassedAsArgument() } [BugVerificationTest(IssueUrl = "https://github.com/Vannevelj/SharpSource/issues/56")] - [Ignore("Code fix introduces a \r\n newline which fails the equality check because the test expects \n. Presumably fixed when https://github.com/Vannevelj/SharpSource/issues/274 is done")] public async Task ComparingStringsWithoutStringComparison_WithOtherUsingStatements() { var original = @" @@ -493,8 +492,8 @@ public async Task ComparingStringsWithoutStringComparison_WithOtherUsingStatemen bool result = {|#0:s1.ToLower()|} == s2.ToLower();"; var result = @$" -using System.Text; using System; +using System.Text; string s1 = string.Empty; string s2 = string.Empty; diff --git a/SharpSource/SharpSource.Test/Helpers/CSCodeFix.cs b/SharpSource/SharpSource.Test/Helpers/CSCodeFix.cs index 40bfc6e..081807c 100644 --- a/SharpSource/SharpSource.Test/Helpers/CSCodeFix.cs +++ b/SharpSource/SharpSource.Test/Helpers/CSCodeFix.cs @@ -18,6 +18,12 @@ public static partial class CSharpCodeFixVerifier public static DiagnosticResult Diagnostic(int location = 0) => CSharpCodeFixVerifier.Diagnostic().WithLocation(location); + /// + /// Gets a without a predefined location. + /// + public static DiagnosticResult DiagnosticWithoutLocation() + => CSharpCodeFixVerifier.Diagnostic(); + /// public static DiagnosticResult Diagnostic(string diagnosticId) => CSharpCodeFixVerifier.Diagnostic(diagnosticId); @@ -62,14 +68,23 @@ public static async Task VerifyCodeFix(string source, DiagnosticResult expected, => await VerifyCodeFix(source, [expected], fixedSource, codeActionIndex, additionalFiles: null, batchFixedSource: null, disabledDiagnostics); /// - public static async Task VerifyCodeFix(string source, DiagnosticResult[] expected, string fixedSource, int codeActionIndex = 0, string[]? additionalFiles = null, string? batchFixedSource = null, string[]? disabledDiagnostics = null) + public static async Task VerifyCodeFix( + string source, + DiagnosticResult[] expected, + string fixedSource, + int codeActionIndex = 0, + string[]? additionalFiles = null, + string? batchFixedSource = null, + string[]? disabledDiagnostics = null, + int? numberOfIncrementalIterations = null) { var test = new Test { TestCode = source, FixedCode = fixedSource, BatchFixedCode = batchFixedSource!, - CodeActionIndex = codeActionIndex + CodeActionIndex = codeActionIndex, + NumberOfIncrementalIterations = numberOfIncrementalIterations }; if (disabledDiagnostics != null) diff --git a/SharpSource/SharpSource.Test/MultipleFromBodyParametersTests.cs b/SharpSource/SharpSource.Test/MultipleFromBodyParametersTests.cs index ec58208..f93c2a6 100644 --- a/SharpSource/SharpSource.Test/MultipleFromBodyParametersTests.cs +++ b/SharpSource/SharpSource.Test/MultipleFromBodyParametersTests.cs @@ -25,7 +25,6 @@ class MyController } [TestMethod] - [Ignore("Minimal Web API is not supported yet. See https://github.com/Vannevelj/SharpSource/issues/140")] [DataRow("[FromBody]")] [DataRow("[FromBodyAttribute]")] [DataRow("[Microsoft.AspNetCore.Mvc.FromBody]")] @@ -35,14 +34,14 @@ public async Task MultipleFromBodyParameters_MinimalWebApiAsync(string attribute using Microsoft.AspNetCore.Mvc; var app = new WebApplication(); -app.MapGet(""/"", ({attribute} string first, {attribute} string second, Service service) => {{ }}); +app.MapGet(""/"", {{|#0:({attribute} string first, {attribute} string second, Service service) => {{ }}|}}); class WebApplication {{ public void MapGet(string path, System.Action handler) {{ }} }} class Service {{ }}"; - await VerifyCS.VerifyDiagnosticWithoutFix(original, VerifyCS.Diagnostic().WithMessage("Method DoThing specifies multiple [FromBody] parameters but only one is allowed. Specify a wrapper type or use [FromForm], [FromRoute], [FromHeader] and [FromQuery] instead.")); + await VerifyCS.VerifyDiagnosticWithoutFix(original, VerifyCS.Diagnostic().WithMessage("Method lambda expression specifies multiple [FromBody] parameters but only one is allowed. Specify a wrapper type or use [FromForm], [FromRoute], [FromHeader] and [FromQuery] instead.")); } [TestMethod] diff --git a/SharpSource/SharpSource.Test/UnnecessaryEnumerableMaterializationTests.cs b/SharpSource/SharpSource.Test/UnnecessaryEnumerableMaterializationTests.cs index 7e0cf09..0ce6f58 100644 --- a/SharpSource/SharpSource.Test/UnnecessaryEnumerableMaterializationTests.cs +++ b/SharpSource/SharpSource.Test/UnnecessaryEnumerableMaterializationTests.cs @@ -213,7 +213,6 @@ public async Task UnnecessaryEnumerableMaterialization_ConditionalAccess() } [TestMethod] - [Ignore("Need to find a way to handle the testing of a code fix when there are multiple issues, see https://github.com/Vannevelj/SharpSource/issues/288")] public async Task UnnecessaryEnumerableMaterialization_ConditionalAccess_Chained() { var original = @" @@ -221,19 +220,39 @@ public async Task UnnecessaryEnumerableMaterialization_ConditionalAccess_Chained using System.Collections.Generic; IEnumerable values = new [] { ""test"" }; -values?.ToArray().ToList().AsEnumerable();"; +values?{|#0:.ToArray().ToList()|}.AsEnumerable();"; - var expected = $@" + var expected = @" using System.Linq; using System.Collections.Generic; -IEnumerable values = new [] {{ ""test"" }}; +IEnumerable values = new [] { ""test"" }; values?.ToList().AsEnumerable();"; - await VerifyCS.VerifyCodeFix(original, new[] { - VerifyCS.Diagnostic().WithNoLocation().WithMessage("ToArray is unnecessarily materializing the IEnumerable and can be omitted").WithSpan(6, 8, 6, 27), - VerifyCS.Diagnostic().WithNoLocation().WithMessage("ToList is unnecessarily materializing the IEnumerable and can be omitted").WithSpan(6, 8, 6, 42) - }, expected); + await VerifyCS.VerifyCodeFix(original, VerifyCS.Diagnostic().WithMessage("ToArray is unnecessarily materializing the IEnumerable and can be omitted"), expected); + } + + [TestMethod] + public async Task UnnecessaryEnumerableMaterialization_MultipleMaterializations_IncrementalFixes() + { + var original = @" +using System.Linq; +using System.Collections.Generic; + +IEnumerable values = new [] { ""test"" }; +values.ToArray().Where(x => true).ToList().Where(x => true);"; + + var expected = @" +using System.Linq; +using System.Collections.Generic; + +IEnumerable values = new [] { ""test"" }; +values.Where(x => true).Where(x => true);"; + + await VerifyCS.VerifyCodeFix(original, [ + VerifyCS.DiagnosticWithoutLocation().WithSpan(6, 1, 6, 34).WithMessage("ToArray is unnecessarily materializing the IEnumerable and can be omitted"), + VerifyCS.DiagnosticWithoutLocation().WithSpan(6, 1, 6, 60).WithMessage("ToList is unnecessarily materializing the IEnumerable and can be omitted") + ], expected, numberOfIncrementalIterations: 2); } [TestMethod] diff --git a/SharpSource/SharpSource/Diagnostics/MultipleFromBodyParametersAnalyzer.cs b/SharpSource/SharpSource/Diagnostics/MultipleFromBodyParametersAnalyzer.cs index 40b2adf..ba99b9c 100644 --- a/SharpSource/SharpSource/Diagnostics/MultipleFromBodyParametersAnalyzer.cs +++ b/SharpSource/SharpSource/Diagnostics/MultipleFromBodyParametersAnalyzer.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using SharpSource.Utilities; @@ -30,12 +32,13 @@ public override void Initialize(AnalysisContext context) var fromBodySymbol = compilationContext.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.FromBodyAttribute"); if (fromBodySymbol is not null) { - compilationContext.RegisterSymbolAction(context => Analyze(context, fromBodySymbol), SymbolKind.Method); + compilationContext.RegisterSymbolAction(context => AnalyzeMethod(context, fromBodySymbol), SymbolKind.Method); + compilationContext.RegisterSyntaxNodeAction(context => AnalyzeLambda(context, fromBodySymbol), SyntaxKind.ParenthesizedLambdaExpression); } }); } - private static void Analyze(SymbolAnalysisContext context, INamedTypeSymbol fromBodySymbol) + private static void AnalyzeMethod(SymbolAnalysisContext context, INamedTypeSymbol fromBodySymbol) { var methodSymbol = (IMethodSymbol)context.Symbol; var attributesOnParameters = methodSymbol.Parameters @@ -47,4 +50,30 @@ private static void Analyze(SymbolAnalysisContext context, INamedTypeSymbol from context.ReportDiagnostic(Diagnostic.Create(Rule, methodSymbol.Locations[0], methodSymbol.Name)); } } + + private static void AnalyzeLambda(SyntaxNodeAnalysisContext context, INamedTypeSymbol fromBodySymbol) + { + var lambda = (ParenthesizedLambdaExpressionSyntax)context.Node; + var fromBodyCount = 0; + + foreach (var parameter in lambda.ParameterList.Parameters) + { + foreach (var attributeList in parameter.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + var attributeSymbol = context.SemanticModel.GetTypeInfo(attribute).Type; + if (fromBodySymbol.Equals(attributeSymbol, SymbolEqualityComparer.Default)) + { + fromBodyCount++; + if (fromBodyCount > 1) + { + context.ReportDiagnostic(Diagnostic.Create(Rule, lambda.GetLocation(), "lambda expression")); + return; + } + } + } + } + } + } } \ No newline at end of file