Skip to content

Commit 6ca30bb

Browse files
authored
Add runtime support for Blazor attribute splatting (#10357)
* Add basic tests for duplicate attributes * Add AddMultipleAttributes improve RTB - Adds AddMultipleAttributes - Fix RTB to de-dupe attributes - Fix RTB behaviour with boxed EventCallback (#8336) - Add lots of tests for new RTB behaviour and EventCallback * Harden EventCallback test This was being flaky while I was running E2E tests locally, and it wasn't using a resiliant equality comparison. * Add new setting on ParameterAttribute Adds the option to mark a parameter as an *extra* parameter. Why is this on ParameterAttribute and not a new type? It makes sense to make it a modifier on Parameter so you can use it both ways (explicitly set it, or allow it to collect *extras*). Added unit tests and validations for interacting with the new setting. * Add renderer tests for 'extra' parameters * Refactor Diagnostics for more analyzers * Simplify analyzer and fix CascadingParameter This is the *easy way* to write an analyzer that looks at declarations. The information that's avaialable from symbols is much more high level than syntax. Much of what's in this code today is needed to reverse engineer what the compiler does already. If you use symbols you get to benefit from all of that. Also added validation for cascading parameters to the analyzer that I think was just missing due to oversight. The overall design pattern here is what I've been converging on for the ASP.NET Core analyzers as a whole, and it seems to scale really well. * Add analyzer for types * Add analyzer for uniqueness This involved a refactor to run the analyzer per-type instead of per-property. * Fix project file * Adjust name * PR feedback on PCE and more renames * Remove unused parameter * Fix #10398 * Add E2E test * Pranavs cool feedback * Optimize silent frame removal * code check * pr feedback
1 parent 9f9c79b commit 6ca30bb

30 files changed

+2060
-169
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
8+
namespace Microsoft.AspNetCore.Components.Analyzers
9+
{
10+
internal static class ComponentFacts
11+
{
12+
public static bool IsAnyParameter(ComponentSymbols symbols, IPropertySymbol property)
13+
{
14+
if (symbols == null)
15+
{
16+
throw new ArgumentNullException(nameof(symbols));
17+
}
18+
19+
if (property == null)
20+
{
21+
throw new ArgumentNullException(nameof(property));
22+
}
23+
24+
return property.GetAttributes().Any(a =>
25+
{
26+
return a.AttributeClass == symbols.ParameterAttribute || a.AttributeClass == symbols.CascadingParameterAttribute;
27+
});
28+
}
29+
30+
public static bool IsParameter(ComponentSymbols symbols, IPropertySymbol property)
31+
{
32+
if (symbols == null)
33+
{
34+
throw new ArgumentNullException(nameof(symbols));
35+
}
36+
37+
if (property == null)
38+
{
39+
throw new ArgumentNullException(nameof(property));
40+
}
41+
42+
return property.GetAttributes().Any(a => a.AttributeClass == symbols.ParameterAttribute);
43+
}
44+
45+
public static bool IsParameterWithCaptureUnmatchedValues(ComponentSymbols symbols, IPropertySymbol property)
46+
{
47+
if (symbols == null)
48+
{
49+
throw new ArgumentNullException(nameof(symbols));
50+
}
51+
52+
if (property == null)
53+
{
54+
throw new ArgumentNullException(nameof(property));
55+
}
56+
57+
var attribute = property.GetAttributes().FirstOrDefault(a => a.AttributeClass == symbols.ParameterAttribute);
58+
if (attribute == null)
59+
{
60+
return false;
61+
}
62+
63+
foreach (var kvp in attribute.NamedArguments)
64+
{
65+
if (string.Equals(kvp.Key, ComponentsApi.ParameterAttribute.CaptureUnmatchedValues, StringComparison.Ordinal))
66+
{
67+
return kvp.Value.Value as bool? ?? false;
68+
}
69+
}
70+
71+
return false;
72+
}
73+
74+
public static bool IsCascadingParameter(ComponentSymbols symbols, IPropertySymbol property)
75+
{
76+
if (symbols == null)
77+
{
78+
throw new ArgumentNullException(nameof(symbols));
79+
}
80+
81+
if (property == null)
82+
{
83+
throw new ArgumentNullException(nameof(property));
84+
}
85+
86+
return property.GetAttributes().Any(a => a.AttributeClass == symbols.CascadingParameterAttribute);
87+
}
88+
}
89+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
namespace Microsoft.AspNetCore.Components.Analyzers
13+
{
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class ComponentParameterAnalyzer : DiagnosticAnalyzer
16+
{
17+
public ComponentParameterAnalyzer()
18+
{
19+
SupportedDiagnostics = ImmutableArray.Create(new[]
20+
{
21+
DiagnosticDescriptors.ComponentParametersShouldNotBePublic,
22+
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
23+
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
24+
});
25+
}
26+
27+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
28+
29+
public override void Initialize(AnalysisContext context)
30+
{
31+
context.RegisterCompilationStartAction(context =>
32+
{
33+
if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
34+
{
35+
// Types we need are not defined.
36+
return;
37+
}
38+
39+
// This operates per-type because one of the validations we need has to look for duplicates
40+
// defined on the same type.
41+
context.RegisterSymbolStartAction(context =>
42+
{
43+
var properties = new List<IPropertySymbol>();
44+
45+
var type = (INamedTypeSymbol)context.Symbol;
46+
foreach (var member in type.GetMembers())
47+
{
48+
if (member is IPropertySymbol property && ComponentFacts.IsAnyParameter(symbols, property))
49+
{
50+
// Annotated with [Parameter] or [CascadingParameter]
51+
properties.Add(property);
52+
}
53+
}
54+
55+
if (properties.Count == 0)
56+
{
57+
return;
58+
}
59+
60+
context.RegisterSymbolEndAction(context =>
61+
{
62+
var captureUnmatchedValuesParameters = new List<IPropertySymbol>();
63+
64+
// Per-property validations
65+
foreach (var property in properties)
66+
{
67+
if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public)
68+
{
69+
context.ReportDiagnostic(Diagnostic.Create(
70+
DiagnosticDescriptors.ComponentParametersShouldNotBePublic,
71+
property.Locations[0],
72+
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
73+
}
74+
75+
if (ComponentFacts.IsParameterWithCaptureUnmatchedValues(symbols, property))
76+
{
77+
captureUnmatchedValuesParameters.Add(property);
78+
79+
// Check the type, we need to be able to assign a Dictionary<string, object>
80+
var conversion = context.Compilation.ClassifyConversion(symbols.ParameterCaptureUnmatchedValuesRuntimeType, property.Type);
81+
if (!conversion.Exists || conversion.IsExplicit)
82+
{
83+
context.ReportDiagnostic(Diagnostic.Create(
84+
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
85+
property.Locations[0],
86+
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
87+
property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
88+
symbols.ParameterCaptureUnmatchedValuesRuntimeType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
89+
}
90+
}
91+
}
92+
93+
// Check if the type defines multiple CaptureUnmatchedValues parameters. Doing this outside the loop means we place the
94+
// errors on the type.
95+
if (captureUnmatchedValuesParameters.Count > 1)
96+
{
97+
context.ReportDiagnostic(Diagnostic.Create(
98+
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
99+
context.Symbol.Locations[0],
100+
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
101+
Environment.NewLine,
102+
string.Join(
103+
Environment.NewLine,
104+
captureUnmatchedValuesParameters.Select(p => p.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)).OrderBy(n => n))));
105+
}
106+
});
107+
}, SymbolKind.NamedType);
108+
});
109+
}
110+
}
111+
}

src/Components/Analyzers/src/ComponentParametersShouldNotBePublicAnalyzer.cs

Lines changed: 0 additions & 76 deletions
This file was deleted.

src/Components/Analyzers/src/ComponentParametersShouldNotBePublicCodeFixProvider.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Linq;
7+
using System.Threading.Tasks;
48
using Microsoft.CodeAnalysis;
59
using Microsoft.CodeAnalysis.CodeActions;
610
using Microsoft.CodeAnalysis.CodeFixes;
711
using Microsoft.CodeAnalysis.CSharp;
812
using Microsoft.CodeAnalysis.CSharp.Syntax;
9-
using System.Collections.Immutable;
10-
using System.Composition;
11-
using System.Linq;
12-
using System.Threading.Tasks;
1313

1414
namespace Microsoft.AspNetCore.Components.Analyzers
1515
{
@@ -19,7 +19,7 @@ public class ComponentParametersShouldNotBePublicCodeFixProvider : CodeFixProvid
1919
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));
2020

2121
public override ImmutableArray<string> FixableDiagnosticIds
22-
=> ImmutableArray.Create(ComponentParametersShouldNotBePublicAnalyzer.DiagnosticId);
22+
=> ImmutableArray.Create(DiagnosticDescriptors.ComponentParametersShouldNotBePublic.Id);
2323

2424
public sealed override FixAllProvider GetFixAllProvider()
2525
{
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.CodeAnalysis;
7+
8+
namespace Microsoft.AspNetCore.Components.Analyzers
9+
{
10+
internal class ComponentSymbols
11+
{
12+
public static bool TryCreate(Compilation compilation, out ComponentSymbols symbols)
13+
{
14+
if (compilation == null)
15+
{
16+
throw new ArgumentNullException(nameof(compilation));
17+
}
18+
19+
var parameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.ParameterAttribute.MetadataName);
20+
if (parameterAttribute == null)
21+
{
22+
symbols = null;
23+
return false;
24+
}
25+
26+
var cascadingParameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.CascadingParameterAttribute.MetadataName);
27+
if (cascadingParameterAttribute == null)
28+
{
29+
symbols = null;
30+
return false;
31+
}
32+
33+
var dictionary = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");
34+
var @string = compilation.GetSpecialType(SpecialType.System_String);
35+
var @object = compilation.GetSpecialType(SpecialType.System_Object);
36+
if (dictionary == null || @string == null || @object == null)
37+
{
38+
symbols = null;
39+
return false;
40+
}
41+
42+
var parameterCaptureUnmatchedValuesRuntimeType = dictionary.Construct(@string, @object);
43+
44+
symbols = new ComponentSymbols(parameterAttribute, cascadingParameterAttribute, parameterCaptureUnmatchedValuesRuntimeType);
45+
return true;
46+
}
47+
48+
private ComponentSymbols(
49+
INamedTypeSymbol parameterAttribute,
50+
INamedTypeSymbol cascadingParameterAttribute,
51+
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType)
52+
{
53+
ParameterAttribute = parameterAttribute;
54+
CascadingParameterAttribute = cascadingParameterAttribute;
55+
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
56+
}
57+
58+
public INamedTypeSymbol ParameterAttribute { get; }
59+
60+
// Dictionary<string, object>
61+
public INamedTypeSymbol ParameterCaptureUnmatchedValuesRuntimeType { get; }
62+
63+
public INamedTypeSymbol CascadingParameterAttribute { get; }
64+
}
65+
}

src/Components/Analyzers/src/ComponentsApi.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
namespace Microsoft.AspNetCore.Components.Shared
4+
namespace Microsoft.AspNetCore.Components.Analyzers
55
{
66
// Constants for type and method names used in code-generation
77
// Keep these in sync with the actual definitions
@@ -13,6 +13,14 @@ public static class ParameterAttribute
1313
{
1414
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
1515
public static readonly string MetadataName = FullTypeName;
16+
17+
public static readonly string CaptureUnmatchedValues = "CaptureUnmatchedValues";
18+
}
19+
20+
public static class CascadingParameterAttribute
21+
{
22+
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.CascadingParameterAttribute";
23+
public static readonly string MetadataName = FullTypeName;
1624
}
1725
}
1826
}

0 commit comments

Comments
 (0)