Skip to content

Commit e336a0b

Browse files
authored
[Blazor] Add analyzer to warn when SupplyParameterFromForm properties have non-default initializers (#63110)
1 parent f9722e5 commit e336a0b

File tree

7 files changed

+439
-0
lines changed

7 files changed

+439
-0
lines changed

src/Components/Analyzers/src/ComponentFacts.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,57 @@ public static bool IsCascadingParameter(ComponentSymbols symbols, IPropertySymbo
8787
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.CascadingParameterAttribute));
8888
}
8989

90+
public static bool IsSupplyParameterFromForm(ComponentSymbols symbols, IPropertySymbol property)
91+
{
92+
if (symbols == null)
93+
{
94+
throw new ArgumentNullException(nameof(symbols));
95+
}
96+
97+
if (property == null)
98+
{
99+
throw new ArgumentNullException(nameof(property));
100+
}
101+
102+
if (symbols.SupplyParameterFromFormAttribute == null)
103+
{
104+
return false;
105+
}
106+
107+
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.SupplyParameterFromFormAttribute));
108+
}
109+
110+
public static bool IsComponentBase(ComponentSymbols symbols, INamedTypeSymbol type)
111+
{
112+
if (symbols is null)
113+
{
114+
throw new ArgumentNullException(nameof(symbols));
115+
}
116+
117+
if (type is null)
118+
{
119+
throw new ArgumentNullException(nameof(type));
120+
}
121+
122+
if (symbols.ComponentBaseType == null)
123+
{
124+
return false;
125+
}
126+
127+
// Check if the type inherits from ComponentBase
128+
var current = type.BaseType;
129+
while (current != null)
130+
{
131+
if (SymbolEqualityComparer.Default.Equals(current, symbols.ComponentBaseType))
132+
{
133+
return true;
134+
}
135+
current = current.BaseType;
136+
}
137+
138+
return false;
139+
}
140+
90141
public static bool IsComponent(ComponentSymbols symbols, Compilation compilation, INamedTypeSymbol type)
91142
{
92143
if (symbols is null)

src/Components/Analyzers/src/ComponentSymbols.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
4747

4848
var parameterCaptureUnmatchedValuesRuntimeType = dictionary.Construct(@string, @object);
4949

50+
// Try to get optional symbols for SupplyParameterFromForm analyzer
51+
var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
52+
var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
53+
5054
symbols = new ComponentSymbols(
5155
parameterAttribute,
5256
cascadingParameterAttribute,
57+
supplyParameterFromFormAttribute,
58+
componentBaseType,
5359
parameterCaptureUnmatchedValuesRuntimeType,
5460
icomponentType);
5561
return true;
@@ -58,11 +64,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
5864
private ComponentSymbols(
5965
INamedTypeSymbol parameterAttribute,
6066
INamedTypeSymbol cascadingParameterAttribute,
67+
INamedTypeSymbol supplyParameterFromFormAttribute,
68+
INamedTypeSymbol componentBaseType,
6169
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
6270
INamedTypeSymbol icomponentType)
6371
{
6472
ParameterAttribute = parameterAttribute;
6573
CascadingParameterAttribute = cascadingParameterAttribute;
74+
SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
75+
ComponentBaseType = componentBaseType; // Can be null
6676
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
6777
IComponentType = icomponentType;
6878
}
@@ -74,5 +84,9 @@ private ComponentSymbols(
7484

7585
public INamedTypeSymbol CascadingParameterAttribute { get; }
7686

87+
public INamedTypeSymbol SupplyParameterFromFormAttribute { get; } // Can be null if not available
88+
89+
public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available
90+
7791
public INamedTypeSymbol IComponentType { get; }
7892
}

src/Components/Analyzers/src/ComponentsApi.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ public static class CascadingParameterAttribute
2323
public const string MetadataName = FullTypeName;
2424
}
2525

