Skip to content

Commit 64804f9

Browse files
authored
[Blazor] Implement property analyzer for [PersistentState] attribute (#63236)
* The analyzer will warn when you use an initializer on the property with a non-default value, as it will be overwritten.
1 parent 78b0ad2 commit 64804f9

File tree

7 files changed

+434
-1
lines changed

7 files changed

+434
-1
lines changed

src/Components/Analyzers/src/ComponentFacts.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ public static bool IsSupplyParameterFromForm(ComponentSymbols symbols, IProperty
107107
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.SupplyParameterFromFormAttribute));
108108
}
109109

110+
public static bool IsPersistentState(ComponentSymbols symbols, IPropertySymbol property)
111+
{
112+
if (symbols == null)
113+
{
114+
throw new ArgumentNullException(nameof(symbols));
115+
}
116+
117+
if (property == null)
118+
{
119+
throw new ArgumentNullException(nameof(property));
120+
}
121+
122+
if (symbols.PersistentStateAttribute == null)
123+
{
124+
return false;
125+
}
126+
127+
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.PersistentStateAttribute));
128+
}
129+
110130
public static bool IsComponentBase(ComponentSymbols symbols, INamedTypeSymbol type)
111131
{
112132
if (symbols is null)

src/Components/Analyzers/src/ComponentSymbols.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ 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
50+
// Try to get optional symbols for SupplyParameterFromForm and PersistentState analyzers
5151
var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
52+
var persistentStateAttribute = compilation.GetTypeByMetadataName(ComponentsApi.PersistentStateAttribute.MetadataName);
5253
var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
5354

5455
symbols = new ComponentSymbols(
5556
parameterAttribute,
5657
cascadingParameterAttribute,
5758
supplyParameterFromFormAttribute,
59+
persistentStateAttribute,
5860
componentBaseType,
5961
parameterCaptureUnmatchedValuesRuntimeType,
6062
icomponentType);
@@ -65,13 +67,15 @@ private ComponentSymbols(
6567
INamedTypeSymbol parameterAttribute,
6668
INamedTypeSymbol cascadingParameterAttribute,
6769
INamedTypeSymbol supplyParameterFromFormAttribute,
70+
INamedTypeSymbol persistentStateAttribute,
6871
INamedTypeSymbol componentBaseType,
6972
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
7073
INamedTypeSymbol icomponentType)
7174
{
7275
ParameterAttribute = parameterAttribute;
7376
CascadingParameterAttribute = cascadingParameterAttribute;
7477
SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
78+
PersistentStateAttribute = persistentStateAttribute; // Can be null
7579
ComponentBaseType = componentBaseType; // Can be null
7680
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
7781
IComponentType = icomponentType;
@@ -86,6 +90,8 @@ private ComponentSymbols(
8690

8791
public INamedTypeSymbol SupplyParameterFromFormAttribute { get; } // Can be null if not available
8892

93+
public INamedTypeSymbol PersistentStateAttribute { get; } // Can be null if not available
94+
8995
public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available
9096

9197
public INamedTypeSymbol IComponentType { get; }

src/Components/Analyzers/src/ComponentsApi.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ public static class ComponentBase
3535
public const string MetadataName = FullTypeName;
3636
}
3737

38+
public static class PersistentStateAttribute
39+
{
40+
public const string FullTypeName = "Microsoft.AspNetCore.Components.PersistentStateAttribute";
41+
public const string MetadataName = FullTypeName;
42+
}
43+
3844
public static class IComponent
3945
{
4046
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
@@ -83,4 +83,13 @@ internal static class DiagnosticDescriptors
8383
DiagnosticSeverity.Warning,
8484
isEnabledByDefault: true,
8585
description: CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Description)));
86+
87+
public static readonly DiagnosticDescriptor PersistentStateShouldNotHavePropertyInitializer = new(
88+
"BL0009",
89+
CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Title)),
90+
CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Format)),
91+
Usage,
92+
DiagnosticSeverity.Warning,
93+
isEnabledByDefault: true,
94+
description: CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Description)));
8695
}
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 PersistentStateAnalyzer : DiagnosticAnalyzer
17+
{
18+
public PersistentStateAnalyzer()
19+
{
20+
SupportedDiagnostics = ImmutableArray.Create(
21+
DiagnosticDescriptors.PersistentStateShouldNotHavePropertyInitializer);
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 [PersistentState] attribute
61+
if (!ComponentFacts.IsPersistentState(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.PersistentStateShouldNotHavePropertyInitializer,
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+
}

src/Components/Analyzers/src/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,13 @@
189189
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Title" xml:space="preserve">
190190
<value>Property with [SupplyParameterFromForm] should not have initializer</value>
191191
</data>
192+
<data name="PersistentStateShouldNotHavePropertyInitializer_Description" xml:space="preserve">
193+
<value>The value of a property decorated with [PersistentState] and initialized with a property initializer can be overwritten 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>
194+
</data>
195+
<data name="PersistentStateShouldNotHavePropertyInitializer_Format" xml:space="preserve">
196+
<value>Property '{0}' has [PersistentState] and a property initializer. This can be overwritten during parameter binding.</value>
197+
</data>
198+
<data name="PersistentStateShouldNotHavePropertyInitializer_Title" xml:space="preserve">
199+
<value>Property with [PersistentState] should not have initializer</value>
200+
</data>
192201
</root>

0 commit comments

Comments
 (0)