Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -442,4 +442,13 @@ public static RazorDiagnostic CreateRenderModeAttribute_ComponentDeclaredRenderM
{
return RazorDiagnostic.Create(RenderModeAttribute_ComponentDeclaredRenderMode, source, component);
}

public static readonly RazorDiagnosticDescriptor UnknownComponentParameter =
new($"{DiagnosticPrefix}10025",
"The component '{0}' does not have a parameter named '{1}'.",
RazorDiagnosticSeverity.Warning,
warningLevel: 11);

public static RazorDiagnostic Create_UnknownComponentParameter(SourceSpan? source, string component, string parameter)
=> RazorDiagnostic.Create(UnknownComponentParameter, source, component, parameter);
}
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,16 @@ private bool TryGetAttributeStringContent(TagHelperPropertyIntermediateNode prop
public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node)
{
var attribute = new ComponentAttributeIntermediateNode(node);

// A splat cannot make an explicitly unmatched attribute valid.
Comment thread
davidwengier marked this conversation as resolved.
Outdated
if (!_component.Component.AcceptsUnmatchedAttributes())
{
attribute.AddDiagnostic(ComponentDiagnosticFactory.Create_UnknownComponentParameter(
node.AttributeNameSpan,
_component.TagName,
node.AttributeName));
}

_children.Add(attribute);

// Since we don't support complex content, we can rewrite the inside of this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,29 @@ public sealed record class ComponentMetadata() : MetadataObject(MetadataKind.Com

public bool IsGeneric { get; init; }
public bool HasRenderModeDirective { get; init; }
internal bool AcceptsUnmatchedAttributes { get; init; }

internal override bool HasDefaultValue => Equals(Default);

private protected override void BuildChecksum(in Checksum.Builder builder)
{
builder.Append(IsGeneric);
builder.Append(HasRenderModeDirective);
builder.Append(AcceptsUnmatchedAttributes);
}

public ref struct Builder
{
public bool IsGeneric { get; set; }
public bool HasRenderModeDirective { get; set; }
internal bool AcceptsUnmatchedAttributes { get; set; }

public readonly ComponentMetadata Build()
=> new()
{
IsGeneric = IsGeneric,
HasRenderModeDirective = HasRenderModeDirective
HasRenderModeDirective = HasRenderModeDirective,
AcceptsUnmatchedAttributes = AcceptsUnmatchedAttributes
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class ParameterAttribute
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.ParameterAttribute";
public const string MetadataName = FullTypeName;
public const string CaptureUnmatchedValues = nameof(CaptureUnmatchedValues);
}

public static class LayoutAttribute
Expand All @@ -50,6 +51,11 @@ public static class IDictionary
public const string MetadataName = "System.Collection.IDictionary`2";
}

public static class Dictionary
{
public const string MetadataName = "System.Collections.Generic.Dictionary`2";
}

public static class IComponentRenderMode
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponentRenderMode";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public static bool IsGenericTypedComponent(this TagHelperDescriptor tagHelper)
Metadata: ComponentMetadata { IsGeneric: true }
};

public static bool AcceptsUnmatchedAttributes(this TagHelperDescriptor tagHelper)
=> tagHelper is
{
Kind: TagHelperKind.Component,
Metadata: ComponentMetadata { AcceptsUnmatchedAttributes: true }
};

/// <summary>
/// Given a taghelper binding it finds the BoundAttribute that is a type parameter and then the
/// actual binding value for that type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ private static void ConvertToUnresolvedUnboundAttribute(
{
AttributeName = attributeName,
AttributeStructure = unresolvedAttr.AttributeStructure,
AttributeNameSpan = unresolvedAttr.AttributeNameSpan,
};

