Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
51 changes: 51 additions & 0 deletions src/Components/Analyzers/src/ComponentFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,57 @@ public static bool IsCascadingParameter(ComponentSymbols symbols, IPropertySymbo
return property.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, symbols.CascadingParameterAttribute));
}

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

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

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

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

public static bool IsComponentBase(ComponentSymbols symbols, INamedTypeSymbol type)
{
if (symbols is null)
{
throw new ArgumentNullException(nameof(symbols));
}

if (type is null)
{
throw new ArgumentNullException(nameof(type));
}

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

// Check if the type inherits from ComponentBase
var current = type.BaseType;
while (current != null)
{
if (SymbolEqualityComparer.Default.Equals(current, symbols.ComponentBaseType))
{
return true;
}
current = current.BaseType;
}

return false;
}

public static bool IsComponent(ComponentSymbols symbols, Compilation compilation, INamedTypeSymbol type)
{
if (symbols is null)
Expand Down
14 changes: 14 additions & 0 deletions src/Components/Analyzers/src/ComponentSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo

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

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

symbols = new ComponentSymbols(
parameterAttribute,
cascadingParameterAttribute,
supplyParameterFromFormAttribute,
componentBaseType,
parameterCaptureUnmatchedValuesRuntimeType,
icomponentType);
return true;
Expand All @@ -58,11 +64,15 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
private ComponentSymbols(
INamedTypeSymbol parameterAttribute,
INamedTypeSymbol cascadingParameterAttribute,
INamedTypeSymbol supplyParameterFromFormAttribute,
INamedTypeSymbol componentBaseType,
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
INamedTypeSymbol icomponentType)
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
ComponentBaseType = componentBaseType; // Can be null
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
}
Expand All @@ -74,5 +84,9 @@ private ComponentSymbols(

public INamedTypeSymbol CascadingParameterAttribute { get; }

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

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

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

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

public static class ComponentBase
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.ComponentBase";
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 @@ -74,4 +74,13 @@ internal static class DiagnosticDescriptors
Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor SupplyParameterFromFormShouldNotHavePropertyInitializer = new(
"BL0008",
CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Title)),
CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Format)),
Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.SupplyParameterFromFormShouldNotHavePropertyInitializer_Description)));
}
9 changes: 9 additions & 0 deletions src/Components/Analyzers/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,13 @@
<data name="ComponentParametersShouldBeAutoProperties_Title" xml:space="preserve">
<value>Component parameters should be auto properties</value>
</data>
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Description" xml:space="preserve">
<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>
</data>
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Format" xml:space="preserve">
<value>Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.</value>
</data>
<data name="SupplyParameterFromFormShouldNotHavePropertyInitializer_Title" xml:space="preserve">
<value>Property with [SupplyParameterFromForm] should not have initializer</value>
</data>
</root>
101 changes: 101 additions & 0 deletions src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.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 SupplyParameterFromFormAnalyzer : DiagnosticAnalyzer
{
public SupplyParameterFromFormAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(
DiagnosticDescriptors.SupplyParameterFromFormShouldNotHavePropertyInitializer);
}

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 [SupplyParameterFromForm] attribute
if (!ComponentFacts.IsSupplyParameterFromForm(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.SupplyParameterFromFormShouldNotHavePropertyInitializer,
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
};
}
}
Loading
Loading