From f6c6877716b79dbbb0c4be5419cc6a9bc0154a55 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 18:34:39 +0000
Subject: [PATCH 01/10] Initial plan
From 7f0fdf9d1d180b8b997bc5b2366ff3039f174618 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 18:51:04 +0000
Subject: [PATCH 02/10] Add initial implementation of SupplyParameterFromForm
analyzer
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Analyzers/src/ComponentFacts.cs | 51 +++++
.../Analyzers/src/ComponentSymbols.cs | 13 ++
src/Components/Analyzers/src/ComponentsApi.cs | 12 +
.../Analyzers/src/DiagnosticDescriptors.cs | 9 +
src/Components/Analyzers/src/Resources.resx | 9 +
.../src/SupplyParameterFromFormAnalyzer.cs | 78 +++++++
.../SupplyParameterFromFormAnalyzerTest.cs | 206 ++++++++++++++++++
7 files changed, 378 insertions(+)
create mode 100644 src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
create mode 100644 src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
diff --git a/src/Components/Analyzers/src/ComponentFacts.cs b/src/Components/Analyzers/src/ComponentFacts.cs
index 43561128b0a5..f7f976dce6dc 100644
--- a/src/Components/Analyzers/src/ComponentFacts.cs
+++ b/src/Components/Analyzers/src/ComponentFacts.cs
@@ -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)
diff --git a/src/Components/Analyzers/src/ComponentSymbols.cs b/src/Components/Analyzers/src/ComponentSymbols.cs
index ccdca61d9749..44d8903fbc41 100644
--- a/src/Components/Analyzers/src/ComponentSymbols.cs
+++ b/src/Components/Analyzers/src/ComponentSymbols.cs
@@ -29,6 +29,9 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
return false;
}
+ var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
+ var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
+
var icomponentType = compilation.GetTypeByMetadataName(ComponentsApi.IComponent.MetadataName);
if (icomponentType == null)
{
@@ -50,6 +53,8 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
symbols = new ComponentSymbols(
parameterAttribute,
cascadingParameterAttribute,
+ supplyParameterFromFormAttribute,
+ componentBaseType,
parameterCaptureUnmatchedValuesRuntimeType,
icomponentType);
return true;
@@ -58,11 +63,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;
+ ComponentBaseType = componentBaseType;
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
}
@@ -74,5 +83,9 @@ private ComponentSymbols(
public INamedTypeSymbol CascadingParameterAttribute { get; }
+ public INamedTypeSymbol SupplyParameterFromFormAttribute { get; }
+
+ public INamedTypeSymbol ComponentBaseType { get; }
+
public INamedTypeSymbol IComponentType { get; }
}
diff --git a/src/Components/Analyzers/src/ComponentsApi.cs b/src/Components/Analyzers/src/ComponentsApi.cs
index a62d070dedef..361e99b3e146 100644
--- a/src/Components/Analyzers/src/ComponentsApi.cs
+++ b/src/Components/Analyzers/src/ComponentsApi.cs
@@ -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";
diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
index ea3332c56fc7..afba1e3acd50 100644
--- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs
+++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs
@@ -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)));
}
diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx
index 5ef9f54b53eb..047427744b79 100644
--- a/src/Components/Analyzers/src/Resources.resx
+++ b/src/Components/Analyzers/src/Resources.resx
@@ -180,4 +180,13 @@
Component parameters should be auto properties
+
+ Initializing a property decorated with [SupplyParameterFromForm] can be overwritten with null during a form post. To ensure the property is never null, move the initialization to a component lifecycle method.
+
+
+ Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.
+
+
+ Property with [SupplyParameterFromForm] should not have initializer
+
\ No newline at end of file
diff --git a/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
new file mode 100644
index 000000000000..d81dfc70a567
--- /dev/null
+++ b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
@@ -0,0 +1,78 @@
+// 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 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;
+ }
+
+ 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);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
new file mode 100644
index 000000000000..cb909348f9c6
--- /dev/null
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -0,0 +1,206 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using TestHelper;
+
+namespace Microsoft.AspNetCore.Components.Analyzers.Test;
+
+public class SupplyParameterFromFormAnalyzerTest : DiagnosticVerifier
+{
+ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new SupplyParameterFromFormAnalyzer();
+
+ private static readonly string TestDeclarations = $@"
+ namespace {typeof(ParameterAttribute).Namespace}
+ {{
+ public class {typeof(ParameterAttribute).Name} : System.Attribute
+ {{
+ public bool CaptureUnmatchedValues {{ get; set; }}
+ }}
+
+ public class {typeof(CascadingParameterAttribute).Name} : System.Attribute
+ {{
+ }}
+
+ public class SupplyParameterFromFormAttribute : System.Attribute
+ {{
+ public string Name {{ get; set; }}
+ public string FormName {{ get; set; }}
+ }}
+
+ public interface {typeof(IComponent).Name}
+ {{
+ }}
+
+ public abstract class ComponentBase : {typeof(IComponent).Name}
+ {{
+ }}
+ }}
+";
+
+ [Fact]
+ public void IgnoresPropertiesWithoutSupplyParameterFromFormAttribute()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ public string MyProperty {{ get; set; }} = ""default"";
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithoutInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }}
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresNonComponentBaseClasses()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class NotAComponent
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void ReportsWarningForSupplyParameterFromFormWithInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 6, 50)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+
+ [Fact]
+ public void ReportsWarningForSupplyParameterFromFormWithObjectInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public InputModel Input {{ get; set; }} = new InputModel();
+ }}
+
+ class InputModel
+ {{
+ public string Value {{ get; set; }} = """";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.Input' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 6, 54)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+
+ [Fact]
+ public void ReportsWarningWithFormNameSpecified()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm(FormName = ""main-form"")] public string MyProperty {{ get; set; }} = ""default"";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 6, 80)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+
+ [Fact]
+ public void WorksWithInheritedComponentBase()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class BaseComponent : ComponentBase
+ {{
+ }}
+
+ class TestComponent : BaseComponent
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ }}
+ }}" + TestDeclarations;
+
+ var expected = new DiagnosticResult
+ {
+ Id = "BL0008",
+ Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[]
+ {
+ new DiagnosticResultLocation("Test0.cs", 11, 50)
+ }
+ };
+
+ VerifyCSharpDiagnostic(test, expected);
+ }
+}
\ No newline at end of file
From 8f7c6398901e33e4c9d3579d51b6620b1b218d11 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 19:01:38 +0000
Subject: [PATCH 03/10] Complete implementation and testing of
SupplyParameterFromForm analyzer
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
src/Components/Analyzers/src/ComponentSymbols.cs | 15 ++++++++-------
.../test/SupplyParameterFromFormAnalyzerTest.cs | 8 ++++----
2 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/src/Components/Analyzers/src/ComponentSymbols.cs b/src/Components/Analyzers/src/ComponentSymbols.cs
index 44d8903fbc41..e28da74f28d8 100644
--- a/src/Components/Analyzers/src/ComponentSymbols.cs
+++ b/src/Components/Analyzers/src/ComponentSymbols.cs
@@ -29,9 +29,6 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
return false;
}
- var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
- var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
-
var icomponentType = compilation.GetTypeByMetadataName(ComponentsApi.IComponent.MetadataName);
if (icomponentType == null)
{
@@ -50,6 +47,10 @@ 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,
@@ -70,8 +71,8 @@ private ComponentSymbols(
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
- SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute;
- ComponentBaseType = componentBaseType;
+ SupplyParameterFromFormAttribute = supplyParameterFromFormAttribute; // Can be null
+ ComponentBaseType = componentBaseType; // Can be null
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
}
@@ -83,9 +84,9 @@ private ComponentSymbols(
public INamedTypeSymbol CascadingParameterAttribute { get; }
- public INamedTypeSymbol SupplyParameterFromFormAttribute { get; }
+ public INamedTypeSymbol SupplyParameterFromFormAttribute { get; } // Can be null if not available
- public INamedTypeSymbol ComponentBaseType { get; }
+ public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available
public INamedTypeSymbol IComponentType { get; }
}
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
index cb909348f9c6..3e6a65d8c92f 100644
--- a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -107,7 +107,7 @@ class TestComponent : ComponentBase
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
- new DiagnosticResultLocation("Test0.cs", 6, 50)
+ new DiagnosticResultLocation("Test0.cs", 7, 53)
}
};
@@ -139,7 +139,7 @@ class InputModel
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
- new DiagnosticResultLocation("Test0.cs", 6, 54)
+ new DiagnosticResultLocation("Test0.cs", 7, 57)
}
};
@@ -166,7 +166,7 @@ class TestComponent : ComponentBase
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
- new DiagnosticResultLocation("Test0.cs", 6, 80)
+ new DiagnosticResultLocation("Test0.cs", 7, 77)
}
};
@@ -197,7 +197,7 @@ class TestComponent : BaseComponent
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
- new DiagnosticResultLocation("Test0.cs", 11, 50)
+ new DiagnosticResultLocation("Test0.cs", 11, 53)
}
};
From 93a68faf5323d47442acba822208dfdf1bdf3203 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 20:49:35 +0000
Subject: [PATCH 04/10] Update Resources.resx description per feedback
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
src/Components/Analyzers/src/Resources.resx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx
index 047427744b79..3cb016960cae 100644
--- a/src/Components/Analyzers/src/Resources.resx
+++ b/src/Components/Analyzers/src/Resources.resx
@@ -181,7 +181,7 @@
Component parameters should be auto properties
- Initializing a property decorated with [SupplyParameterFromForm] can be overwritten with null during a form post. To ensure the property is never null, move the initialization to a component lifecycle method.
+ The value of a property decorated with [SupplyParameterFromForm] and initialized with a property initializer can be overwritten with null when the component receives parameters.
Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.
From 0305c3f8161ebc7dad4d69f5b70176b7ad9f16fe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 21:00:08 +0000
Subject: [PATCH 05/10] Update Resources.resx description with complete
suggested text including lifecycle method recommendation
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
src/Components/Analyzers/src/Resources.resx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx
index 3cb016960cae..a6dc20636cc6 100644
--- a/src/Components/Analyzers/src/Resources.resx
+++ b/src/Components/Analyzers/src/Resources.resx
@@ -181,7 +181,7 @@
Component parameters should be auto properties
- The value of a property decorated with [SupplyParameterFromForm] and initialized with a property initializer can be overwritten with null when the component receives parameters.
+ 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
Property '{0}' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.
From 3c8fd5ae915fcb8da8577466ab0567dab59de895 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 21:10:44 +0000
Subject: [PATCH 06/10] Remove irrelevant test that checks FormName parameter
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../SupplyParameterFromFormAnalyzerTest.cs | 27 -------------------
1 file changed, 27 deletions(-)
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
index 3e6a65d8c92f..abbb194a27d8 100644
--- a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -146,33 +146,6 @@ class InputModel
VerifyCSharpDiagnostic(test, expected);
}
- [Fact]
- public void ReportsWarningWithFormNameSpecified()
- {
- var test = $@"
- namespace ConsoleApplication1
- {{
- using {typeof(ParameterAttribute).Namespace};
- class TestComponent : ComponentBase
- {{
- [SupplyParameterFromForm(FormName = ""main-form"")] public string MyProperty {{ get; set; }} = ""default"";
- }}
- }}" + TestDeclarations;
-
- var expected = new DiagnosticResult
- {
- Id = "BL0008",
- Message = "Property 'ConsoleApplication1.TestComponent.MyProperty' has [SupplyParameterFromForm] and a property initializer. This can be overwritten with null during form posts.",
- Severity = DiagnosticSeverity.Warning,
- Locations = new[]
- {
- new DiagnosticResultLocation("Test0.cs", 7, 77)
- }
- };
-
- VerifyCSharpDiagnostic(test, expected);
- }
-
[Fact]
public void WorksWithInheritedComponentBase()
{
From 28d5aba5b9736f2025580eff6c9bec6f207b17b8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 4 Aug 2025 22:22:09 +0000
Subject: [PATCH 07/10] Fix template files to follow BL0008 analyzer guidance
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Components/Account/Pages/ExternalLogin.razor | 2 +-
.../Components/Account/Pages/ForgotPassword.razor | 2 +-
.../BlazorWebCSharp.1/Components/Account/Pages/Login.razor | 2 +-
.../Components/Account/Pages/LoginWith2fa.razor | 2 +-
.../Components/Account/Pages/LoginWithRecoveryCode.razor | 2 +-
.../Components/Account/Pages/Manage/ChangePassword.razor | 2 +-
.../Components/Account/Pages/Manage/DeletePersonalData.razor | 2 +-
.../Components/Account/Pages/Manage/Email.razor | 2 +-
.../Components/Account/Pages/Manage/EnableAuthenticator.razor | 2 +-
.../Components/Account/Pages/Manage/Index.razor | 2 +-
.../Components/Account/Pages/Manage/Passkeys.razor | 2 +-
.../Components/Account/Pages/Manage/RenamePasskey.razor | 2 +-
.../Components/Account/Pages/Manage/SetPassword.razor | 2 +-
.../BlazorWebCSharp.1/Components/Account/Pages/Register.razor | 2 +-
.../Components/Account/Pages/ResendEmailConfirmation.razor | 2 +-
.../Components/Account/Pages/ResetPassword.razor | 2 +-
16 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
index 0c30821599fa..58b34581ae7e 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
@@ -54,7 +54,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? RemoteError { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
index ae82692ab7e9..a63ec90b244f 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
@@ -35,7 +35,7 @@
@code {
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override void OnInitialized()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
index 65347e0afdb6..32e78dae8f81 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
@@ -79,7 +79,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
index 3d7243c9b775..a21436d993b0 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
@@ -49,7 +49,7 @@
private ApplicationUser user = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
index f99d02900b12..313395ac9bde 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
@@ -38,7 +38,7 @@
private ApplicationUser user = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
index c035566ae85b..ca8561add275 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
@@ -48,7 +48,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
index f9c247897223..2982bafa7407 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
@@ -46,7 +46,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
index 94f6b6a473ca..c45829e6d4b6 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
@@ -63,7 +63,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
index 588134b5fa5a..3316bac84a74 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
@@ -78,7 +78,7 @@ else
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
index 13faf41804f2..bad84305bcbf 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
@@ -41,7 +41,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
index 283f26de1897..210e71dd3614 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
@@ -65,7 +65,7 @@ else
private string? CredentialId { get; set; }
[SupplyParameterFromForm(FormName = "add-passkey")]
- private PasskeyInputModel Input { get; set; } = default!;
+ private PasskeyInputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
index 80595b5bcc1e..b88b63240bf6 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
@@ -41,7 +41,7 @@
public string? Id { get; set; }
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
index 180b4d77a4cc..c7f01ca3ac1b 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
@@ -44,7 +44,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
index bee8e0530a88..5f395be6a1ab 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
@@ -58,7 +58,7 @@
private IEnumerable? identityErrors;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
index b6fdb411ec8a..68d38a05c057 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
@@ -37,7 +37,7 @@
private string? message;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
protected override void OnInitialized()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
index 74f258a1c518..a6008f73f01b 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
@@ -46,7 +46,7 @@
private IEnumerable? identityErrors;
[SupplyParameterFromForm]
- private InputModel Input { get; set; } = default!;
+ private InputModel Input { get; set; }
[SupplyParameterFromQuery]
private string? Code { get; set; }
From 41245e2aff7edb9c8acaae97f256e2b581e494db Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 07:25:52 +0000
Subject: [PATCH 08/10] Update analyzer to ignore default value initializers
and revert template changes
- Reverted template files to original state with `= default!` initializers
- Modified SupplyParameterFromFormAnalyzer to ignore default value initializers (null, null!, default, default!)
- Added comprehensive tests for ignored cases: = null, = null!, = default, = default!
- All 49 analyzer tests pass, confirming correct behavior
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../src/SupplyParameterFromFormAnalyzer.cs | 23 +++++++
.../SupplyParameterFromFormAnalyzerTest.cs | 64 +++++++++++++++++++
.../Account/Pages/ExternalLogin.razor | 2 +-
.../Account/Pages/ForgotPassword.razor | 2 +-
.../Components/Account/Pages/Login.razor | 2 +-
.../Account/Pages/LoginWith2fa.razor | 2 +-
.../Account/Pages/LoginWithRecoveryCode.razor | 2 +-
.../Account/Pages/Manage/ChangePassword.razor | 2 +-
.../Pages/Manage/DeletePersonalData.razor | 2 +-
.../Account/Pages/Manage/Email.razor | 2 +-
.../Pages/Manage/EnableAuthenticator.razor | 2 +-
.../Account/Pages/Manage/Index.razor | 2 +-
.../Account/Pages/Manage/Passkeys.razor | 2 +-
.../Account/Pages/Manage/RenamePasskey.razor | 2 +-
.../Account/Pages/Manage/SetPassword.razor | 2 +-
.../Components/Account/Pages/Register.razor | 2 +-
.../Pages/ResendEmailConfirmation.razor | 2 +-
.../Account/Pages/ResetPassword.razor | 2 +-
18 files changed, 103 insertions(+), 16 deletions(-)
diff --git a/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
index d81dfc70a567..00bbbcaa1180 100644
--- a/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
+++ b/src/Components/Analyzers/src/SupplyParameterFromFormAnalyzer.cs
@@ -45,6 +45,12 @@ public override void Initialize(AnalysisContext context)
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)
{
@@ -75,4 +81,21 @@ public override void Initialize(AnalysisContext context)
}, 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
+ };
+ }
}
\ No newline at end of file
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
index abbb194a27d8..9151027394d4 100644
--- a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -146,6 +146,70 @@ class InputModel
VerifyCSharpDiagnostic(test, expected);
}
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithNullInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = null;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithNullForgivingInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = null!;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithDefaultInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = default;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
+ [Fact]
+ public void IgnoresSupplyParameterFromFormWithDefaultForgivingInitializer()
+ {
+ var test = $@"
+ namespace ConsoleApplication1
+ {{
+ using {typeof(ParameterAttribute).Namespace};
+ class TestComponent : ComponentBase
+ {{
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = default!;
+ }}
+ }}" + TestDeclarations;
+
+ VerifyCSharpDiagnostic(test);
+ }
+
[Fact]
public void WorksWithInheritedComponentBase()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
index 58b34581ae7e..0c30821599fa 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ExternalLogin.razor
@@ -54,7 +54,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? RemoteError { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
index a63ec90b244f..ae82692ab7e9 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ForgotPassword.razor
@@ -35,7 +35,7 @@
@code {
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override void OnInitialized()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
index 32e78dae8f81..65347e0afdb6 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor
@@ -79,7 +79,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
index a21436d993b0..3d7243c9b775 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWith2fa.razor
@@ -49,7 +49,7 @@
private ApplicationUser user = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
index 313395ac9bde..f99d02900b12 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/LoginWithRecoveryCode.razor
@@ -38,7 +38,7 @@
private ApplicationUser user = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
index ca8561add275..c035566ae85b 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/ChangePassword.razor
@@ -48,7 +48,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
index 2982bafa7407..f9c247897223 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/DeletePersonalData.razor
@@ -46,7 +46,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
index c45829e6d4b6..94f6b6a473ca 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Email.razor
@@ -63,7 +63,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm(FormName = "change-email")]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
index 3316bac84a74..588134b5fa5a 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/EnableAuthenticator.razor
@@ -78,7 +78,7 @@ else
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
index bad84305bcbf..13faf41804f2 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Index.razor
@@ -41,7 +41,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
index 210e71dd3614..283f26de1897 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor
@@ -65,7 +65,7 @@ else
private string? CredentialId { get; set; }
[SupplyParameterFromForm(FormName = "add-passkey")]
- private PasskeyInputModel Input { get; set; }
+ private PasskeyInputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
index b88b63240bf6..80595b5bcc1e 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor
@@ -41,7 +41,7 @@
public string? Id { get; set; }
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
index c7f01ca3ac1b..180b4d77a4cc 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/SetPassword.razor
@@ -44,7 +44,7 @@
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
index 5f395be6a1ab..bee8e0530a88 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Register.razor
@@ -58,7 +58,7 @@
private IEnumerable? identityErrors;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
index 68d38a05c057..b6fdb411ec8a 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResendEmailConfirmation.razor
@@ -37,7 +37,7 @@
private string? message;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
protected override void OnInitialized()
{
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
index a6008f73f01b..74f258a1c518 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/ResetPassword.razor
@@ -46,7 +46,7 @@
private IEnumerable? identityErrors;
[SupplyParameterFromForm]
- private InputModel Input { get; set; }
+ private InputModel Input { get; set; } = default!;
[SupplyParameterFromQuery]
private string? Code { get; set; }
From cb2088ee195d393ded00a1303ef3d81af40ad75d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 09:11:19 +0000
Subject: [PATCH 09/10] Replace confusing 'default' string literal with
'initial-value' in analyzer tests
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
index 9151027394d4..df56f01b08d9 100644
--- a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -96,7 +96,7 @@ namespace ConsoleApplication1
using {typeof(ParameterAttribute).Namespace};
class TestComponent : ComponentBase
{{
- [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
}}
}}" + TestDeclarations;
@@ -223,7 +223,7 @@ class BaseComponent : ComponentBase
class TestComponent : BaseComponent
{{
- [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
}}
}}" + TestDeclarations;
From 023fae459ab439ba849ceec698cbafc7b00be793 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 Aug 2025 10:59:31 +0000
Subject: [PATCH 10/10] Replace remaining string literal 'default' with
'initial-value' in analyzer tests
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
index df56f01b08d9..549b3cf115ec 100644
--- a/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
+++ b/src/Components/Analyzers/test/SupplyParameterFromFormAnalyzerTest.cs
@@ -48,7 +48,7 @@ namespace ConsoleApplication1
using {typeof(ParameterAttribute).Namespace};
class TestComponent : ComponentBase
{{
- public string MyProperty {{ get; set; }} = ""default"";
+ public string MyProperty {{ get; set; }} = ""initial-value"";
}}
}}" + TestDeclarations;
@@ -80,7 +80,7 @@ namespace ConsoleApplication1
using {typeof(ParameterAttribute).Namespace};
class NotAComponent
{{
- [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""default"";
+ [SupplyParameterFromForm] public string MyProperty {{ get; set; }} = ""initial-value"";
}}
}}" + TestDeclarations;