Skip to content

Commit

Permalink
Improve MA0115 to detect pascal-case parameters in a component with C…
Browse files Browse the repository at this point in the history
…aptureUnmatchedValues
  • Loading branch information
meziantou committed May 17, 2024
1 parent 01206ef commit 23b9118
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Polyfill" Version="1.0.37">
<PackageReference Include="Meziantou.Polyfill" Version="1.0.38">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
26 changes: 25 additions & 1 deletion docs/Rules/MA0115.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,29 @@ Detect usage of invalid parameter in Razor components.
```razor
<SampleComponent
Text="Dummy"
InvalidParameter="Dummy" /> // 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<string, object> 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
*@
<MyComponent validAttribute="value" InvalidParameter="value" />
````
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard1.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<Description>Annotations to configure Meziantou.Analyzer</Description>
Expand Down
5 changes: 2 additions & 3 deletions src/Meziantou.Analyzer/Internals/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ public static T WithoutTrailingSpacesTrivia<T>(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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand All @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions tests/Meziantou.Analyzer.Test/Meziantou.Analyzer.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2">
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="System.Reflection.Metadata" Version="8.0.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SampleComponent>(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<SampleComponent>(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
Expand All @@ -136,6 +179,7 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
""";
await CreateProjectBuilder()
.WithSourceCode(Usings + sourceCode + ComponentWithCaptureUnmatchedValues)
.AddAnalyzerConfiguration("MA0115.ReportPascalCaseUnmatchedParameter", "false")
.ValidateAsync();
}

Expand Down

0 comments on commit 23b9118

Please sign in to comment.