|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
public class DefaultTagHelperDescriptorFactoryTest : TagHelperDescriptorProviderTestBase
{
public DefaultTagHelperDescriptorFactoryTest() : base(AdditionalCode)
{
Compilation = BaseCompilation;
}
private Compilation Compilation { get; }
public static TheoryData<string, Action<RequiredAttributeDescriptorBuilder>> RequiredAttributeParserErrorData
=> new()
{
("name,", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("name,"))),
(" ", static b => b
.Name(string.Empty)
.AddDiagnostic(AspNetCore.Razor.Language.RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeNameNullOrWhitespace())),
("n@me", static b => b
.Name("n@me")
.AddDiagnostic(AspNetCore.Razor.Language.RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeName("n@me", '@'))),
("name extra", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeCharacter('e', "name extra"))),
("[[ ", static b => b
.Name("[")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[[ "))
),
("[ ", static b => b
.Name("")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[ "))
),
("[name='unended]", static b => b
.Name("name")
.ValueComparison(RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeMismatchedQuotes('\'', "[name='unended]"))
),
("[name='unended", static b => b
.Name("name")
.ValueComparison(RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeMismatchedQuotes('\'', "[name='unended"))
),
("[name", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[name"))
),
("[ ]", static b => b
.Name(string.Empty)
.AddDiagnostic(AspNetCore.Razor.Language.RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeNameNullOrWhitespace())
),
("[name='unended]", static b => b
.Name("name")
.ValueComparison(RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeMismatchedQuotes('\'', "[name='unended]"))
),
("[name='unended", static b => b
.Name("name")
.ValueComparison(RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeMismatchedQuotes('\'', "[name='unended"))
),
("[name", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[name"))
),
("[n@me]", static b => b
.Name("n@me")
.AddDiagnostic(AspNetCore.Razor.Language.RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeName("n@me", '@'))
),
("[name@]", static b => b
.Name("name@")
.AddDiagnostic(AspNetCore.Razor.Language.RazorDiagnosticFactory.CreateTagHelper_InvalidTargetedAttributeName("name@", '@'))
),
("[name^]", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_PartialRequiredAttributeOperator('^', "[name^]"))
),
("[name='value'", static b => b
.Name("name")
.Value("value", RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[name='value'"))
),
("[name ", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[name "))
),
("[name extra]", static b => b
.Name("name")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeOperator('e', "[name extra]"))
),
("[name=value ", static b => b
.Name("name")
.Value("value", RequiredAttributeValueComparison.FullMatch)
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace("[name=value "))
)
};
[Theory]
[MemberData(nameof(RequiredAttributeParserErrorData))]
public void RequiredAttributeParser_ParsesRequiredAttributesAndLogsDiagnosticsCorrectly(
string requiredAttributes,
Action<RequiredAttributeDescriptorBuilder> configure)
{
// Arrange
var expected = TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "Test")
.TagMatchingRuleDescriptor(rule =>
{
rule.Attribute(configure);
})
.Build();
// Act
var actual = TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "Test")
.TagMatchingRuleDescriptor(rule =>
{
RequiredAttributeParser.AddRequiredAttributes(requiredAttributes, rule);
})
.Build();
// Assert
Assert.Equal<RequiredAttributeDescriptor>(expected.TagMatchingRules[0].Attributes, actual.TagMatchingRules[0].Attributes);
}
public static TheoryData<string, ImmutableArray<Action<RequiredAttributeDescriptorBuilder>>> RequiredAttributeParserData
{
get
{
return new()
{
(null!, []),
(string.Empty, []),
("name", [plain("name", RequiredAttributeNameComparison.FullMatch)]),
("name-*", [plain("name-", RequiredAttributeNameComparison.PrefixMatch)]),
(" name-* ", [plain("name-", RequiredAttributeNameComparison.PrefixMatch)]),
("asp-route-*,valid , name-* ,extra", [
plain("asp-route-", RequiredAttributeNameComparison.PrefixMatch),
plain("valid", RequiredAttributeNameComparison.FullMatch),
plain("name-", RequiredAttributeNameComparison.PrefixMatch),
plain("extra", RequiredAttributeNameComparison.FullMatch)]),
("[name]", [css("name", null, RequiredAttributeValueComparison.None)]),
("[ name ]", [css("name", null, RequiredAttributeValueComparison.None)]),
(" [ name ] ", [css("name", null, RequiredAttributeValueComparison.None)]),
("[name=]", [css("name", "", RequiredAttributeValueComparison.FullMatch)]),
("[name='']", [css("name", "", RequiredAttributeValueComparison.FullMatch)]),
("[name ^=]", [css("name", "", RequiredAttributeValueComparison.PrefixMatch)]),
("[name=hello]", [css("name", "hello", RequiredAttributeValueComparison.FullMatch)]),
("[name= hello]", [css("name", "hello", RequiredAttributeValueComparison.FullMatch)]),
("[name='hello']", [css("name", "hello", RequiredAttributeValueComparison.FullMatch)]),
("[name=\"hello\"]", [css("name", "hello", RequiredAttributeValueComparison.FullMatch)]),
(" [ name $= \" hello\" ] ", [css("name", " hello", RequiredAttributeValueComparison.SuffixMatch)]),
("[name=\"hello\"],[other^=something ], [val = 'cool']", [
css("name", "hello", RequiredAttributeValueComparison.FullMatch),
css("other", "something", RequiredAttributeValueComparison.PrefixMatch),
css("val", "cool", RequiredAttributeValueComparison.FullMatch)]),
("asp-route-*,[name=\"hello\"],valid ,[other^=something ], name-* ,[val = 'cool'],extra", [
plain("asp-route-", RequiredAttributeNameComparison.PrefixMatch),
css("name", "hello", RequiredAttributeValueComparison.FullMatch),
plain("valid", RequiredAttributeNameComparison.FullMatch),
css("other", "something", RequiredAttributeValueComparison.PrefixMatch),
plain("name-", RequiredAttributeNameComparison.PrefixMatch),
css("val", "cool", RequiredAttributeValueComparison.FullMatch),
plain("extra", RequiredAttributeNameComparison.FullMatch)]),
};
static Action<RequiredAttributeDescriptorBuilder> plain(string name, RequiredAttributeNameComparison nameComparison)
{
return builder => builder
.Name(name, nameComparison);
}
static Action<RequiredAttributeDescriptorBuilder> css(string name, string? value, RequiredAttributeValueComparison valueComparison)
{
return builder => builder
.Name(name)
.Value(value, valueComparison);
}
}
}
[Theory]
[MemberData(nameof(RequiredAttributeParserData))]
public void RequiredAttributeParser_ParsesRequiredAttributesCorrectly(
string requiredAttributes,
ImmutableArray<Action<RequiredAttributeDescriptorBuilder>> configures)
{
// Arrange
var expected = TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "Test")
.TagMatchingRuleDescriptor(rule =>
{
foreach (var configure in configures)
{
rule.Attribute(configure);
}
})
.Build();
// Act
var actual = TagHelperDescriptorBuilder.CreateTagHelper("TestTagHelper", "Test")
.TagMatchingRuleDescriptor(rule =>
{
RequiredAttributeParser.AddRequiredAttributes(requiredAttributes, rule);
})
.Build();
// Assert
Assert.Equal<RequiredAttributeDescriptor>(expected.TagMatchingRules[0].Attributes, actual.TagMatchingRules[0].Attributes);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> IsEnumData
=> new()
{
NameAndTagHelper("EnumTagHelper", static b => b
.TagMatchingRule(tagName: "enum")
.BoundAttribute<int>(name: "non-enum-property", propertyName: "NonEnumProperty")
.BoundAttribute(name: "enum-property", propertyName: "EnumProperty", typeName: "TestNamespace.CustomEnum", static b => b
.AsEnum())),
NameAndTagHelper("MultiEnumTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "input")
.BoundAttribute<int>(name: "non-enum-property", propertyName: "NonEnumProperty")
.BoundAttribute(name: "enum-property", propertyName: "EnumProperty", typeName: "TestNamespace.CustomEnum", static b => b
.AsEnum())),
NameAndTagHelper("NestedEnumTagHelper", static b => b
.TagMatchingRule(tagName: "nested-enum")
.BoundAttribute(name: "nested-enum-property", propertyName: "NestedEnumProperty", typeName: "TestNamespace.NestedEnumTagHelper.NestedEnum", static b => b
.AsEnum())
.BoundAttribute<int>(name: "non-enum-property", propertyName: "NonEnumProperty")
.BoundAttribute(name: "enum-property", propertyName: "EnumProperty", typeName: "TestNamespace.CustomEnum", static b => b
.AsEnum())),
};
[Theory]
[MemberData(nameof(IsEnumData))]
public void CreateDescriptor_IsEnumIsSetCorrectly(string tagHelperTypeName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> RequiredParentData
=> new()
{
NameAndTagHelper("RequiredParentTagHelper", static b => b
.TagMatchingRule(tagName: "input", parentTagName: "div")),
NameAndTagHelper("MultiSpecifiedRequiredParentTagHelper", static b => b
.TagMatchingRule(tagName: "p", parentTagName: "div")
.TagMatchingRule(tagName: "input", parentTagName: "section")),
NameAndTagHelper("MultiWithUnspecifiedRequiredParentTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "input", parentTagName: "div"))
};
[Theory]
[MemberData(nameof(RequiredParentData))]
public void CreateDescriptor_CreatesDesignTimeDescriptorsWithRequiredParent(string tagHelperTypeName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> RestrictChildrenData
=> new()
{
NameAndTagHelper("RestrictChildrenTagHelper", static b => b
.TagMatchingRule(tagName: "restrict-children")
.AllowedChildTag(tagName: "p")),
NameAndTagHelper("DoubleRestrictChildrenTagHelper", static b => b
.TagMatchingRule(tagName: "double-restrict-children")
.AllowedChildTag(tagName: "p")
.AllowedChildTag(tagName: "strong")),
NameAndTagHelper("MultiTargetRestrictChildrenTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "div")
.AllowedChildTag(tagName: "p")
.AllowedChildTag(tagName: "strong")),
};
[Theory]
[MemberData(nameof(RestrictChildrenData))]
public void CreateDescriptor_CreatesDescriptorsWithAllowedChildren(string tagHelperTypeName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> TagStructureData
=> new()
{
NameAndTagHelper("TagStructureTagHelper", static b => b
.TagMatchingRule(tagName: "input", tagStructure: TagStructure.WithoutEndTag)),
NameAndTagHelper("MultiSpecifiedTagStructureTagHelper", static b => b
.TagMatchingRule(tagName: "p", tagStructure: TagStructure.NormalOrSelfClosing)
.TagMatchingRule(tagName: "input", tagStructure: TagStructure.WithoutEndTag)),
NameAndTagHelper("MultiWithUnspecifiedTagStructureTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "input", tagStructure: TagStructure.WithoutEndTag))
};
[Theory]
[MemberData(nameof(TagStructureData))]
public void CreateDescriptor_CreatesDesignTimeDescriptorsWithTagStructure(string tagHelperTypeFullName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, designTime, expectedDescriptor
public static TheoryData<string, bool, TagHelperDescriptor?> EditorBrowsableData
{
get
{
return new()
{
NameAndTagHelper("InheritedEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "inherited-editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("EditorBrowsableTagHelper", designTime: true, configure: null),
NameAndTagHelper("EditorBrowsableTagHelper", designTime: false, static b => b
.TagMatchingRule(tagName: "editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("HiddenPropertyEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "hidden-property-editor-browsable")),
NameAndTagHelper("HiddenPropertyEditorBrowsableTagHelper", designTime: false, static b => b
.TagMatchingRule(tagName: "hidden-property-editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("OverriddenEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "overridden-editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("MultiPropertyEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "multi-property-editor-browsable")
.BoundAttribute<int>(name: "property2", propertyName: "Property2")),
NameAndTagHelper("MultiPropertyEditorBrowsableTagHelper", designTime: false, static b => b
.TagMatchingRule(tagName: "multi-property-editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")
.BoundAttribute<int>(name: "property2", propertyName: "Property2")),
NameAndTagHelper("OverriddenPropertyEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "overridden-property-editor-browsable")),
NameAndTagHelper("OverriddenPropertyEditorBrowsableTagHelper", designTime: false, static b => b
.TagMatchingRule(tagName: "overridden-property-editor-browsable")
.BoundAttribute<int>(name: "property2", propertyName: "Property2")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("DefaultEditorBrowsableTagHelper", designTime: true, static b => b
.TagMatchingRule(tagName: "default-editor-browsable")
.BoundAttribute<int>(name: "property", propertyName: "Property")),
NameAndTagHelper("MultiEditorBrowsableTagHelper", designTime: true, configure: null)
};
}
}
[Theory]
[MemberData(nameof(EditorBrowsableData))]
public void CreateDescriptor_UnderstandsEditorBrowsableAttribute(string tagHelperTypeName, bool designTime, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(designTime, designTime);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> AttributeTargetData
=> new()
{
NameAndTagHelper("AttributeTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "class"))),
NameAndTagHelper("MultiAttributeTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "class")
.RequiredAttribute(name: "style"))),
NameAndTagHelper("MultiAttributeAttributeTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "custom"))
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "class")
.RequiredAttribute(name: "style"))),
NameAndTagHelper("InheritedAttributeTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "style"))),
NameAndTagHelper("RequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "class"))),
NameAndTagHelper("InheritedRequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "div", static b => b
.RequiredAttribute(name: "class"))),
NameAndTagHelper("MultiAttributeRequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "div", static b => b
.RequiredAttribute(name: "class"))
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "class"))),
NameAndTagHelper("MultiAttributeSameTagRequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "style"))
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "class"))),
NameAndTagHelper("MultiRequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "class")
.RequiredAttribute(name: "style"))),
NameAndTagHelper("MultiTagMultiRequiredAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "div", static b => b
.RequiredAttribute(name: "class")
.RequiredAttribute(name: "style"))
.TagMatchingRule(tagName: "input", static b => b
.RequiredAttribute(name: "class")
.RequiredAttribute(name: "style"))),
NameAndTagHelper("AttributeWildcardTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "class", nameComparison: RequiredAttributeNameComparison.PrefixMatch))),
NameAndTagHelper("MultiAttributeWildcardTargetingTagHelper", static b => b
.TagMatchingRule(tagName: TagHelperMatchingConventions.ElementCatchAllName, static b => b
.RequiredAttribute(name: "class", nameComparison: RequiredAttributeNameComparison.PrefixMatch)
.RequiredAttribute(name: "style", nameComparison: RequiredAttributeNameComparison.PrefixMatch)))
};
[Theory]
[MemberData(nameof(AttributeTargetData))]
public void CreateDescriptor_ReturnsExpectedDescriptors(string tagHelperTypeName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedTagName, expectedAttributeName
public static TheoryData<string, string, string> HtmlCaseData
=> new()
{
("TestNamespace.SingleAttributeTagHelper", "single-attribute", "int-attribute"),
("TestNamespace.ALLCAPSTAGHELPER", "allcaps", "allcapsattribute"),
("TestNamespace.CAPSOnOUTSIDETagHelper", "caps-on-outside", "caps-on-outsideattribute"),
("TestNamespace.capsONInsideTagHelper", "caps-on-inside", "caps-on-insideattribute"),
("TestNamespace.One1Two2Three3TagHelper", "one1-two2-three3", "one1-two2-three3-attribute"),
("TestNamespace.ONE1TWO2THREE3TagHelper", "one1two2three3", "one1two2three3-attribute"),
("TestNamespace.First_Second_ThirdHiTagHelper", "first_second_third-hi", "first_second_third-attribute"),
("TestNamespace.UNSuffixedCLASS", "un-suffixed-class", "un-suffixed-attribute"),
};
[Theory]
[MemberData(nameof(HtmlCaseData))]
public void CreateDescriptor_HtmlCasesTagNameAndAttributeName(string tagHelperTypeName, string expectedTagName, string expectedAttributeName)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.NotNull(descriptor);
var rule = Assert.Single(descriptor.TagMatchingRules);
Assert.Equal(expectedTagName, rule.TagName, StringComparer.Ordinal);
var attributeDescriptor = Assert.Single(descriptor.BoundAttributes);
Assert.Equal(expectedAttributeName, attributeDescriptor.Name);
}
[Fact]
public void CreateDescriptor_OverridesAttributeNameFromAttribute()
{
// Arrange
var expectedDescriptor = CreateTagHelper("OverriddenAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "overridden-attribute")
.BoundAttribute<string>(name: "SomethingElse", propertyName: "ValidAttribute1")
.BoundAttribute<string>(name: "Something-Else", propertyName: "ValidAttribute2"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.OverriddenAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_DoesNotInheritOverridenAttributeName()
{
// Arrange
var expectedDescriptor = CreateTagHelper("InheritedOverriddenAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "inherited-overridden-attribute")
.BoundAttribute<string>(name: "valid-attribute1", propertyName: "ValidAttribute1")
.BoundAttribute<string>(name: "Something-Else", propertyName: "ValidAttribute2"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedOverriddenAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_AllowsOverriddenAttributeNameOnUnimplementedVirtual()
{
// Arrange
var expectedDescriptor = CreateTagHelper("InheritedNotOverriddenAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "inherited-not-overridden-attribute")
.BoundAttribute<string>(name: "SomethingElse", propertyName: "ValidAttribute1")
.BoundAttribute<string>(name: "Something-Else", propertyName: "ValidAttribute2"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedNotOverriddenAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_BuildsDescriptorsWithInheritedProperties()
{
// Arrange
var expectedDescriptor = CreateTagHelper("InheritedSingleAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "inherited-single-attribute")
.BoundAttribute<int>(name: "int-attribute", propertyName: "IntAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedSingleAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_BuildsDescriptorsWithConventionNames()
{
// Arrange
var expectedDescriptor = CreateTagHelper("SingleAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "single-attribute")
.BoundAttribute<int>(name: "int-attribute", propertyName: "IntAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.SingleAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_OnlyAcceptsPropertiesWithGetAndSet()
{
// Arrange
var expectedDescriptor = CreateTagHelper("MissingAccessorTagHelper", static b => b
.TagMatchingRule(tagName: "missing-accessor")
.BoundAttribute<string>(name: "valid-attribute", propertyName: "ValidAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.MissingAccessorTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_OnlyAcceptsPropertiesWithPublicGetAndSet()
{
// Arrange
var expectedDescriptor = CreateTagHelper("NonPublicAccessorTagHelper", static b => b
.TagMatchingRule(tagName: "non-public-accessor")
.BoundAttribute<string>(name: "valid-attribute", propertyName: "ValidAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.NonPublicAccessorTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_DoesNotIncludePropertiesWithNotBound()
{
// Arrange
var expectedDescriptor = CreateTagHelper("NotBoundAttributeTagHelper", static b => b
.TagMatchingRule(tagName: "not-bound-attribute")
.BoundAttribute<object>(name: "bound-property", propertyName: "BoundProperty"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.NotBoundAttributeTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType()
{
// Arrange
var expectedDescriptor = CreateTagHelper("MultiTagTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "div")
.BoundAttribute<string>(name: "valid-attribute", propertyName: "ValidAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.MultiTagTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_DoesNotResolveInheritedTagNames()
{
// Arrange
var expectedDescriptor = CreateTagHelper("InheritedMultiTagTagHelper", static b => b
.TagMatchingRule(tagName: "inherited-multi-tag")
.BoundAttribute<string>(name: "valid-attribute", propertyName: "ValidAttribute"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedMultiTagTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute()
{
// Arrange
var expectedDescriptor = CreateTagHelper("DuplicateTagNameTagHelper", static b => b
.TagMatchingRule(tagName: "p")
.TagMatchingRule(tagName: "div"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.DuplicateTagNameTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_OverridesTagNameFromAttribute()
{
// Arrange
var expectedDescriptor = CreateTagHelper("OverrideNameTagHelper", static b => b
.TagMatchingRule(tagName: "data-condition"));
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.OverrideNameTagHelper");
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// name, expectedErrorMessages
public static TheoryData<string, string[]> InvalidNameData
{
get
{
var whitespaceErrorString = "Targeted tag name cannot be null or whitespace.";
var data = GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError: null);
data.Add(string.Empty, [whitespaceErrorString]);
return data;
static string onNameError(string invalidText, string invalidCharacter)
{
return $"Tag helpers cannot target tag name '{invalidText}' because it contains a '{invalidCharacter}' character.";
}
}
}
[Theory]
[MemberData(nameof(InvalidNameData))]
public void CreateDescriptor_CreatesErrorOnInvalidNames(string name, string[] expectedErrorMessages)
{
// Arrange
name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\"");
var text = $$"""
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("{{name}}")]
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var attribute = tagHelperType.GetAttributes().Single();
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
var rule = Assert.Single(descriptor.TagMatchingRules);
var errorMessages = rule.GetAllDiagnostics().Select(diagnostic => diagnostic.GetMessage(CultureInfo.CurrentCulture)).ToArray();
Assert.Equal(expectedErrorMessages.Length, errorMessages.Length);
for (var i = 0; i < expectedErrorMessages.Length; i++)
{
Assert.Equal(expectedErrorMessages[i], errorMessages[i], StringComparer.Ordinal);
}
}
// name, expectedNames
public static TheoryData<string, ImmutableArray<string>> ValidNameData
=> new()
{
("p", ["p"]),
(" p", ["p"]),
("p ", ["p"]),
(" p ", ["p"]),
("p,div", ["p", "div"]),
(" p,div", ["p", "div"]),
("p ,div", ["p", "div"]),
(" p ,div", ["p", "div"]),
("p, div", ["p", "div"]),
("p,div ", ["p", "div"]),
("p, div ", ["p", "div"]),
(" p, div ", ["p", "div"]),
(" p , div ", ["p", "div"]),
};
// type, expectedAttributeDescriptors
public static TheoryData<string, ImmutableArray<BoundAttributeDescriptor>> InvalidTagHelperAttributeDescriptorData
=> new()
{
NameAndBoundAttributes("InvalidBoundAttribute", static b => b
.BoundAttribute<string>(name: "data-something", propertyName: "DataSomething")),
NameAndBoundAttributes("InvalidBoundAttributeWithValid", static b => b
.BoundAttribute<string>(name: "data-something", propertyName: "DataSomething")
.BoundAttribute<int>(name: "int-attribute", propertyName: "IntAttribute")),
NameAndBoundAttributes("OverriddenInvalidBoundAttributeWithValid", static b => b
.BoundAttribute<string>(name: "valid-something", propertyName: "DataSomething")),
NameAndBoundAttributes("OverriddenValidBoundAttributeWithInvalid", static b => b
.BoundAttribute<string>(name: "data-something", propertyName: "ValidSomething")),
NameAndBoundAttributes("OverriddenValidBoundAttributeWithInvalidUpperCase", static b => b
.BoundAttribute<string>(name: "DATA-SOMETHING", propertyName: "ValidSomething"))
};
[Theory]
[MemberData(nameof(InvalidTagHelperAttributeDescriptorData))]
public void CreateDescriptor_DoesNotAllowDataDashAttributes(
string tagHelperTypeName,
ImmutableArray<BoundAttributeDescriptor> expectedAttributes)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.NotNull(descriptor);
Assert.Equal<BoundAttributeDescriptor>(expectedAttributes, descriptor.BoundAttributes);
var id = AspNetCore.Razor.Language.RazorDiagnosticFactory.TagHelper_InvalidBoundAttributeNameStartsWith.Id;
foreach (var attribute in descriptor.BoundAttributes.Where(a => a.Name.StartsWith("data-", StringComparison.OrdinalIgnoreCase)))
{
var diagnostic = Assert.Single(attribute.Diagnostics);
Assert.Equal(id, diagnostic.Id);
}
}
public static TheoryData<string> ValidAttributeNameData
=>
[
"data",
"dataa-",
"ValidName",
"valid-name",
"--valid--name--",
",,--__..oddly.valid::;;",
];
[Theory]
[MemberData(nameof(ValidAttributeNameData))]
public void CreateDescriptor_WithValidAttributeName_HasNoErrors(string name)
{
// Arrange
var text = $$"""
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("{{name}}")]
public string SomeAttribute { get; set; }
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
Assert.False(descriptor.HasErrors);
}
public static TheoryData<string> ValidAttributePrefixData
=>
[
string.Empty,
"data",
"dataa-",
"ValidName",
"valid-name",
"--valid--name--",
",,--__..oddly.valid::;;",
];
[Theory]
[MemberData(nameof(ValidAttributePrefixData))]
public void CreateDescriptor_WithValidAttributePrefix_HasNoErrors(string prefix)
{
// Arrange
var text = $$"""
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute(DictionaryAttributePrefix = "{{prefix}}")]
public System.Collections.Generic.IDictionary<string, int> SomeAttribute { get; set; }
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
Assert.False(descriptor.HasErrors);
}
// name, expectedErrorMessages
public static TheoryData<string, string[]> InvalidAttributeNameData
{
get
{
var whitespaceErrorString =
"Invalid tag helper bound property 'string DynamicTestTagHelper.InvalidProperty' on tag helper 'DynamicTestTagHelper'. Tag helpers cannot " +
"bind to HTML attributes with a null or empty name.";
return GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError);
static string onNameError(string invalidText, string invalidCharacter)
{
return "Invalid tag helper bound property 'string DynamicTestTagHelper.InvalidProperty' on tag helper 'DynamicTestTagHelper'. Tag helpers " +
$"cannot bind to HTML attributes with name '{invalidText}' because the name contains a '{invalidCharacter}' character.";
}
static string onDataError(string invalidText)
{
return "Invalid tag helper bound property 'string DynamicTestTagHelper.InvalidProperty' on tag helper 'DynamicTestTagHelper'. Tag helpers cannot bind " +
$"to HTML attributes with name '{invalidText}' because the name starts with 'data-'.";
}
}
}
[Theory]
[MemberData(nameof(InvalidAttributeNameData))]
public void CreateDescriptor_WithInvalidAttributeName_HasErrors(string name, string[] expectedErrorMessages)
{
// Arrange
name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\"");
var text = $$"""
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("{{name}}")]
public string InvalidProperty { get; set; }
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
Assert.NotNull(descriptor);
// Assert
var errorMessages = descriptor.GetAllDiagnostics().Select(diagnostic => diagnostic.GetMessage(CultureInfo.CurrentCulture));
Assert.Equal(expectedErrorMessages, errorMessages);
}
// prefix, expectedErrorMessages
public static TheoryData<string, string[]> InvalidAttributePrefixData
{
get
{
var whitespaceErrorString =
"Invalid tag helper bound property 'System.Collections.Generic.IDictionary<System.String, System.Int32> DynamicTestTagHelper.InvalidProperty' " +
"on tag helper 'DynamicTestTagHelper'. Tag helpers cannot bind to HTML attributes with a null or empty name.";
return GetInvalidNameOrPrefixData(onPrefixError, whitespaceErrorString, onDataError);
static string onPrefixError(string invalidText, string invalidCharacter)
{
return "Invalid tag helper bound property 'System.Collections.Generic.IDictionary<System.String, System.Int32> DynamicTestTagHelper.InvalidProperty' " +
"on tag helper 'DynamicTestTagHelper'. Tag helpers " +
$"cannot bind to HTML attributes with prefix '{invalidText}' because the prefix contains a '{invalidCharacter}' character.";
}
static string onDataError(string invalidText)
{
return "Invalid tag helper bound property 'System.Collections.Generic.IDictionary<System.String, System.Int32> DynamicTestTagHelper.InvalidProperty' " +
"on tag helper 'DynamicTestTagHelper'. Tag helpers cannot bind to HTML attributes " +
$"with prefix '{invalidText}' because the prefix starts with 'data-'.";
}
}
}
[Theory]
[MemberData(nameof(InvalidAttributePrefixData))]
public void CreateDescriptor_WithInvalidAttributePrefix_HasErrors(string prefix, string[] expectedErrorMessages)
{
// Arrange
prefix = prefix.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\"");
var text = $$"""
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute(DictionaryAttributePrefix = "{{prefix}}")]
public System.Collections.Generic.IDictionary<System.String, System.Int32> InvalidProperty { get; set; }
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
var errorMessages = descriptor.GetAllDiagnostics().Select(diagnostic => diagnostic.GetMessage(CultureInfo.CurrentCulture));
Assert.Equal(expectedErrorMessages, errorMessages);
}
public static TheoryData<string, string[]> InvalidRestrictChildrenNameData
{
get
{
var nullOrWhiteSpaceError = Resources.FormatTagHelper_InvalidRestrictedChildNullOrWhitespace("DynamicTestTagHelper");
return GetInvalidNameOrPrefixData(
onNameError: static (invalidInput, invalidCharacter) =>
Resources.FormatTagHelper_InvalidRestrictedChild("DynamicTestTagHelper", invalidInput, invalidCharacter),
whitespaceErrorString: nullOrWhiteSpaceError,
onDataError: null);
}
}
[Theory]
[MemberData(nameof(InvalidRestrictChildrenNameData))]
public void CreateDescriptor_WithInvalidAllowedChildren_HasErrors(string name, string[] expectedErrorMessages)
{
// Arrange
name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\"");
var text = $$"""
[Microsoft.AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute("{{name}}")]
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
var errorMessages = descriptor.GetAllDiagnostics().Select(diagnostic => diagnostic.GetMessage(CultureInfo.CurrentCulture));
Assert.Equal(expectedErrorMessages, errorMessages);
}
public static TheoryData<string, string[]> InvalidParentTagData
{
get
{
var nullOrWhiteSpaceError = Resources.TagHelper_InvalidTargetedParentTagNameNullOrWhitespace;
return GetInvalidNameOrPrefixData(
onNameError: static (invalidInput, invalidCharacter) =>
Resources.FormatTagHelper_InvalidTargetedParentTagName(invalidInput, invalidCharacter),
whitespaceErrorString: nullOrWhiteSpaceError,
onDataError: null);
}
}
[Theory]
[MemberData(nameof(InvalidParentTagData))]
public void CreateDescriptor_WithInvalidParentTag_HasErrors(string name, string[] expectedErrorMessages)
{
// Arrange
name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\"");
var text = $$"""
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute(ParentTag = "{{name}}")]
public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
}
""";
var syntaxTree = Parse(text);
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper");
Assert.NotNull(tagHelperType);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
// Act
var descriptor = factory.CreateDescriptor(tagHelperType);
// Assert
Assert.NotNull(descriptor);
var errorMessages = descriptor.GetAllDiagnostics().Select(diagnostic => diagnostic.GetMessage(CultureInfo.CurrentCulture));
Assert.Equal(expectedErrorMessages, errorMessages);
}
[Fact]
public void CreateDescriptor_BuildsDescriptorsFromSimpleTypes()
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(typeof(Enumerable).FullName!);
Assert.NotNull(typeSymbol);
var expectedDescriptor = CreateTagHelper(
@namespace: typeSymbol.ContainingNamespace.GetDefaultDisplayString(),
typeName: typeSymbol.Name,
assemblyName: typeSymbol.ContainingAssembly.Identity.Name, static b => b
.TagMatchingRule("enumerable"));
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
// tagHelperType, expectedAttributeDescriptors, expectedDiagnostics
public static TheoryData<string, ImmutableArray<BoundAttributeDescriptor>, ImmutableArray<RazorDiagnostic>> TagHelperWithPrefixData
{
get
{
return new()
{
Combine(
NameAndBoundAttributes("DefaultValidHtmlAttributePrefix", static b => b
.BoundAttribute(name: "dictionary-property", propertyName: "DictionaryProperty", typeName: GetDictionaryTypeName<IDictionary<string, string>>(), static b => b
.AsDictionaryAttribute<string>("dictionary-property-"))),
diagnostics: []),
Combine(
NameAndBoundAttributes("SingleValidHtmlAttributePrefix", static b => b
.BoundAttribute(name: "valid-name", propertyName: "DictionaryProperty", typeName: GetDictionaryTypeName<IDictionary<string, string>>(), static b => b
.AsDictionaryAttribute<string>("valid-name-"))),
diagnostics: []),
Combine(
NameAndBoundAttributes("MultipleValidHtmlAttributePrefix", static b => b
.BoundAttribute(name: "valid-name1", propertyName: "DictionaryProperty", typeName:GetDictionaryTypeName<Dictionary<string, object>>(), static b => b
.AsDictionaryAttribute<object>("valid-prefix1-"))
.BoundAttribute(name: "valid-name2", propertyName: "DictionarySubclassProperty", typeName: "TestNamespace.DictionarySubclass", static b => b
.AsDictionaryAttribute<string>("valid-prefix2-"))
.BoundAttribute(name: "valid-name3", propertyName: "DictionaryWithoutParameterlessConstructorProperty", typeName: "TestNamespace.DictionaryWithoutParameterlessConstructor", static b => b
.AsDictionaryAttribute<string>("valid-prefix3-"))
.BoundAttribute(name: "valid-name4", propertyName: "GenericDictionarySubclassProperty", typeName: "TestNamespace.GenericDictionarySubclass<System.Object>", static b => b
.AsDictionaryAttribute<object>("valid-prefix4-"))
.BoundAttribute(name: "valid-name5", propertyName: "SortedDictionaryProperty", typeName: GetDictionaryTypeName<SortedDictionary<string, int>>(), static b => b
.AsDictionaryAttribute<int>("valid-prefix5-"))
.BoundAttribute(name: "valid-name6", propertyName: "StringProperty", typeName: typeof(string).FullName!)
.BoundAttribute(name: string.Empty, propertyName: "GetOnlyDictionaryProperty", typeName: GetDictionaryTypeName<IDictionary<string, int>>(), static b => b
.AsDictionaryAttribute<int>("get-only-dictionary-property-"))
.BoundAttribute(name: string.Empty, propertyName: "GetOnlyDictionaryPropertyWithAttributePrefix", typeName: GetDictionaryTypeName<IDictionary<string, string>>(), static b => b
.AsDictionaryAttribute<string>("valid-prefix6"))),
diagnostics: []),
Combine(
NameAndBoundAttributes("SingleInvalidHtmlAttributePrefix", static b => b
.BoundAttribute(name: "valid-name", propertyName: "StringProperty", typeName: typeof(string).FullName!, static b => b
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.SingleInvalidHtmlAttributePrefix",
"StringProperty")))),
diagnostics: RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.SingleInvalidHtmlAttributePrefix",
"StringProperty")),
Combine(
NameAndBoundAttributes("MultipleInvalidHtmlAttributePrefix", static b => b
.BoundAttribute<long>(name: "valid-name1", propertyName: "LongProperty")
.BoundAttribute(name: "valid-name2", propertyName: "DictionaryOfIntProperty", typeName: GetDictionaryTypeName<Dictionary<int, string>>(), static b => b
.AsDictionaryAttribute<string>("valid-prefix2-")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"DictionaryOfIntProperty")))
.BoundAttribute(name: "valid-name3", propertyName: "ReadOnlyDictionaryProperty", typeName: GetDictionaryTypeName<IReadOnlyDictionary<string, object>>(), static b => b
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"ReadOnlyDictionaryProperty")))
.BoundAttribute<int>(name: "valid-name4", propertyName: "IntProperty", static b => b
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"IntProperty")))
.BoundAttribute(name: "valid-name5", propertyName: "DictionaryOfIntSubclassProperty", typeName: "TestNamespace.DictionaryOfIntSubclass", static b => b
.AsDictionaryAttribute<string>("valid-prefix5-")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"DictionaryOfIntSubclassProperty")))
.BoundAttribute(name: string.Empty, propertyName: "GetOnlyDictionaryAttributePrefix", typeName: GetDictionaryTypeName<IDictionary<int, string>>(), static b => b
.AsDictionaryAttribute<string>("valid-prefix6")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"GetOnlyDictionaryAttributePrefix")))
.BoundAttribute(name: string.Empty, propertyName: "GetOnlyDictionaryPropertyWithAttributeName", typeName: GetDictionaryTypeName<IDictionary<string, object>>(), static b => b
.AsDictionaryAttribute<object>("invalid-name7-")
.AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"GetOnlyDictionaryPropertyWithAttributeName")))),
diagnostics: [
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"DictionaryOfIntProperty"),
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"ReadOnlyDictionaryProperty"),
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"IntProperty"),
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"DictionaryOfIntSubclassProperty"),
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"GetOnlyDictionaryAttributePrefix"),
RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNull(
"TestNamespace.MultipleInvalidHtmlAttributePrefix",
"GetOnlyDictionaryPropertyWithAttributeName")])
};
static (string, ImmutableArray<BoundAttributeDescriptor>, ImmutableArray<RazorDiagnostic>) Combine(
(string name, ImmutableArray<BoundAttributeDescriptor> boundAttributes) pair, params ImmutableArray<RazorDiagnostic> diagnostics)
{
return (pair.name, pair.boundAttributes, diagnostics);
}
static string GetDictionaryTypeName<T>()
{
var dictionaryType = typeof(T);
Assert.True(dictionaryType.IsConstructedGenericType);
Assert.Equal(2, dictionaryType.GenericTypeArguments.Length);
Assert.Equal("`2", dictionaryType.Name[^2..]);
var arg1Type = dictionaryType.GenericTypeArguments[0];
var arg2Type = dictionaryType.GenericTypeArguments[1];
return $"{dictionaryType.Namespace}.{dictionaryType.Name[..^2]}<{arg1Type.FullName}, {arg2Type.FullName}>";
}
}
}
[Theory]
[MemberData(nameof(TagHelperWithPrefixData))]
public void CreateDescriptor_WithPrefixes_ReturnsExpectedAttributeDescriptors(
string tagHelperTypeName,
ImmutableArray<BoundAttributeDescriptor> expectedAttributes,
ImmutableArray<RazorDiagnostic> expectedDiagnostics)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.NotNull(descriptor);
Assert.Equal<BoundAttributeDescriptor>(expectedAttributes, descriptor.BoundAttributes);
Assert.Equal<RazorDiagnostic>(expectedDiagnostics, [.. descriptor.GetAllDiagnostics()]);
}
// tagHelperType, expectedDescriptor
public static TheoryData<string, TagHelperDescriptor?> TagOutputHintData
=> new()
{
NameAndTagHelper("MultipleDescriptorTagHelperWithOutputElementHint", static b => b
.TagMatchingRule(tagName: "a")
.TagMatchingRule(tagName: "p")
.TagOutputHint("div")),
NameAndTagHelper("TestNamespace2", "InheritedOutputElementHintTagHelper", static b => b
.TagMatchingRule(tagName: "inherited-output-element-hint")),
NameAndTagHelper("TestNamespace2", "OutputElementHintTagHelper", static b => b
.TagMatchingRule(tagName: "output-element-hint")
.TagOutputHint("hinted-value")),
NameAndTagHelper("TestNamespace2", "OverriddenOutputElementHintTagHelper", static b => b
.TagMatchingRule(tagName: "overridden-output-element-hint")
.TagOutputHint("overridden"))
};
[Theory]
[MemberData(nameof(TagOutputHintData))]
public void CreateDescriptor_CreatesDescriptorsWithOutputElementHint(string tagHelperTypeName, TagHelperDescriptor? expectedDescriptor)
{
// Arrange
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: false, excludeHidden: false);
var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeName);
Assert.NotNull(typeSymbol);
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.Equal(expectedDescriptor, descriptor);
}
[Fact]
public void CreateDescriptor_CapturesDocumentationOnTagHelperClass()
{
// Arrange
var syntaxTree = Parse("""
using Microsoft.AspNetCore.Razor.TagHelpers;
/// <summary>
/// The summary for <see cref="DocumentedTagHelper"/>.
/// </summary>
/// <remarks>
/// Inherits from <see cref="TagHelper"/>.
/// </remarks>
public class DocumentedTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
}
""");
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: true, excludeHidden: false);
var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper");
Assert.NotNull(typeSymbol);
var expectedDocumentation = """
<member name="T:DocumentedTagHelper">
<summary>
The summary for <see cref="T:DocumentedTagHelper"/>.
</summary>
<remarks>
Inherits from <see cref="T:Microsoft.AspNetCore.Razor.TagHelpers.TagHelper"/>.
</remarks>
</member>
""";
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.NotNull(descriptor);
Assert.NotNull(descriptor.Documentation);
Assert.Equal(expectedDocumentation, descriptor.Documentation.Trim());
}
[Fact]
public void CreateDescriptor_CapturesDocumentationOnTagHelperProperties()
{
// Arrange
var syntaxTree = Parse("""
using System.Collections.Generic;
public class DocumentedTagHelper : Microsoft.spNetCore.Razor.TagHelpers.TagHelper
{
/// <summary>
/// This <see cref="SummaryProperty"/> is of type <see cref="string"/>.
/// </summary>
public string SummaryProperty { get; set; }
/// <remarks>
/// The <see cref="SummaryProperty"/> may be <c>null</c>.
/// </remarks>
public int RemarksProperty { get; set; }
/// <summary>
/// This is a complex <see cref="List{bool}"/>.
/// </summary>
/// <remarks>
/// <see cref="SummaryProperty"/><see cref="RemarksProperty"/>
/// </remarks>
public List<bool> RemarksAndSummaryProperty { get; set; }
}
""");
var compilation = Compilation.AddSyntaxTrees(syntaxTree);
var factory = new DefaultTagHelperDescriptorFactory(includeDocumentation: true, excludeHidden: false);
var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper");
Assert.NotNull(typeSymbol);
var expectedDocumentations = new[]
{
"""
<member name="P:DocumentedTagHelper.SummaryProperty">
<summary>
This <see cref="P:DocumentedTagHelper.SummaryProperty"/> is of type <see cref="T:System.String"/>.
</summary>
</member>
""",
"""
<member name="P:DocumentedTagHelper.RemarksProperty">
<remarks>
The <see cref="P:DocumentedTagHelper.SummaryProperty"/> may be <c>null</c>.
</remarks>
</member>
""",
"""
<member name="P:DocumentedTagHelper.RemarksAndSummaryProperty">
<summary>
This is a complex <see cref="T:System.Collections.Generic.List`1"/>.
</summary>
<remarks>
<see cref="P:DocumentedTagHelper.SummaryProperty"/><see cref="P:DocumentedTagHelper.RemarksProperty"/>
</remarks>
</member>
""",
};
// Act
var descriptor = factory.CreateDescriptor(typeSymbol);
// Assert
Assert.NotNull(descriptor);
var actuaDocumentations = descriptor.BoundAttributes
.SelectAsArray(boundAttribute =>
{
Assert.NotNull(boundAttribute.Documentation);
return boundAttribute.Documentation.Trim();
});
Assert.Equal(expectedDocumentations, actuaDocumentations);
}
private static TheoryData<string, string[]> GetInvalidNameOrPrefixData(
Func<string, string, string> onNameError,
string whitespaceErrorString,
Func<string, string>? onDataError)
{
// name, expectedErrorMessages
var data = new TheoryData<string, string[]>
{
("!", [onNameError("!", "!")]),
("hello!", [onNameError("hello!", "!")]),
("!hello", [onNameError("!hello", "!")]),
("he!lo", [onNameError("he!lo", "!")]),
("!he!lo!", [onNameError("!he!lo!", "!")]),
("@", [onNameError("@", "@")]),
("hello@", [onNameError("hello@", "@")]),
("@hello", [onNameError("@hello", "@")]),
("he@lo", [onNameError("he@lo", "@")]),
("@he@lo@", [onNameError("@he@lo@", "@")]),
("/", [onNameError("/", "/")]),
("hello/", [onNameError("hello/", "/")]),
("/hello", [onNameError("/hello", "/")]),
("he/lo", [onNameError("he/lo", "/")]),
("/he/lo/", [onNameError("/he/lo/", "/")]),
("<", [onNameError("<", "<")]),
("hello<", [onNameError("hello<", "<")]),
("<hello", [onNameError("<hello", "<")]),
("he<lo", [onNameError("he<lo", "<")]),
("<he<lo<", [onNameError("<he<lo<", "<")]),
("?", [onNameError("?", "?")]),
("hello?", [onNameError("hello?", "?")]),
("?hello", [onNameError("?hello", "?")]),
("he?lo", [onNameError("he?lo", "?")]),
("?he?lo?", [onNameError("?he?lo?", "?")]),
("[", [onNameError("[", "[")]),
("hello[", [onNameError("hello[", "[")]),
("[hello", [onNameError("[hello", "[")]),
("he[lo", [onNameError("he[lo", "[")]),
("[he[lo[", [onNameError("[he[lo[", "[")]),
(">", [onNameError(">", ">")]),
("hello>", [onNameError("hello>", ">")]),
(">hello", [onNameError(">hello", ">")]),
("he>lo", [onNameError("he>lo", ">")]),
(">he>lo>", [onNameError(">he>lo>", ">")]),
("]", [onNameError("]", "]")]),
("hello]", [onNameError("hello]", "]")]),
("]hello", [onNameError("]hello", "]")]),
("he]lo", [onNameError("he]lo", "]")]),
("]he]lo]", [onNameError("]he]lo]", "]")]),
("=", [onNameError("=", "=")]),
("hello=", [onNameError("hello=", "=")]),
("=hello", [onNameError("=hello", "=")]),
("he=lo", [onNameError("he=lo", "=")]),
("=he=lo=", [onNameError("=he=lo=", "=")]),
("\"", [onNameError("\"", "\"")] ),
("hello\"", [onNameError("hello\"", "\"")] ),
("\"hello", [onNameError("\"hello", "\"")] ),
("he\"lo", [onNameError("he\"lo", "\"")] ),
("\"he\"lo\"", [onNameError("\"he\"lo\"", "\"")] ),
("'", [onNameError("'", "'")] ),
("hello'", [onNameError("hello'", "'")] ),
("'hello", [onNameError("'hello", "'")] ),
("he'lo", [onNameError("he'lo", "'")] ),
("'he'lo'", [onNameError("'he'lo'", "'")] ),
("hello*", [onNameError("hello*", "*")] ),
("*hello", [onNameError("*hello", "*")] ),
("he*lo", [onNameError("he*lo", "*")] ),
("*he*lo*", [onNameError("*he*lo*", "*")] ),
(Environment.NewLine, [whitespaceErrorString]),
("\t", [whitespaceErrorString]),
(" \t ", [whitespaceErrorString]),
(" ", [whitespaceErrorString]),
(Environment.NewLine + " ", [whitespaceErrorString]),
("! \t\r\n@/<>?[]=\"'*", [
onNameError("! \t\r\n@/<>?[]=\"'*", "!"),
onNameError("! \t\r\n@/<>?[]=\"'*", " "),
onNameError("! \t\r\n@/<>?[]=\"'*", "\t"),
onNameError("! \t\r\n@/<>?[]=\"'*", "\r"),
onNameError("! \t\r\n@/<>?[]=\"'*", "\n"),
onNameError("! \t\r\n@/<>?[]=\"'*", "@"),
onNameError("! \t\r\n@/<>?[]=\"'*", "/"),
onNameError("! \t\r\n@/<>?[]=\"'*", "<"),
onNameError("! \t\r\n@/<>?[]=\"'*", ">"),
onNameError("! \t\r\n@/<>?[]=\"'*", "?"),
onNameError("! \t\r\n@/<>?[]=\"'*", "["),
onNameError("! \t\r\n@/<>?[]=\"'*", "]"),
onNameError("! \t\r\n@/<>?[]=\"'*", "="),
onNameError("! \t\r\n@/<>?[]=\"'*", "\""),
onNameError("! \t\r\n@/<>?[]=\"'*", "'"),
onNameError("! \t\r\n@/<>?[]=\"'*", "*")]),
("! \tv\ra\nl@i/d<>?[]=\"'*", [
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "!"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", " "),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\t"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\r"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\n"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "@"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "/"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "<"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", ">"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "?"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "["),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "]"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "="),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "\""),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "'"),
onNameError("! \tv\ra\nl@i/d<>?[]=\"'*", "*")])
};
if (onDataError is not null)
{
data.Add("data-", [onDataError("data-")]);
data.Add("data-something", [onDataError("data-something")]);
data.Add("Data-Something", [onDataError("Data-Something")]);
data.Add("DATA-SOMETHING", [onDataError("DATA-SOMETHING")]);
}
return data;
}
private static (string, bool, TagHelperDescriptor?) NameAndTagHelper(
string typeName, bool designTime, Action<TagHelperDescriptorBuilder>? configure)
=> NameAndTagHelper(@namespace: "TestNamespace", typeName, designTime, configure);
private static (string, bool, TagHelperDescriptor?) NameAndTagHelper(
string @namespace, string typeName, bool designTime, Action<TagHelperDescriptorBuilder>? configure)
{
var (fullName, tagHelper) = NameAndTagHelper(@namespace, typeName, configure);
return (fullName, designTime, tagHelper);
}
private static (string, ImmutableArray<BoundAttributeDescriptor>) NameAndBoundAttributes(
string typeName, Action<TagHelperDescriptorBuilder>? configure)
{
var (fullName, tagHelper) = NameAndTagHelper(@namespace: "TestNamespace", typeName, configure);
return (fullName, tagHelper?.BoundAttributes ?? []);
}
private static (string, TagHelperDescriptor?) NameAndTagHelper(
string typeName, Action<TagHelperDescriptorBuilder>? configure)
=> NameAndTagHelper(@namespace: "TestNamespace", typeName, configure);
private static (string, TagHelperDescriptor?) NameAndTagHelper(
string @namespace, string typeName, Action<TagHelperDescriptorBuilder>? configure)
{
var tagHelper = configure is not null
? CreateTagHelper(@namespace, typeName, configure)
: null;
return ($"{@namespace}.{typeName}", tagHelper);
}
private static TagHelperDescriptor CreateTagHelper(
string typeName,
Action<TagHelperDescriptorBuilder> configure)
=> CreateTagHelper(@namespace: "TestNamespace", typeName, configure);
private static TagHelperDescriptor CreateTagHelper(
string @namespace,
string typeName,
Action<TagHelperDescriptorBuilder> configure)
=> CreateTagHelper(@namespace, typeName, AssemblyName, configure);
private static TagHelperDescriptor CreateTagHelper(
string @namespace,
string typeName,
string assemblyName,
Action<TagHelperDescriptorBuilder>? configure = null)
{
var fullName = $"{@namespace}.{typeName}";
var builder = TagHelperDescriptorBuilder.CreateTagHelper(fullName, assemblyName);
builder.SetTypeName(fullName, @namespace, typeName);
configure?.Invoke(builder);
return builder.Build();
}
private const string AdditionalCode =
"""
namespace TestNamespace2
{
[Microsoft.AspNetCore.Razor.TagHelpers.OutputElementHint("hinted-value")]
public class OutputElementHintTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
}
public class InheritedOutputElementHintTagHelper : OutputElementHintTagHelper
{
}
[Microsoft.AspNetCore.Razor.TagHelpers.OutputElementHint("overridden")]
public class OverriddenOutputElementHintTagHelper : OutputElementHintTagHelper
{
}
}
""";
}
|