if (!unresolvedAttr.IsMinimized && unresolvedAttr.AsTagHelperAttribute is HtmlAttributeIntermediateNode fallbackAttr)
Expand Down Expand Up @@ -928,6 +929,7 @@ private static void ConvertComponentAttributeToTagHelper(
{
AttributeName = attributeName,
AttributeStructure = InferAttributeStructure(htmlAttr),
AttributeNameSpan = attributeNameSpan,
};

thHtml.Children.AddRange(htmlAttr.Children);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public sealed class TagHelperHtmlAttributeIntermediateNode : IntermediateNode
{
public required string AttributeName { get; init; }
public required AttributeStructure AttributeStructure { get; init; }
internal SourceSpan? AttributeNameSpan { get; init; }

public override IntermediateNodeCollection Children { get => field ??= []; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ internal sealed partial class ComponentTagHelperProducer : TagHelperProducer
{
private readonly Compilation _compilation;
private readonly BindTagHelperProducer? _bindTagHelperProducer;
private readonly INamedTypeSymbol? _dictionaryOfStringObject;

private ComponentTagHelperProducer(Compilation compilation, BindTagHelperProducer? bindTagHelperProducer)
{
_compilation = compilation;
_bindTagHelperProducer = bindTagHelperProducer;

if (compilation.GetTypeByMetadataName(ComponentsApi.Dictionary.MetadataName) is { } dictionaryType)
{
_dictionaryOfStringObject = dictionaryType.Construct(
compilation.GetSpecialType(SpecialType.System_String),
compilation.GetSpecialType(SpecialType.System_Object));
}
}

public override TagHelperProducerKind Kind => TagHelperProducerKind.Component;
Expand All @@ -47,15 +55,18 @@ public override void AddTagHelpersForType(
// First, compute the relevant properties for this type so that we
// don't need to compute them twice.
var properties = GetProperties(type);
var acceptsUnmatchedAttributes = AcceptsUnmatchedAttributes(properties);

var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(_compilation, type, properties);
var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(
_compilation, type, properties, acceptsUnmatchedAttributes);
results.Add(shortNameMatchingDescriptor);

// If the component is in the global namespace, skip adding this descriptor which will be the same as the short name one.
TagHelperDescriptor? fullyQualifiedNameMatchingDescriptor = null;
if (!type.ContainingNamespace.IsGlobalNamespace)
{
fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(_compilation, type, properties);
fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(
_compilation, type, properties, acceptsUnmatchedAttributes);
results.Add(fullyQualifiedNameMatchingDescriptor);
}

Expand Down Expand Up @@ -84,19 +95,22 @@ public override void AddTagHelpersForType(
private static TagHelperDescriptor CreateShortNameMatchingDescriptor(
Compilation compilation,
INamedTypeSymbol type,
ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties)
=> CreateNameMatchingDescriptor(compilation, type, properties, fullyQualified: false);
ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties,
bool acceptsUnmatchedAttributes)
=> CreateNameMatchingDescriptor(compilation, type, properties, acceptsUnmatchedAttributes, fullyQualified: false);

private static TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor(
Compilation compilation,
INamedTypeSymbol type,
ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties)
=> CreateNameMatchingDescriptor(compilation, type, properties, fullyQualified: true);
ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties,
bool acceptsUnmatchedAttributes)
=> CreateNameMatchingDescriptor(compilation, type, properties, acceptsUnmatchedAttributes, fullyQualified: true);

private static TagHelperDescriptor CreateNameMatchingDescriptor(
Compilation compilation,
INamedTypeSymbol type,
ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties,
bool acceptsUnmatchedAttributes,
bool fullyQualified)
{
var typeName = TypeNameObject.From(type);
Expand All @@ -109,6 +123,7 @@ private static TagHelperDescriptor CreateNameMatchingDescriptor(
builder.SetTypeName(typeName);

var metadata = new ComponentMetadata.Builder();
metadata.AcceptsUnmatchedAttributes = acceptsUnmatchedAttributes;

builder.CaseSensitive = true;

Expand Down Expand Up @@ -193,6 +208,59 @@ private static TagHelperDescriptor CreateNameMatchingDescriptor(
return builder.Build();
}

private bool AcceptsUnmatchedAttributes(ImmutableArray<(IPropertySymbol property, PropertyKind kind)> properties)
{
IPropertySymbol? captureUnmatchedValuesProperty = null;

foreach (var (property, kind) in properties)
{
if (kind == PropertyKind.Ignored || !HasCaptureUnmatchedValues(property))
{
continue;
}

if (captureUnmatchedValuesProperty is not null)
{
// A component cannot have multiple parameters that capture unmatched values.
return false;
}

captureUnmatchedValuesProperty = property;
}

if (captureUnmatchedValuesProperty is null || _dictionaryOfStringObject is null)
{
return false;
}

var conversion = _compilation.ClassifyConversion(_dictionaryOfStringObject, captureUnmatchedValuesProperty.Type);

// The runtime assigns this dictionary through reflection, so user-defined conversions do not apply.
return conversion.IsIdentity || (conversion.IsImplicit && conversion.IsReference);

static bool HasCaptureUnmatchedValues(IPropertySymbol property)
{
var parameterAttribute = property.GetAttributes().FirstOrDefault(
static attribute => attribute.HasFullName(ComponentsApi.ParameterAttribute.MetadataName));

if (parameterAttribute is null)
{
return false;
}

foreach (var (name, value) in parameterAttribute.NamedArguments)
{
if (name == ComponentsApi.ParameterAttribute.CaptureUnmatchedValues &&
value.Value is true)
{
return true;
}
}

return false;
}
}

private static void CreateProperty(Compilation compilation, TagHelperDescriptorBuilder builder, INamedTypeSymbol containingSymbol, IPropertySymbol property, PropertyKind kind)
{
builder.BindAttribute(pb =>
Expand Down