Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ or add a reference yourself:

```xml
<ItemGroup>
<PackageReference Include="SharpSource" Version="1.31.0" PrivateAssets="All" />
<PackageReference Include="SharpSource" Version="1.31.1" PrivateAssets="All" />
</ItemGroup>
```

Expand Down
2 changes: 1 addition & 1 deletion SharpSource/SharpSource.Package/SharpSource.Package.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<PropertyGroup>
<PackageId>SharpSource</PackageId>
<PackageVersion>1.31.0</PackageVersion>
<PackageVersion>1.31.1</PackageVersion>
<Authors>Jeroen Vannevel</Authors>
<PackageLicenseUrl>https://github.com/Vannevelj/SharpSource/blob/master/LICENSE.md</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/Vannevelj/SharpSource</PackageProjectUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"
Expand All @@ -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;
Expand Down
19 changes: 17 additions & 2 deletions SharpSource/SharpSource.Test/Helpers/CSCodeFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public static partial class CSharpCodeFixVerifier<TAnalyzer, TCodeFix>
public static DiagnosticResult Diagnostic(int location = 0)
=> CSharpCodeFixVerifier<TAnalyzer, TCodeFix, DefaultVerifier>.Diagnostic().WithLocation(location);

/// <summary>
/// Gets a <see cref="DiagnosticResult"/> without a predefined location.
/// </summary>
public static DiagnosticResult DiagnosticWithoutLocation()
=> CSharpCodeFixVerifier<TAnalyzer, TCodeFix, DefaultVerifier>.Diagnostic();

/// <inheritdoc cref="CodeFixVerifier{TAnalyzer, TCodeFix, TTest, TVerifier}.Diagnostic(string)"/>
public static DiagnosticResult Diagnostic(string diagnosticId)
=> CSharpCodeFixVerifier<TAnalyzer, TCodeFix, DefaultVerifier>.Diagnostic(diagnosticId);
Expand Down Expand Up @@ -62,14 +68,23 @@ public static async Task VerifyCodeFix(string source, DiagnosticResult expected,
=> await VerifyCodeFix(source, [expected], fixedSource, codeActionIndex, additionalFiles: null, batchFixedSource: null, disabledDiagnostics);

/// <inheritdoc cref="CodeFixVerifier{TAnalyzer, TCodeFix, TTest, TVerifier}.VerifyCodeFixAsync(string, DiagnosticResult[], string)"/>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]")]
Expand All @@ -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<string, string, Service> 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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,27 +213,46 @@ 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 = @"
using System.Linq;
using System.Collections.Generic;

IEnumerable<string> values = new [] { ""test"" };
values?.ToArray().ToList().AsEnumerable();";
values?{|#0:.ToArray().ToList()|}.AsEnumerable();";

var expected = $@"
var expected = @"
using System.Linq;
using System.Collections.Generic;

IEnumerable<string> values = new [] {{ ""test"" }};
IEnumerable<string> 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<string> values = new [] { ""test"" };
values.ToArray().Where(x => true).ToList().Where(x => true);";

var expected = @"
using System.Linq;
using System.Collections.Generic;

IEnumerable<string> 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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
}
}
}
}
}
Loading