Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add runtime support for Blazor attribute splatting #10357

Merged
merged 19 commits into from
May 29, 2019
Merged
89 changes: 89 additions & 0 deletions src/Components/Analyzers/src/ComponentFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Components.Analyzers
{
internal static class ComponentFacts
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
public static bool IsAnyParameter(ComponentSymbols symbols, IPropertySymbol property)
{
if (symbols == null)
{
throw new ArgumentNullException(nameof(symbols));
}

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

return property.GetAttributes().Any(a =>
{
return a.AttributeClass == symbols.ParameterAttribute || a.AttributeClass == symbols.CascadingParameterAttribute;
});
}

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

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

return property.GetAttributes().Any(a => a.AttributeClass == symbols.ParameterAttribute);
}

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

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

var attribute = property.GetAttributes().FirstOrDefault(a => a.AttributeClass == symbols.ParameterAttribute);
if (attribute == null)
{
return false;
}

foreach (var kvp in attribute.NamedArguments)
{
if (string.Equals(kvp.Key, ComponentsApi.ParameterAttribute.CaptureUnmatchedValues, StringComparison.Ordinal))
{
return kvp.Value.Value as bool? ?? false;
}
}

return false;
}

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

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

return property.GetAttributes().Any(a => a.AttributeClass == symbols.CascadingParameterAttribute);
}
}
}
111 changes: 111 additions & 0 deletions src/Components/Analyzers/src/ComponentParameterAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.AspNetCore.Components.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ComponentParameterAnalyzer : DiagnosticAnalyzer
{
public ComponentParameterAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(new[]
{
DiagnosticDescriptors.ComponentParametersShouldNotBePublic,
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
});
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }

public override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
rynowak marked this conversation as resolved.
Show resolved Hide resolved
{
// Types we need are not defined.
return;
}

// This operates per-type because one of the validations we need has to look for duplicates
// defined on the same type.
context.RegisterSymbolStartAction(context =>
{
var properties = new List<IPropertySymbol>();

var type = (INamedTypeSymbol)context.Symbol;
foreach (var member in type.GetMembers())
{
if (member is IPropertySymbol property && ComponentFacts.IsAnyParameter(symbols, property))
{
// Annotated with [Parameter] or [CascadingParameter]
properties.Add(property);
}
}

if (properties.Count == 0)
{
return;
}

context.RegisterSymbolEndAction(context =>
{
var captureUnmatchedValuesParameters = new List<IPropertySymbol>();

// Per-property validations
foreach (var property in properties)
{
if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParametersShouldNotBePublic,
property.Locations[0],
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}

if (ComponentFacts.IsParameterWithCaptureUnmatchedValues(symbols, property))
{
captureUnmatchedValuesParameters.Add(property);

// Check the type, we need to be able to assign a Dictionary<string, object>
var conversion = context.Compilation.ClassifyConversion(symbols.ParameterCaptureUnmatchedValuesRuntimeType, property.Type);
if (!conversion.Exists || conversion.IsExplicit)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
property.Locations[0],
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
symbols.ParameterCaptureUnmatchedValuesRuntimeType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
}
}

// Check if the type defines multiple CaptureUnmatchedValues parameters. Doing this outside the loop means we place the
// errors on the type.
if (captureUnmatchedValuesParameters.Count > 1)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
context.Symbol.Locations[0],
type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
Environment.NewLine,
string.Join(
Environment.NewLine,
captureUnmatchedValuesParameters.Select(p => p.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)).OrderBy(n => n))));
}
});
}, SymbolKind.NamedType);
});
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading.Tasks;

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

public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(ComponentParametersShouldNotBePublicAnalyzer.DiagnosticId);
=> ImmutableArray.Create(DiagnosticDescriptors.ComponentParametersShouldNotBePublic.Id);

public sealed override FixAllProvider GetFixAllProvider()
{
Expand Down
65 changes: 65 additions & 0 deletions src/Components/Analyzers/src/ComponentSymbols.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Components.Analyzers
{
internal class ComponentSymbols
{
public static bool TryCreate(Compilation compilation, out ComponentSymbols symbols)
{
if (compilation == null)
{
throw new ArgumentNullException(nameof(compilation));
}

var parameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.ParameterAttribute.MetadataName);
if (parameterAttribute == null)
{
symbols = null;
return false;
}

var cascadingParameterAttribute = compilation.GetTypeByMetadataName(ComponentsApi.CascadingParameterAttribute.MetadataName);
if (cascadingParameterAttribute == null)
{
symbols = null;
return false;
}

var dictionary = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2");
var @string = compilation.GetSpecialType(SpecialType.System_String);
var @object = compilation.GetSpecialType(SpecialType.System_Object);
if (dictionary == null || @string == null || @object == null)
{
symbols = null;
return false;
}

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

symbols = new ComponentSymbols(parameterAttribute, cascadingParameterAttribute, parameterCaptureUnmatchedValuesRuntimeType);
return true;
}

private ComponentSymbols(
INamedTypeSymbol parameterAttribute,
INamedTypeSymbol cascadingParameterAttribute,
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType)
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
}

public INamedTypeSymbol ParameterAttribute { get; }

// Dictionary<string, object>
public INamedTypeSymbol ParameterCaptureUnmatchedValuesRuntimeType { get; }

public INamedTypeSymbol CascadingParameterAttribute { get; }
}
}
10 changes: 9 additions & 1 deletion src/Components/Analyzers/src/ComponentsApi.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.AspNetCore.Components.Shared
namespace Microsoft.AspNetCore.Components.Analyzers
{
// Constants for type and method names used in code-generation
// Keep these in sync with the actual definitions
Expand All @@ -13,6 +13,14 @@ public static class ParameterAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
public static readonly string MetadataName = FullTypeName;

public static readonly string CaptureUnmatchedValues = "CaptureUnmatchedValues";
}

public static class CascadingParameterAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.CascadingParameterAttribute";
public static readonly string MetadataName = FullTypeName;
}
}
}
Loading