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
20 changes: 20 additions & 0 deletions src/Components/Analyzers/src/ComponentFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ public static bool IsSupplyParameterFromForm(ComponentSymbols symbols, IProperty
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.SupplyParameterFromFormAttribute));
}

public static bool IsPersistentState(ComponentSymbols symbols, IPropertySymbol property)
{
if (symbols == null)
{
throw new ArgumentNullException(nameof(symbols));
}

if (property == null)
{
throw new ArgumentNullException(nameof(property));
}

if (symbols.PersistentStateAttribute == null)
{
return false;
}

return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.PersistentStateAttribute));
}

public static bool IsComponentBase(ComponentSymbols symbols, INamedTypeSymbol type)
{
if (symbols is null)
Expand Down
8 changes: 7 additions & 1 deletion src/Components/Analyzers/src/ComponentSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,16 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo

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

// Try to get optional symbols for SupplyParameterFromForm analyzer
// Try to get optional symbols for SupplyParameterFromForm and PersistentState analyzers
var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
var persistentStateAttribute = compilation.GetTypeByMetadataName(ComponentsApi.PersistentStateAttribute.MetadataName);
var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);

symbols = new ComponentSymbols(
parameterAttribute,
cascadingParameterAttribute,
supplyParameterFromFormAttribute,
persistentStateAttribute,
componentBaseType,
parameterCaptureUnmatchedValuesRuntimeType,
icomponentType);
Expand All @@ -65,13 +67,15 @@ private ComponentSymbols(
INamedTypeSymbol parameterAttribute,
INamedTypeSymbol cascadingParameterAttribute,
INamedTypeSymbol supplyParameterFromFormAttribute,
INamedTypeSymbol persistentStateAttribute,
INamedTypeSymbol componentBaseType,
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
INamedTypeSymbol icomponentType)
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
PersistentStateAttribute = persistentStateAttribute; // Can be null
ComponentBaseType = componentBaseType; // Can be null
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
Expand All @@ -86,6 +90,8 @@ private ComponentSymbols(

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

public INamedTypeSymbol PersistentStateAttribute { get; } // Can be null if not available

public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available

public INamedTypeSymbol IComponentType { get; }
Expand Down
6 changes: 6 additions & 0 deletions src/Components/Analyzers/src/ComponentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public static class ComponentBase
public const string MetadataName = FullTypeName;
}

public static class PersistentStateAttribute
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.PersistentStateAttribute";
public const string MetadataName = FullTypeName;
}

public static class IComponent
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";
Expand Down
9 changes: 9 additions & 0 deletions src/Components/Analyzers/src/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Description)));

public static readonly DiagnosticDescriptor PersistentStateShouldNotHavePropertyInitializer = new(
"BL0009",
CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Title)),
CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Format)),
Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Description)));
}
101 changes: 101 additions & 0 deletions src/Components/Analyzers/src/PersistentStateAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

#nullable enable

namespace Microsoft.AspNetCore.Components.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PersistentStateAnalyzer : DiagnosticAnalyzer
{
public PersistentStateAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(
DiagnosticDescriptors.PersistentStateShouldNotHavePropertyInitializer);
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(context =>
{
if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
{
// Types we need are not defined.
return;
}

context.RegisterSyntaxNodeAction(context =>
{
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;

// Check if property has an initializer
if (propertyDeclaration.Initializer == null)
{
return;
}

// Ignore initializers that set to default values (null, default, etc.)
if (IsDefaultValueInitializer(propertyDeclaration.Initializer.Value))
{
return;
}

var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration);
if (propertySymbol == null)
{
return;
}

// Check if property has [PersistentState] attribute
if (!ComponentFacts.IsPersistentState(symbols, propertySymbol))
{
return;
}

// Check if the containing type inherits from ComponentBase
var containingType = propertySymbol.ContainingType;
if (!ComponentFacts.IsComponentBase(symbols, containingType))
{
return;
}

var propertyLocation = propertySymbol.Locations.FirstOrDefault();
if (propertyLocation != null)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.PersistentStateShouldNotHavePropertyInitializer,
propertyLocation,
propertySymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}, SyntaxKind.PropertyDeclaration);
});
}

private static bool IsDefaultValueInitializer(ExpressionSyntax expression)
{
return expression switch
{
// null
LiteralExpressionSyntax { Token.ValueText: "null" } => true,
// null!
PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax { Token.ValueText: "null" }, OperatorToken.ValueText: "!" } => true,
// default
LiteralExpressionSyntax literal when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
// default!
PostfixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax literal, OperatorToken.ValueText: "!" }
when literal.Token.IsKind(SyntaxKind.DefaultKeyword) => true,
_ => false
};
}
}
9 changes: 9 additions & 0 deletions src/Components/Analyzers/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,13 @@
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Title" xml:space="preserve">
<value>Property with [SupplyParameterFromForm] should not have initializer</value>
</data>
<data name="PersistentStateShouldNotHavePropertyInitializer_Description" xml:space="preserve">
<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>
</data>
<data name="PersistentStateShouldNotHavePropertyInitializer_Format" xml:space="preserve">
<value>Property '{0}' has [PersistentState] and a property initializer. This can be overwritten during parameter binding.</value>
</data>
<data name="PersistentStateShouldNotHavePropertyInitializer_Title" xml:space="preserve">
<value>Property with [PersistentState] should not have initializer</value>
</data>
</root>
Loading
Loading