Skip to content

Commit 8f674d0

Browse files
Add an analyzer that warns about non-literal sequence numbers (#35805)
1 parent 3ac7940 commit 8f674d0

File tree

5 files changed

+228
-38
lines changed

5 files changed

+228
-38
lines changed

src/Framework/Analyzer/src/DelegateEndpoints/DiagnosticDescriptors.cs

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.CodeAnalysis;
5+
6+
namespace Microsoft.AspNetCore.Analyzers;
7+
8+
[System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
9+
internal static class DiagnosticDescriptors
10+
{
11+
internal static readonly DiagnosticDescriptor DoNotUseModelBindingAttributesOnDelegateEndpointParameters = new(
12+
"ASP0003",
13+
"Do not use model binding attributes with Map handlers",
14+
"{0} should not be specified for a {1} Delegate parameter",
15+
"Usage",
16+
DiagnosticSeverity.Warning,
17+
isEnabledByDefault: true,
18+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
19+
20+
internal static readonly DiagnosticDescriptor DoNotReturnActionResultsFromMapActions = new(
21+
"ASP0004",
22+
"Do not use action results with Map actions",
23+
"IActionResult instances should not be returned from a {0} Delegate parameter. Consider returning an equivalent result from Microsoft.AspNetCore.Http.Results.",
24+
"Usage",
25+
DiagnosticSeverity.Warning,
26+
isEnabledByDefault: true,
27+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
28+
29+
internal static readonly DiagnosticDescriptor DetectMisplacedLambdaAttribute = new(
30+
"ASP0005",
31+
"Do not place attribute on route handlers",
32+
"'{0}' should be placed on the endpoint delegate to be effective",
33+
"Usage",
34+
DiagnosticSeverity.Warning,
35+
isEnabledByDefault: true,
36+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
37+
38+
internal static readonly DiagnosticDescriptor DoNotUseNonLiteralSequenceNumbers = new(
39+
"ASP0006",
40+
"Do not use non-literal sequence numbers",
41+
"'{0}' should not be used as a sequence number. Instead, use an integer literal representing source code order.",
42+
"Usage",
43+
DiagnosticSeverity.Warning,
44+
isEnabledByDefault: true,
45+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
46+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Immutable;
5+
using System.Diagnostics;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using Microsoft.CodeAnalysis.Operations;
10+
11+
namespace Microsoft.AspNetCore.Analyzers.RenderTreeBuilder;
12+
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
public partial class RenderTreeBuilderAnalyzer : DiagnosticAnalyzer
15+
{
16+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(new[]
17+
{
18+
DiagnosticDescriptors.DoNotUseNonLiteralSequenceNumbers,
19+
});
20+
21+
public override void Initialize(AnalysisContext context)
22+
{
23+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
24+
context.EnableConcurrentExecution();
25+
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
26+
{
27+
var compilation = compilationStartAnalysisContext.Compilation;
28+
29+
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
30+
{
31+
return;
32+
}
33+
34+
compilationStartAnalysisContext.RegisterOperationAction(operationAnalysisContext =>
35+
{
36+
var invocation = (IInvocationOperation)operationAnalysisContext.Operation;
37+
38+
if (!IsRenderTreeBuilderMethodWithSequenceParameter(wellKnownTypes, invocation.TargetMethod))
39+
{
40+
return;
41+
}
42+
43+
var sequenceArgument = invocation.Arguments[0];
44+
45+
if (!sequenceArgument.Value.Syntax.IsKind(SyntaxKind.NumericLiteralExpression))
46+
{
47+
operationAnalysisContext.ReportDiagnostic(Diagnostic.Create(
48+
DiagnosticDescriptors.DoNotUseNonLiteralSequenceNumbers,
49+
sequenceArgument.Syntax.GetLocation(),
50+
sequenceArgument.Syntax.ToString()));
51+
}
52+
}, OperationKind.Invocation);
53+
});
54+
}
55+
56+
private static bool IsRenderTreeBuilderMethodWithSequenceParameter(WellKnownTypes wellKnownTypes, IMethodSymbol targetMethod)
57+
=> SymbolEqualityComparer.Default.Equals(wellKnownTypes.RenderTreeBuilder, targetMethod.ContainingType)
58+
&& targetMethod.Parameters.Length != 0
59+
&& targetMethod.Parameters[0].Name == "sequence";
60+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.Analyzers.RenderTreeBuilder;
8+
9+
internal sealed class WellKnownTypes
10+
{
11+
public static bool TryCreate(Compilation compilation, [NotNullWhen(returnValue: true)] out WellKnownTypes? wellKnownTypes)
12+
{
13+
wellKnownTypes = default;
14+
15+
const string RenderTreeBuilder = "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder";
16+
if (compilation.GetTypeByMetadataName(RenderTreeBuilder) is not { } renderTreeBuilder)
17+
{
18+
return false;
19+
}
20+
21+
wellKnownTypes = new()
22+
{
23+
RenderTreeBuilder = renderTreeBuilder
24+
};
25+
26+
return true;
27+
}
28+
29+
public INamedTypeSymbol RenderTreeBuilder { get; private init; }
30+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using Microsoft.AspNetCore.Analyzer.Testing;
6+
7+
namespace Microsoft.AspNetCore.Analyzers.RenderTreeBuilder;
8+
9+
public class DisallowNonLiteralSequenceNumbersTest
10+
{
11+
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RenderTreeBuilderAnalyzer());
12+
13+
[Fact]
14+
public async Task RenderTreeBuilderInvocationWithNumericLiteralArgument_Works()
15+
{
16+
// Arrange
17+
var source = @"
18+
using Microsoft.AspNetCore.Components.Rendering;
19+
var renderTreeBuilder = new RenderTreeBuilder();
20+
renderTreeBuilder.OpenElement(0, ""div"");
21+
renderTreeBuilder.CloseElement();
22+
";
23+
// Act
24+
var diagnostics = await Runner.GetDiagnosticsAsync(source);
25+
26+
// Assert
27+
Assert.Empty(diagnostics);
28+
}
29+
30+
[Fact]
31+
public async Task RenderTreeBuilderInvocationWithNonConstantArgument_ProducesDiagnostics()
32+
{
33+
// Arrange
34+
var source = TestSource.Read(@"
35+
using Microsoft.AspNetCore.Components.Rendering;
36+
var renderTreeBuilder = new RenderTreeBuilder();
37+
var i = 0;
38+
renderTreeBuilder.OpenRegion(/*MM*/i);
39+
renderTreeBuilder.CloseRegion();
40+
");
41+
// Act
42+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
43+
44+
// Assert
45+
var diagnostic = Assert.Single(diagnostics);
46+
Assert.Same(DiagnosticDescriptors.DoNotUseNonLiteralSequenceNumbers, diagnostic.Descriptor);
47+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
48+
Assert.StartsWith("'i' should not be used as a sequence number.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
49+
}
50+
51+
[Fact]
52+
public async Task RenderTreeBuilderInvocationWithConstantArgument_ProducesDiagnostics()
53+
{
54+
// Arrange
55+
var source = TestSource.Read(@"
56+
using Microsoft.AspNetCore.Components.Rendering;
57+
var renderTreeBuilder = new RenderTreeBuilder();
58+
const int i = 0;
59+
renderTreeBuilder.OpenRegion(/*MM*/i);
60+
renderTreeBuilder.CloseRegion();
61+
");
62+
// Act
63+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
64+
65+
// Assert
66+
var diagnostic = Assert.Single(diagnostics);
67+
Assert.Same(DiagnosticDescriptors.DoNotUseNonLiteralSequenceNumbers, diagnostic.Descriptor);
68+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
69+
Assert.StartsWith("'i' should not be used as a sequence number.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
70+
}
71+
72+
[Fact]
73+
public async Task RenderTreeBuilderInvocationWithInvocationArgument_ProducesDiagnostics()
74+
{
75+
// Arrange
76+
var source = TestSource.Read(@"
77+
using Microsoft.AspNetCore.Components.Rendering;
78+
var renderTreeBuilder = new RenderTreeBuilder();
79+
renderTreeBuilder.OpenElement(/*MM*/ComputeSequenceNumber(0), ""div"");
80+
renderTreeBuilder.CloseElement();
81+
static int ComputeSequenceNumber(int i) => i + 1;
82+
");
83+
// Act
84+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
85+
86+
// Assert
87+
var diagnostic = Assert.Single(diagnostics);
88+
Assert.Same(DiagnosticDescriptors.DoNotUseNonLiteralSequenceNumbers, diagnostic.Descriptor);
89+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
90+
Assert.StartsWith("'ComputeSequenceNumber(0)' should not be used as a sequence number.", diagnostic.GetMessage(CultureInfo.InvariantCulture));
91+
}
92+
}

0 commit comments

Comments
 (0)