diff --git a/Directory.Build.props b/Directory.Build.props index d84ef556..57bb1b55 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -38,7 +38,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers diff --git a/docs/Rules/MA0115.md b/docs/Rules/MA0115.md index 069e307f..ebc2b0e7 100644 --- a/docs/Rules/MA0115.md +++ b/docs/Rules/MA0115.md @@ -15,5 +15,29 @@ Detect usage of invalid parameter in Razor components. ```razor // Report diagnostic as InvalidParameter does not exist in SampleComponent + InvalidParameter="Dummy" /> // Report diagnostic as `InvalidParameter` does not exist in SampleComponent ``` + +In the case where the component allows for unmatched parameters, you can still detect parameters that are in PascalCase. + +```.editorconfig +MA0115.ReportPascalCaseUnmatchedParameter +``` + +In the following example, `Param` is reported as an unmatched parameter. + +````c# +class MyComponent : ComponentBase +{ + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary AdditionalAttributes { get; set; } +} +```` + +````razor +@* +attribute1 is valid as it starts with a lowercase character +InvalidParameter is not valid as it starts with an uppercase character +*@ + +```` diff --git a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj index 4be4466c..443c4c62 100644 --- a/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj +++ b/src/Meziantou.Analyzer.Annotations/Meziantou.Analyzer.Annotations.csproj @@ -1,7 +1,7 @@  - netstandard1.0;netstandard2.0 + netstandard2.0 1.0.0 1.0.0 Annotations to configure Meziantou.Analyzer diff --git a/src/Meziantou.Analyzer/Internals/SyntaxNodeExtensions.cs b/src/Meziantou.Analyzer/Internals/SyntaxNodeExtensions.cs index 4607ef08..f4c37cc7 100644 --- a/src/Meziantou.Analyzer/Internals/SyntaxNodeExtensions.cs +++ b/src/Meziantou.Analyzer/Internals/SyntaxNodeExtensions.cs @@ -10,8 +10,7 @@ public static T WithoutTrailingSpacesTrivia(this T syntaxNode) where T : Synt if (!syntaxNode.HasTrailingTrivia) return syntaxNode; - var trivia = syntaxNode.GetTrailingTrivia().Reverse(); - var newTrivia = trivia.SkipWhile(t => t.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.WhitespaceTrivia)); - return syntaxNode.WithTrailingTrivia(newTrivia.Reverse()); + return syntaxNode.WithTrailingTrivia( + syntaxNode.GetTrailingTrivia().Reverse().SkipWhile(t => t.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.WhitespaceTrivia)).Reverse()); } } diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzer.cs index f1b2b664..2c104850 100644 --- a/src/Meziantou.Analyzer/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Meziantou.Analyzer.Configurations; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -81,7 +82,8 @@ public void AnalyzeBlockOptions(OperationAnalysisContext context) var value = invocation.Arguments[1].Value.ConstantValue; if (value.HasValue && value.Value is string parameterName) { - if (!IsValidAttribute(currentComponent, parameterName)) + var reportPascalCaseUnmatchedParameter = context.Options.GetConfigurationValue(operation, "MA0115.ReportPascalCaseUnmatchedParameter", defaultValue: true); + if (!IsValidAttribute(currentComponent, parameterName, reportPascalCaseUnmatchedParameter)) { context.ReportDiagnostic(Rule, invocation.Syntax, parameterName, currentComponent.ToDisplayString(NullableFlowState.None)); } @@ -94,11 +96,16 @@ public void AnalyzeBlockOptions(OperationAnalysisContext context) } } - private bool IsValidAttribute(ITypeSymbol componentType, string parameterName) + private bool IsValidAttribute(ITypeSymbol componentType, string parameterName, bool reportPascalCaseUnmatchedParameter) { var descriptor = GetComponentDescriptor(componentType); if (descriptor.HasMatchUnmatchedParameters) + { + if (reportPascalCaseUnmatchedParameter && parameterName.Length > 0 && char.IsUpper(parameterName[0]) && !descriptor.Parameters.Contains(parameterName)) + return false; + return true; + } if (descriptor.Parameters.Contains(parameterName)) return true; diff --git a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj index 4f07f2c6..75638da6 100644 --- a/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj +++ b/tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj @@ -8,19 +8,22 @@ - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzerTests.cs index d96fad80..c69a1ad4 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseUnknownParameterForRazorComponentAnalyzerTests.cs @@ -116,12 +116,55 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Theory] + [InlineData("Param1")] + [InlineData("Param2")] + [InlineData("unknownParams")] + public async Task ComponentWithCaptureUnmatchedValues_AnyLowercaseParameterIsValid(string parameterName) + { + var sourceCode = $$""" +class TypeName : ComponentBase +{ + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { + __builder.OpenComponent(0); + __builder.AddAttribute(1, "{{parameterName}}", "test"); + __builder.CloseComponent(); + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(Usings + sourceCode + ComponentWithCaptureUnmatchedValues) + .ValidateAsync(); + } + + [Theory] + [InlineData("UnknownParams")] + public async Task ComponentWithCaptureUnmatchedValues_PascalCaseParameterIsInvalid(string parameterName) + { + var sourceCode = $$""" +class TypeName : ComponentBase +{ + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { + __builder.OpenComponent(0); + [||]__builder.AddAttribute(1, "{{parameterName}}", "test"); + __builder.CloseComponent(); + } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(Usings + sourceCode + ComponentWithCaptureUnmatchedValues) + .AddAnalyzerConfiguration("MA0115.ReportPascalCaseUnmatchedParameter", "true") + .ValidateAsync(); + } + [Theory] [InlineData("Param1")] [InlineData("Param2")] [InlineData("Param3")] [InlineData("UnknownParams")] - public async Task ComponentWithCaptureUnmatchedValues_AnyValueIsValid(string parameterName) + public async Task ComponentWithCaptureUnmatchedValues_PascalCaseParameterIsValid(string parameterName) { var sourceCode = $$""" class TypeName : ComponentBase @@ -136,6 +179,7 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin """; await CreateProjectBuilder() .WithSourceCode(Usings + sourceCode + ComponentWithCaptureUnmatchedValues) + .AddAnalyzerConfiguration("MA0115.ReportPascalCaseUnmatchedParameter", "false") .ValidateAsync(); }