26+
public static class SupplyParameterFromFormAttribute
27+
{
28+
public const string FullTypeName = "Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute";
29+
public const string MetadataName = FullTypeName;
30+
}
31+
32+
public static class ComponentBase
33+
{
34+
public const string FullTypeName = "Microsoft.AspNetCore.Components.ComponentBase";
35+
public const string MetadataName = FullTypeName;
36+
}
37+
2638
public static class IComponent
2739
{
2840
public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";

src/Components/Analyzers/src/DiagnosticDescriptors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,13 @@ internal static class DiagnosticDescriptors
7474
Usage,
7575
DiagnosticSeverity.Warning,
7676
isEnabledByDefault: true);
77+
78+
public static readonly DiagnosticDescriptor SupplyParameterFromFormShouldNotHavePropertyInitializer = new(
79+
"BL0008",
80+
CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Title)),
81+
CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Format)),
82+
Usage,
83+
DiagnosticSeverity.Warning,
84+
isEnabledByDefault: true,
85+
description: CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Description)));
7786
}

src/Components/Analyzers/src/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,13 @@
180180
<data name="ComponentParametersShouldBeAutoProperties_Title" xml:space="preserve">
181181
<value>Component parameters should be auto properties</value>
182182
</data>
183+
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Description" xml:space="preserve">
184+
<value>The value of a property decorated with [SupplyParameterFromForm] and initialized with a property initializer can be overwritten with null when the component receives parameters. To ensure the initialized value is not overwritten, move the initialization to a component lifecycle method like OnInitialized or OnInitializedAsync</value>
185+
</data>
186+
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Format" xml:space="preserve">
187+
<value>Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.</value>
188+
</data>
189+
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Title" xml:space="preserve">
190+
<value>Property with [SupplyParameterFromForm] should not have initializer</value>
191+
</data>
183192
</root>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
11+
#nullable enable
12+
13+
namespace Microsoft.AspNetCore.Components.Analyzers;
14+
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
public sealed class SupplyParameterFromFormAnalyzer : DiagnosticAnalyzer
17+
{
18+
public SupplyParameterFromFormAnalyzer()
19+
{
20+
SupportedDiagnostics = ImmutableArray.Create(
21+
DiagnosticDescriptors.SupplyParameterFromFormShouldNotHavePropertyInitializer);
22+
}
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.EnableConcurrentExecution();
29+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
30+
context.RegisterCompilationStartAction(context =>
31+
{
32+
if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
33+
{
34+
// Types we need are not defined.
35+
return;
36+
}
37+
38+
context.RegisterSyntaxNodeAction(context =>
39+
{
40+
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;
41+
42+
// Check if property has an initializer
43+
if (propertyDeclaration.Initializer == null)
44+
{
45+
return;
46+
}
47+
48+
// Ignore initializers that set to default values (null, default, etc.)
49+
if (IsDefaultValueInitializer(propertyDeclaration.Initializer.Value))
50+
{
51+
return;
52+
}
53+
54+
var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
55+
if (propertySymbol == null)
56+
{
57+
return;
58+
}
59+
60+
// Check if property has [SupplyParameterFromForm] attribute
61+
if (!ComponentFacts.IsSupplyParameterFromForm(symbols, propertySymbol))
62+
{
63+
return;
64+
}
65+
66+
// Check if the containing type inherits from ComponentBase
67+
var containingType = propertySymbol.ContainingType;
68+
if (!ComponentFacts.IsComponentBase(symbols, containingType))
69+
{
70+
return;
71+
}
72+
73+
var propertyLocation = propertySymbol.Locations.FirstOrDefault();
74+
if (propertyLocation != null)
75+
{
76+
context.ReportDiagnostic(Diagnostic.Create(
77+
DiagnosticDescriptors.SupplyParameterFromFormShouldNotHavePropertyInitializer,
78+
propertyLocation,
79+
propertySymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
80+
}
81+
}, SyntaxKind.PropertyDeclaration);
82+
});
83+
}
84+
85+
private static bool IsDefaultValueInitializer(ExpressionSyntax expression)
86+
{
87+
return expression switch
88+
{
89+
// null
90+
LiteralExpressionSyntax { Token.ValueText: "null" } => true,
91+
// null!
92+
PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax { Token.ValueText: "null" }, OperatorToken.ValueText: "!" } => true,
93+
// default
94+
LiteralExpressionSyntax literal when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
95+
// default!
96+
PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax literal, OperatorToken.ValueText: "!" }
97+
when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
98+
_ => false
99+
};
100+
}
101+
}

0 commit comments

Comments
 (0)