diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperDescriptorFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperDescriptorFactory.cs index 6c61d06b54..a3f5f2fcc0 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperDescriptorFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperDescriptorFactory.cs @@ -3,12 +3,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; -using Microsoft.AspNetCore.Razor.Evolution.Legacy; using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.CodeAnalysis; namespace Microsoft.CodeAnalysis.Razor { @@ -32,25 +33,32 @@ namespace Microsoft.CodeAnalysis.Razor private readonly INamedTypeSymbol _htmlAttributeNameAttributeSymbol; private readonly INamedTypeSymbol _htmlAttributeNotBoundAttributeSymbol; private readonly INamedTypeSymbol _htmlTargetElementAttributeSymbol; + private readonly INamedTypeSymbol _outputElementHintAttributeSymbol; private readonly INamedTypeSymbol _iDictionarySymbol; private readonly INamedTypeSymbol _restrictChildrenAttributeSymbol; + private readonly INamedTypeSymbol _editorBrowsableAttributeSymbol; public static ICollection InvalidNonWhitespaceNameCharacters { get; } = new HashSet( new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }); - public DefaultTagHelperDescriptorFactory(Compilation compilation) - { - Compilation = compilation; + private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes)); + public DefaultTagHelperDescriptorFactory(Compilation compilation, bool designTime) + { + DesignTime = designTime; _htmlAttributeNameAttributeSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.HtmlAttributeNameAttribute); _htmlAttributeNotBoundAttributeSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.HtmlAttributeNotBoundAttribute); _htmlTargetElementAttributeSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.HtmlTargetElementAttribute); + _outputElementHintAttributeSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.OutputElementHintAttribute); _restrictChildrenAttributeSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.RestrictChildrenAttribute); - - _iDictionarySymbol = Compilation.GetTypeByMetadataName(TagHelperTypes.IDictionary); + _editorBrowsableAttributeSymbol = compilation.GetTypeByMetadataName(typeof(EditorBrowsableAttribute).FullName); + _iDictionarySymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IDictionary); } - protected Compilation Compilation { get; } + protected bool DesignTime { get; } /// public virtual IEnumerable CreateDescriptors( @@ -68,6 +76,11 @@ namespace Microsoft.CodeAnalysis.Razor throw new ArgumentNullException(nameof(errorSink)); } + if (ShouldSkipDescriptorCreation(type)) + { + return Enumerable.Empty(); + } + var attributeDescriptors = GetAttributeDescriptors(type, errorSink); var targetElementAttributes = GetValidHtmlTargetElementAttributes(type, errorSink); var allowedChildren = GetAllowedChildren(type, errorSink); @@ -98,7 +111,36 @@ namespace Microsoft.CodeAnalysis.Razor IEnumerable targetElementAttributes, IEnumerable allowedChildren) { - TagHelperDesignTimeDescriptor typeDesignTimeDescriptor = null; + TagHelperDesignTimeDescriptor designTimeDescriptor = null; + if (DesignTime) + { + XmlMemberDocumentation documentation = null; + var xml = type.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + documentation = new XmlMemberDocumentation(xml); + } + + string outputElementHint = null; + var outputElementHintAttribute = type.GetAttributes().Where(a => a.AttributeClass == _outputElementHintAttributeSymbol).FirstOrDefault(); + if (outputElementHintAttribute != null) + { + outputElementHint = (string)(outputElementHintAttribute.ConstructorArguments[0]).Value; + } + + var remarks = documentation?.GetRemarks(); + var summary = documentation?.GetSummary(); + + if (outputElementHint != null || summary != null || remarks != null) + { + designTimeDescriptor = new TagHelperDesignTimeDescriptor() + { + OutputElementHint = outputElementHint, + Remarks = remarks, + Summary = summary, + }; + } + } var typeName = GetFullName(type); @@ -123,7 +165,7 @@ namespace Microsoft.CodeAnalysis.Razor allowedChildren: allowedChildren, tagStructure: default(TagStructure), parentTag: null, - designTimeDescriptor: typeDesignTimeDescriptor) + designTimeDescriptor: designTimeDescriptor) }; } @@ -135,7 +177,7 @@ namespace Microsoft.CodeAnalysis.Razor attributeDescriptors, attribute, allowedChildren, - typeDesignTimeDescriptor)); + designTimeDescriptor)); } private IEnumerable GetAllowedChildren(INamedTypeSymbol type, ErrorSink errorSink) @@ -156,7 +198,7 @@ namespace Microsoft.CodeAnalysis.Razor allowedChildren.Add((string)value.Value); } } - + var validAllowedChildren = GetValidAllowedChildren(allowedChildren, GetFullName(type), errorSink); if (validAllowedChildren.Any()) @@ -180,13 +222,21 @@ namespace Microsoft.CodeAnalysis.Razor foreach (var name in allowedChildren) { - var valid = TryValidateName( + if (string.IsNullOrWhiteSpace(name)) + { + var whitespaceError = Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( + TagHelperTypes.RestrictChildrenAttribute, + tagHelperName); + errorSink.OnError(SourceLocation.Zero, whitespaceError, length: 0); + } + else if (TryValidateName( name, - whitespaceError: "invalid", - characterErrorBuilder: (invalidCharacter) => "invalid", - errorSink: errorSink); - - if (valid) + invalidCharacter => Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( + TagHelperTypes.RestrictChildrenAttribute, + name, + tagHelperName, + invalidCharacter), + errorSink)) { validAllowedChildren.Add(name); } @@ -251,7 +301,7 @@ namespace Microsoft.CodeAnalysis.Razor { if (attibute.ConstructorArguments.Length == 0) { - return null; + return TagHelperDescriptorProvider.ElementCatchAllTarget; } else { @@ -317,12 +367,29 @@ namespace Microsoft.CodeAnalysis.Razor /// internal static bool ValidateParentTagName(string parentTag, ErrorSink errorSink) { - return parentTag == null || - TryValidateName( + if (parentTag == null) + { + return true; + } + else if (string.IsNullOrWhiteSpace(parentTag)) + { + var error = Workspaces.Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace( + Workspaces.Resources.TagHelperDescriptorFactory_ParentTag); + errorSink.OnError(SourceLocation.Zero, error, length: 0); + return false; + } + else if (!TryValidateName( + parentTag, + invalidCharacter => Workspaces.Resources.FormatHtmlTargetElementAttribute_InvalidName( + Workspaces.Resources.TagHelperDescriptorFactory_ParentTag.ToLower(), parentTag, - "invalid", - characterErrorBuilder: (invalidCharacter) => "invalid", - errorSink: errorSink); + invalidCharacter), + errorSink)) + { + return false; + } + + return true; } private static bool TryGetRequiredAttributeDescriptors( @@ -340,50 +407,53 @@ namespace Microsoft.CodeAnalysis.Razor if (!targetingAttributes && string.Equals( name, - "*", + TagHelperDescriptorProvider.ElementCatchAllTarget, StringComparison.OrdinalIgnoreCase)) { // '*' as the entire name is OK in the HtmlTargetElement catch-all case. return true; } - var targetName = targetingAttributes ? "invalid" : "invalid"; + var targetName = targetingAttributes ? + Workspaces.Resources.TagHelperDescriptorFactory_Attribute : + Workspaces.Resources.TagHelperDescriptorFactory_Tag; - var validName = TryValidateName( + if (string.IsNullOrWhiteSpace(name)) + { + var error = Workspaces.Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName); + errorSink.OnError(SourceLocation.Zero, error, length: 0); + return false; + } + else if (!TryValidateName( name, - whitespaceError: "invalid", - characterErrorBuilder: (invalidCharacter) => "invalid", - errorSink: errorSink); + invalidCharacter => Workspaces.Resources.FormatHtmlTargetElementAttribute_InvalidName( + targetName.ToLower(), + name, + invalidCharacter), + errorSink)) + { + return false; + } - return validName; + return true; } private static bool TryValidateName( string name, - string whitespaceError, Func characterErrorBuilder, ErrorSink errorSink) { var validName = true; - if (string.IsNullOrWhiteSpace(name)) + foreach (var character in name) { - errorSink.OnError(SourceLocation.Zero, whitespaceError, length: 0); - - validName = false; - } - else - { - foreach (var character in name) + if (char.IsWhiteSpace(character) || + InvalidNonWhitespaceNameCharacters.Contains(character)) { - if (char.IsWhiteSpace(character) || - InvalidNonWhitespaceNameCharacters.Contains(character)) - { - var error = characterErrorBuilder(character); - errorSink.OnError(SourceLocation.Zero, error, length: 0); + var error = characterErrorBuilder(character); + errorSink.OnError(SourceLocation.Zero, error, length: 0); - validName = false; - } + validName = false; } } @@ -392,15 +462,19 @@ namespace Microsoft.CodeAnalysis.Razor private IEnumerable GetAttributeDescriptors(INamedTypeSymbol type, ErrorSink errorSink) { - var attributeDescriptors = new List(); // Keep indexer descriptors separate to avoid sorting the combined list later. var indexerDescriptors = new List(); - var accessibleProperties = type.GetMembers().OfType().Where(IsAccessibleProperty); + var accessibleProperties = GetAccessibleProperties(type); foreach (var property in accessibleProperties) { + if (ShouldSkipDescriptorCreation(property)) + { + continue; + } + var attributeNameAttribute = property .GetAttributes() .Where(a => a.AttributeClass == _htmlAttributeNameAttributeSymbol) @@ -408,8 +482,8 @@ namespace Microsoft.CodeAnalysis.Razor bool hasExplicitName; string attributeName; - if (attributeNameAttribute == null || - attributeNameAttribute.ConstructorArguments.Length == 0 || + if (attributeNameAttribute == null || + attributeNameAttribute.ConstructorArguments.Length == 0 || string.IsNullOrEmpty((string)attributeNameAttribute.ConstructorArguments[0].Value)) { hasExplicitName = false; @@ -436,7 +510,11 @@ namespace Microsoft.CodeAnalysis.Razor // Specified HtmlAttributeNameAttribute.Name though property has no public setter. errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty( + GetFullName(type), + property.Name, + TagHelperTypes.HtmlAttributeNameAttribute, + TagHelperTypes.HtmlAttributeName.Name), length: 0); continue; } @@ -478,6 +556,33 @@ namespace Microsoft.CodeAnalysis.Razor return attributeDescriptors; } + private IEnumerable GetAccessibleProperties(INamedTypeSymbol typeSymbol) + { + var accessibleProperties = new Dictionary(StringComparer.Ordinal); + do + { + var members = typeSymbol.GetMembers(); + for (var i = 0; i < members.Length; i++) + { + var property = members[i] as IPropertySymbol; + if (property != null && + property.Parameters.Length == 0 && + property.GetMethod != null && + property.GetMethod.DeclaredAccessibility == Accessibility.Public && + property.GetAttributes().Where(a => a.AttributeClass == _htmlAttributeNotBoundAttributeSymbol).FirstOrDefault() == null && + !accessibleProperties.ContainsKey(property.Name)) + { + accessibleProperties.Add(property.Name, property); + } + } + + typeSymbol = typeSymbol.BaseType; + } + while (typeSymbol != null); + + return accessibleProperties.Values; + } + // Internal for testing. internal static bool ValidateTagHelperAttributeDescriptor( TagHelperAttributeDescriptor attributeDescriptor, @@ -487,20 +592,22 @@ namespace Microsoft.CodeAnalysis.Razor string nameOrPrefix; if (attributeDescriptor.IsIndexer) { - nameOrPrefix = "invalid"; + nameOrPrefix = Workspaces.Resources.TagHelperDescriptorFactory_Prefix; } else if (string.IsNullOrEmpty(attributeDescriptor.Name)) { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty( + GetFullName(parentType), + attributeDescriptor.PropertyName), length: 0); return false; } else { - nameOrPrefix = "invalid"; + nameOrPrefix = Workspaces.Resources.TagHelperDescriptorFactory_Name; } return ValidateTagHelperAttributeNameOrPrefix( @@ -533,7 +640,10 @@ namespace Microsoft.CodeAnalysis.Razor // Provide a single error if the entire name is whitespace, not an error per character. errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace( + GetFullName(parentType), + propertyName, + nameOrPrefix), length: 0); return false; @@ -545,7 +655,12 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart( + GetFullName(parentType), + propertyName, + nameOrPrefix, + attributeNameOrPrefix, + DataDashPrefix), length: 0); return false; @@ -558,7 +673,12 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter( + GetFullName(parentType), + propertyName, + nameOrPrefix, + attributeNameOrPrefix, + character), length: 0); isValid = false; @@ -619,7 +739,7 @@ namespace Microsoft.CodeAnalysis.Razor { dictionaryType = null; } - + if (dictionaryType == null || dictionaryType.TypeArguments[0].SpecialType != SpecialType.System_String) { @@ -630,7 +750,12 @@ namespace Microsoft.CodeAnalysis.Razor isInvalid = true; errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( + GetFullName(parentType), + property.Name, + TagHelperTypes.HtmlAttributeNameAttribute, + TagHelperTypes.HtmlAttributeName.DictionaryAttributePrefix, + "IDictionary"), length: 0); } else if (attributeNameAttribute != null && !hasPublicSetter) @@ -640,7 +765,11 @@ namespace Microsoft.CodeAnalysis.Razor isInvalid = true; errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( + GetFullName(parentType), + property.Name, + TagHelperTypes.HtmlAttributeNameAttribute, + "IDictionary"), length: 0); } @@ -656,7 +785,12 @@ namespace Microsoft.CodeAnalysis.Razor isInvalid = true; errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( + GetFullName(parentType), + property.Name, + TagHelperTypes.HtmlAttributeNameAttribute, + TagHelperTypes.HtmlAttributeName.DictionaryAttributePrefix, + "IDictionary"), length: 0); return null; @@ -687,6 +821,28 @@ namespace Microsoft.CodeAnalysis.Razor bool isIndexer, bool isStringProperty) { + TagHelperAttributeDesignTimeDescriptor designTimeDescriptor = null; + if (DesignTime) + { + XmlMemberDocumentation documentation = null; + var xml = property.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + documentation = new XmlMemberDocumentation(xml); + } + + var remarks = documentation?.GetRemarks(); + var summary = documentation?.GetSummary(); + if (summary != null || remarks != null) + { + designTimeDescriptor = new TagHelperAttributeDesignTimeDescriptor() + { + Remarks = remarks, + Summary = summary, + }; + } + } + return new TagHelperAttributeDescriptor { Name = attributeName, @@ -695,16 +851,28 @@ namespace Microsoft.CodeAnalysis.Razor TypeName = typeName, IsStringProperty = isStringProperty, IsIndexer = isIndexer, + DesignTimeDescriptor = designTimeDescriptor, }; } - private bool IsAccessibleProperty(IPropertySymbol property) + private bool ShouldSkipDescriptorCreation(ISymbol symbol) { - // Accessible properties are those with public getters and without [HtmlAttributeNotBound]. - return property.Parameters.Length == 0 && - property.GetMethod != null && - property.GetMethod.DeclaredAccessibility == Accessibility.Public && - property.GetAttributes().Where(a => a.AttributeClass == _htmlAttributeNotBoundAttributeSymbol).FirstOrDefault() == null; + if (DesignTime) + { + var editorBrowsableAttribute = symbol.GetAttributes().Where(a => a.AttributeClass == _editorBrowsableAttributeSymbol).FirstOrDefault(); + + if (editorBrowsableAttribute == null) + { + return false; + } + + if (editorBrowsableAttribute.ConstructorArguments.Length > 0) + { + return (EditorBrowsableState)editorBrowsableAttribute.ConstructorArguments[0].Value == EditorBrowsableState.Never; + } + } + + return false; } /// @@ -719,15 +887,12 @@ namespace Microsoft.CodeAnalysis.Razor /// ONE1TWO2THREE3 => one1two2three3 /// First_Second_ThirdHi => first_second_third-hi /// - private static string ToHtmlCase(string name) + internal static string ToHtmlCase(string name) { return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); } - private static string GetFullName(ITypeSymbol type) - { - return type.ContainingNamespace.ToDisplayString() + "." + type.Name; - } + private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(FullNameTypeDisplayFormat); // Internal for testing internal class RequiredAttributeParser @@ -809,7 +974,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes), length: 0); return false; } @@ -891,7 +1056,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(_requiredAttributes, op), length: 0); return null; } @@ -900,7 +1065,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(Current, _requiredAttributes), length: 0); return null; } @@ -925,7 +1090,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(_requiredAttributes, quote), length: 0); return null; } @@ -1000,7 +1165,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes), length: 0); return null; } @@ -1008,7 +1173,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(Current, _requiredAttributes), length: 0); return null; } @@ -1028,7 +1193,7 @@ namespace Microsoft.CodeAnalysis.Razor { errorSink.OnError( SourceLocation.Zero, - "invalid", + Workspaces.Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(_requiredAttributes), length: 0); return false; diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs index 770ef2b6a3..f68e1e4fa1 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolver.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.AspNetCore.Razor.Evolution.Legacy; @@ -11,10 +9,15 @@ namespace Microsoft.CodeAnalysis.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { - public override async Task> GetTagHelpersAsync(Project project, CancellationToken cancellationToken = default(CancellationToken)) + public DefaultTagHelperResolver(bool designTime) { - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + DesignTime = designTime; + } + public bool DesignTime { get; } + + public override IReadOnlyList GetTagHelpers(Compilation compilation) + { var results = new List(); // If ITagHelper isn't defined, then we couldn't possibly find anything. @@ -38,7 +41,7 @@ namespace Microsoft.CodeAnalysis.Razor } var errors = new ErrorSink(); - var factory = new DefaultTagHelperDescriptorFactory(compilation); + var factory = new DefaultTagHelperDescriptorFactory(compilation, DesignTime); foreach (var type in types) { @@ -49,7 +52,7 @@ namespace Microsoft.CodeAnalysis.Razor } // Visits top-level types and finds interface implementations. - private class Visitor : SymbolVisitor + internal class Visitor : SymbolVisitor { private INamedTypeSymbol _interface; private List _results; @@ -62,7 +65,7 @@ namespace Microsoft.CodeAnalysis.Razor public override void VisitNamedType(INamedTypeSymbol symbol) { - if (symbol.AllInterfaces.Contains(_interface)) + if (IsTagHelper(symbol)) { _results.Add(symbol); } @@ -75,6 +78,14 @@ namespace Microsoft.CodeAnalysis.Razor Visit(member); } } + + internal bool IsTagHelper(INamedTypeSymbol symbol) + { + return symbol.DeclaredAccessibility == Accessibility.Public && + !symbol.IsAbstract && + !symbol.IsGenericType && + symbol.AllInterfaces.Contains(_interface); + } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolverFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolverFactory.cs index 38e091cd0f..76560efa7e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperResolverFactory.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTagHelperResolver(); + return new DefaultTagHelperResolver(designTime: true); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj index 2a4e7b3651..92b13d0e05 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj @@ -7,9 +7,6 @@ true aspnetcore;cshtml;razor - - Microsoft.CodeAnalysis.Razor - diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/Resources.Designer.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..12ea76a9a2 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/Resources.Designer.cs @@ -0,0 +1,398 @@ +// +namespace Microsoft.CodeAnalysis.Razor.Workspaces +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.CodeAnalysis.Razor.Workspaces.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// {0} cannot be null or an empty string. + /// + internal static string Argument_Cannot_Be_Null_Or_Empty + { + get { return GetString("Argument_Cannot_Be_Null_Or_Empty"); } + } + + /// + /// {0} cannot be null or an empty string. + /// + internal static string FormatArgument_Cannot_Be_Null_Or_Empty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Cannot_Be_Null_Or_Empty"), p0); + } + + /// + /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + /// + internal static string HtmlTargetElementAttribute_InvalidName + { + get { return GetString("HtmlTargetElementAttribute_InvalidName"); } + } + + /// + /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + /// + internal static string FormatHtmlTargetElementAttribute_InvalidName(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlTargetElementAttribute_InvalidName"), p0, p1, p2); + } + + /// + /// {0} name cannot be null or whitespace. + /// + internal static string HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace + { + get { return GetString("HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace"); } + } + + /// + /// {0} name cannot be null or whitespace. + /// + internal static string FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace"), p0); + } + + /// + /// Attribute + /// + internal static string TagHelperDescriptorFactory_Attribute + { + get { return GetString("TagHelperDescriptorFactory_Attribute"); } + } + + /// + /// Attribute + /// + internal static string FormatTagHelperDescriptorFactory_Attribute() + { + return GetString("TagHelperDescriptorFactory_Attribute"); + } + + /// + /// Could not find matching ']' for required attribute '{0}'. + /// + internal static string TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace + { + get { return GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"); } + } + + /// + /// Could not find matching ']' for required attribute '{0}'. + /// + internal static string FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_CouldNotFindMatchingEndBrace"), p0); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameAttribute + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"), p0, p1, p2, p3); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"), p0, p1, p2, p3); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"), p0, p1); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"), p0, p1, p2); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNotNull + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNull + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas. + /// + internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter + { + get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"); } + } + + /// + /// Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeCharacter"), p0, p1); + } + + /// + /// Required attribute '{0}' has mismatched quotes '{1}' around value. + /// + internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes + { + get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"); } + } + + /// + /// Required attribute '{0}' has mismatched quotes '{1}' around value. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes"), p0, p1); + } + + /// + /// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'. + /// + internal static string TagHelperDescriptorFactory_InvalidRequiredAttributeOperator + { + get { return GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"); } + } + + /// + /// Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRequiredAttributeOperator"), p0, p1); + } + + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"); } + } + + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"), p0, p1, p2, p3); + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"); } + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1); + } + + /// + /// name + /// + internal static string TagHelperDescriptorFactory_Name + { + get { return GetString("TagHelperDescriptorFactory_Name"); } + } + + /// + /// name + /// + internal static string FormatTagHelperDescriptorFactory_Name() + { + return GetString("TagHelperDescriptorFactory_Name"); + } + + /// + /// Parent Tag + /// + internal static string TagHelperDescriptorFactory_ParentTag + { + get { return GetString("TagHelperDescriptorFactory_ParentTag"); } + } + + /// + /// Parent Tag + /// + internal static string FormatTagHelperDescriptorFactory_ParentTag() + { + return GetString("TagHelperDescriptorFactory_ParentTag"); + } + + /// + /// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals. + /// + internal static string TagHelperDescriptorFactory_PartialRequiredAttributeOperator + { + get { return GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"); } + } + + /// + /// Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals. + /// + internal static string FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_PartialRequiredAttributeOperator"), p0, p1); + } + + /// + /// prefix + /// + internal static string TagHelperDescriptorFactory_Prefix + { + get { return GetString("TagHelperDescriptorFactory_Prefix"); } + } + + /// + /// prefix + /// + internal static string FormatTagHelperDescriptorFactory_Prefix() + { + return GetString("TagHelperDescriptorFactory_Prefix"); + } + + /// + /// Tag + /// + internal static string TagHelperDescriptorFactory_Tag + { + get { return GetString("TagHelperDescriptorFactory_Tag"); } + } + + /// + /// Tag + /// + internal static string FormatTagHelperDescriptorFactory_Tag() + { + return GetString("TagHelperDescriptorFactory_Tag"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources.resx b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources.resx new file mode 100644 index 0000000000..ee2c26d650 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources.resx @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0} cannot be null or an empty string. + + + Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + + + {0} name cannot be null or whitespace. + + + Attribute + + + Could not find matching ']' for required attribute '{0}'. + + + Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + + + Invalid required attribute character '{0}' in required attribute '{1}'. Separate required attributes with commas. + + + Required attribute '{0}' has mismatched quotes '{1}' around value. + + + Invalid character '{0}' in required attribute '{1}'. Expected supported CSS operator or ']'. + + + Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + + + Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + + + name + + + Parent Tag + + + Required attribute '{0}' has a partial CSS operator. '{1}' must be followed by an equals. + + + prefix + + + Tag + + \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index 2819c263ed..29945f8d86 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -4,15 +4,21 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.Razor { internal abstract class TagHelperResolver : ILanguageService { - public abstract Task> GetTagHelpersAsync( + public abstract IReadOnlyList GetTagHelpers(Compilation compilation); + + public virtual async Task> GetTagHelpersAsync( Project project, - CancellationToken cancellationToken = default(CancellationToken)); + CancellationToken cancellationToken = default(CancellationToken)) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + return GetTagHelpers(compilation); + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperTypes.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperTypes.cs index 4a65817e7e..d2ea2e3507 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperTypes.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperTypes.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.CodeAnalysis.Razor -{ +{ internal static class TagHelperTypes { public const string ITagHelper = "Microsoft.AspNetCore.Razor.TagHelpers.ITagHelper"; @@ -15,10 +15,13 @@ namespace Microsoft.CodeAnalysis.Razor public const string HtmlTargetElementAttribute = "Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute"; + public const string OutputElementHintAttribute = "Microsoft.AspNetCore.Razor.TagHelpers.OutputElementHintAttribute"; + public const string RestrictChildrenAttribute = "Microsoft.AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute"; public static class HtmlAttributeName { + public const string Name = "Name"; public const string DictionaryAttributePrefix = "DictionaryAttributePrefix"; } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/XmlMemberDocumentation.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/XmlMemberDocumentation.cs new file mode 100644 index 0000000000..3d17a50176 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/XmlMemberDocumentation.cs @@ -0,0 +1,82 @@ +// 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.Diagnostics; +using System.Text; +using System.Xml.Linq; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Razor +{ + /// + /// Extracts summary and remarks XML documentation from XML member documentation. + /// + internal class XmlMemberDocumentation + { + private readonly XElement _element; + + public XmlMemberDocumentation(string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.FormatArgument_Cannot_Be_Null_Or_Empty(nameof(content))); + } + + // the structure of the XML is defined by: https://msdn.microsoft.com/en-us/library/fsbx0t7x.aspx + // we expect the root node of the content we are passed to always be 'member'. + _element = XElement.Parse(content); + Debug.Assert(_element.Name == "member"); + } + + /// + /// Retrieves the <summary> documentation. + /// + /// <summary> documentation. + public string GetSummary() + { + var summaryElement = _element.Element("summary"); + if (summaryElement != null) + { + var summaryValue = GetElementValue(summaryElement); + + return summaryValue; + } + + return null; + } + + /// + /// Retrieves the <remarks> documentation. + /// + /// <remarks> documentation. + public string GetRemarks() + { + var remarksElement = _element.Element("remarks"); + + if (remarksElement != null) + { + var remarksValue = GetElementValue(remarksElement); + + return remarksValue; + } + + return null; + } + + private static string GetElementValue(XElement element) + { + var stringBuilder = new StringBuilder(); + var node = element.FirstNode; + + while (node != null) + { + stringBuilder.Append(node.ToString(SaveOptions.DisableFormatting)); + + node = node.NextNode; + } + + return stringBuilder.ToString().Trim(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs index 3f6fa2438b..69a9fad93e 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs @@ -9,14 +9,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Evolution; -using Microsoft.AspNetCore.Razor.Evolution.Legacy; using Microsoft.CodeAnalysis.Razor; namespace Microsoft.CodeAnalysis.Remote.Razor { internal class RazorLanguageService : ServiceHubServiceBase { - public RazorLanguageService(Stream stream, IServiceProvider serviceProvider) + public RazorLanguageService(Stream stream, IServiceProvider serviceProvider) : base(stream, serviceProvider) { } @@ -28,7 +27,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor var solution = await GetSolutionAsync().ConfigureAwait(false); var project = solution.GetProject(projectId); - var resolver = new DefaultTagHelperResolver(); + var resolver = new DefaultTagHelperResolver(designTime: true); var results = await resolver.GetTagHelpersAsync(project, cancellationToken).ConfigureAwait(false); return results; diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperDescriptorComparer.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperDescriptorComparer.cs new file mode 100644 index 0000000000..c63d50c266 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperDescriptorComparer.cs @@ -0,0 +1,97 @@ +// 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.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers +{ + internal class CaseSensitiveTagHelperDescriptorComparer : TagHelperDescriptorComparer + { + public new static readonly CaseSensitiveTagHelperDescriptorComparer Default = + new CaseSensitiveTagHelperDescriptorComparer(); + + private CaseSensitiveTagHelperDescriptorComparer() + : base() + { + } + + public override bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.True(base.Equals(descriptorX, descriptorY)); + + // Normal comparer doesn't care about the case, required attribute order, allowed children order, + // attributes or prefixes. In tests we do. + Assert.Equal(descriptorX.TagName, descriptorY.TagName, StringComparer.Ordinal); + Assert.Equal(descriptorX.Prefix, descriptorY.Prefix, StringComparer.Ordinal); + Assert.Equal( + descriptorX.RequiredAttributes, + descriptorY.RequiredAttributes, + CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default); + Assert.Equal(descriptorX.RequiredParent, descriptorY.RequiredParent, StringComparer.Ordinal); + + if (descriptorX.AllowedChildren != descriptorY.AllowedChildren) + { + Assert.Equal(descriptorX.AllowedChildren, descriptorY.AllowedChildren, StringComparer.Ordinal); + } + + Assert.Equal( + descriptorX.Attributes, + descriptorY.Attributes, + TagHelperAttributeDescriptorComparer.Default); + Assert.Equal( + descriptorX.DesignTimeDescriptor, + descriptorY.DesignTimeDescriptor, + TagHelperDesignTimeDescriptorComparer.Default); + + return true; + } + + public override int GetHashCode(TagHelperDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(base.GetHashCode(descriptor)); + hashCodeCombiner.Add(descriptor.TagName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.Prefix, StringComparer.Ordinal); + + if (descriptor.DesignTimeDescriptor != null) + { + hashCodeCombiner.Add( + TagHelperDesignTimeDescriptorComparer.Default.GetHashCode(descriptor.DesignTimeDescriptor)); + } + + foreach (var requiredAttribute in descriptor.RequiredAttributes.OrderBy(attribute => attribute.Name)) + { + hashCodeCombiner.Add( + CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default.GetHashCode(requiredAttribute)); + } + + if (descriptor.AllowedChildren != null) + { + foreach (var child in descriptor.AllowedChildren.OrderBy(child => child)) + { + hashCodeCombiner.Add(child, StringComparer.Ordinal); + } + } + + var orderedAttributeHashCodes = descriptor.Attributes + .Select(attribute => TagHelperAttributeDescriptorComparer.Default.GetHashCode(attribute)) + .OrderBy(hashcode => hashcode); + foreach (var attributeHashCode in orderedAttributeHashCodes) + { + hashCodeCombiner.Add(attributeHashCode); + } + + return hashCodeCombiner.CombinedHash; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs new file mode 100644 index 0000000000..f5c521deed --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.cs @@ -0,0 +1,44 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers +{ + internal class CaseSensitiveTagHelperRequiredAttributeDescriptorComparer : TagHelperRequiredAttributeDescriptorComparer + { + public new static readonly CaseSensitiveTagHelperRequiredAttributeDescriptorComparer Default = + new CaseSensitiveTagHelperRequiredAttributeDescriptorComparer(); + + private CaseSensitiveTagHelperRequiredAttributeDescriptorComparer() + : base() + { + } + + public override bool Equals(TagHelperRequiredAttributeDescriptor descriptorX, TagHelperRequiredAttributeDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.True(base.Equals(descriptorX, descriptorY)); + Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal); + + return true; + } + + public override int GetHashCode(TagHelperRequiredAttributeDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(base.GetHashCode(descriptor)); + hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal); + + return hashCodeCombiner.CombinedHash; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDescriptorComparer.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDescriptorComparer.cs new file mode 100644 index 0000000000..d913abba3e --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDescriptorComparer.cs @@ -0,0 +1,57 @@ +// 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.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers +{ + internal class TagHelperAttributeDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperAttributeDescriptorComparer Default = + new TagHelperAttributeDescriptorComparer(); + + private TagHelperAttributeDescriptorComparer() + { + } + + public bool Equals(TagHelperAttributeDescriptor descriptorX, TagHelperAttributeDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.NotNull(descriptorX); + Assert.NotNull(descriptorY); + Assert.Equal(descriptorX.IsIndexer, descriptorY.IsIndexer); + Assert.Equal(descriptorX.Name, descriptorY.Name, StringComparer.Ordinal); + Assert.Equal(descriptorX.PropertyName, descriptorY.PropertyName, StringComparer.Ordinal); + Assert.Equal(descriptorX.TypeName, descriptorY.TypeName, StringComparer.Ordinal); + Assert.Equal(descriptorX.IsEnum, descriptorY.IsEnum); + Assert.Equal(descriptorX.IsStringProperty, descriptorY.IsStringProperty); + + return TagHelperAttributeDesignTimeDescriptorComparer.Default.Equals( + descriptorX.DesignTimeDescriptor, + descriptorY.DesignTimeDescriptor); + } + + public int GetHashCode(TagHelperAttributeDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(descriptor.IsIndexer); + hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.PropertyName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.IsEnum); + hashCodeCombiner.Add(descriptor.IsStringProperty); + hashCodeCombiner.Add(TagHelperAttributeDesignTimeDescriptorComparer.Default.GetHashCode( + descriptor.DesignTimeDescriptor)); + + return hashCodeCombiner; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDesignTimeDescriptorComparer.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDesignTimeDescriptorComparer.cs new file mode 100644 index 0000000000..a463531891 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperAttributeDesignTimeDescriptorComparer.cs @@ -0,0 +1,48 @@ +// 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.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers +{ + internal class TagHelperAttributeDesignTimeDescriptorComparer : + IEqualityComparer + { + public static readonly TagHelperAttributeDesignTimeDescriptorComparer Default = + new TagHelperAttributeDesignTimeDescriptorComparer(); + + private TagHelperAttributeDesignTimeDescriptorComparer() + { + } + + public bool Equals( + TagHelperAttributeDesignTimeDescriptor descriptorX, + TagHelperAttributeDesignTimeDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.NotNull(descriptorX); + Assert.NotNull(descriptorY); + Assert.Equal(descriptorX.Summary, descriptorY.Summary, StringComparer.Ordinal); + Assert.Equal(descriptorX.Remarks, descriptorY.Remarks, StringComparer.Ordinal); + + return true; + } + + public int GetHashCode(TagHelperAttributeDesignTimeDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(descriptor.Summary, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.Remarks, StringComparer.Ordinal); + + return hashCodeCombiner; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperDesignTimeDescriptorComparer.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperDesignTimeDescriptorComparer.cs new file mode 100644 index 0000000000..52a7ef7805 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Comparers/TagHelperDesignTimeDescriptorComparer.cs @@ -0,0 +1,48 @@ +// 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.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers +{ + internal class TagHelperDesignTimeDescriptorComparer : IEqualityComparer + { + public static readonly TagHelperDesignTimeDescriptorComparer Default = + new TagHelperDesignTimeDescriptorComparer(); + + private TagHelperDesignTimeDescriptorComparer() + { + } + + public bool Equals(TagHelperDesignTimeDescriptor descriptorX, TagHelperDesignTimeDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + Assert.NotNull(descriptorX); + Assert.NotNull(descriptorY); + Assert.Equal(descriptorX.Summary, descriptorY.Summary, StringComparer.Ordinal); + Assert.Equal(descriptorX.Remarks, descriptorY.Remarks, StringComparer.Ordinal); + Assert.Equal(descriptorX.OutputElementHint, descriptorY.OutputElementHint, StringComparer.Ordinal); + + return true; + } + + public int GetHashCode(TagHelperDesignTimeDescriptor descriptor) + { + var hashCodeCombiner = HashCodeCombiner.Start(); + + hashCodeCombiner.Add(descriptor.Summary, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.Remarks, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.OutputElementHint, StringComparer.Ordinal); + + return hashCodeCombiner; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperDescriptorFactoryTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperDescriptorFactoryTest.cs new file mode 100644 index 0000000000..1af795fd37 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperDescriptorFactoryTest.cs @@ -0,0 +1,2726 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test.Comparers; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces +{ + public class DefaultTagHelperDescriptorFactoryTest + { + protected static readonly AssemblyName TagHelperDescriptorFactoryTestAssembly = + typeof(DefaultTagHelperDescriptorFactoryTest).GetTypeInfo().Assembly.GetName(); + + protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name; + + private static Compilation Compilation { get; } = TestCompilation.Create(); + + public static TheoryData RequiredAttributeParserErrorData + { + get + { + Func error = (message) => new RazorError(message, SourceLocation.Zero, 0); + + return new TheoryData + { + { "name,", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("name,")) }, + { " ", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) }, + { "n@me", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) }, + { "name extra", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeCharacter('e', "name extra")) }, + { "[[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[[ ")) }, + { "[ ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[ ")) }, + { + "[name='unended]", + error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended]", '\'')) + }, + { + "[name='unended", + error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeMismatchedQuotes("[name='unended", '\'')) + }, + { "[name", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name")) }, + { "[ ]", error(Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace("Attribute")) }, + { "[n@me]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "n@me", '@')) }, + { "[name@]", error(Resources.FormatHtmlTargetElementAttribute_InvalidName("attribute", "name@", '@')) }, + { "[name^]", error(Resources.FormatTagHelperDescriptorFactory_PartialRequiredAttributeOperator("[name^]", '^')) }, + { "[name='value'", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name='value'")) }, + { "[name ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name ")) }, + { "[name extra]", error(Resources.FormatTagHelperDescriptorFactory_InvalidRequiredAttributeOperator('e', "[name extra]")) }, + { "[name=value ", error(Resources.FormatTagHelperDescriptorFactory_CouldNotFindMatchingEndBrace("[name=value ")) }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeParserErrorData))] + public void RequiredAttributeParser_ParsesRequiredAttributesAndLogsErrorCorrectly( + string requiredAttributes, + object expectedError) + { + // Arrange + var parser = new DefaultTagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes); + var errorSink = new ErrorSink(); + IEnumerable descriptors; + + // Act + var parsedCorrectly = parser.TryParse(errorSink, out descriptors); + + // Assert + Assert.False(parsedCorrectly); + Assert.Null(descriptors); + var error = Assert.Single(errorSink.Errors); + Assert.Equal((RazorError)expectedError, error); + } + + public static TheoryData RequiredAttributeParserData + { + get + { + Func plain = + (name, nameComparison) => new TagHelperRequiredAttributeDescriptor + { + Name = name, + NameComparison = nameComparison + }; + Func css = + (name, value, valueComparison) => new TagHelperRequiredAttributeDescriptor + { + Name = name, + NameComparison = TagHelperRequiredAttributeNameComparison.FullMatch, + Value = value, + ValueComparison = valueComparison, + }; + + return new TheoryData> + { + { null, Enumerable.Empty() }, + { string.Empty, Enumerable.Empty() }, + { "name", new[] { plain("name", TagHelperRequiredAttributeNameComparison.FullMatch) } }, + { "name-*", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } }, + { " name-* ", new[] { plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch) } }, + { + "asp-route-*,valid , name-* ,extra", + new[] + { + plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch), + plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch), + plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch), + plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch), + } + }, + { "[name]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } }, + { "[ name ]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } }, + { " [ name ] ", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.None) } }, + { "[name=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { "[name='']", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { "[name ^=]", new[] { css("name", "", TagHelperRequiredAttributeValueComparison.PrefixMatch) } }, + { "[name=hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { "[name= hello]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { "[name='hello']", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { "[name=\"hello\"]", new[] { css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch) } }, + { " [ name $= \" hello\" ] ", new[] { css("name", " hello", TagHelperRequiredAttributeValueComparison.SuffixMatch) } }, + { + "[name=\"hello\"],[other^=something ], [val = 'cool']", + new[] + { + css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch), + css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch), + css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch) } + }, + { + "asp-route-*,[name=\"hello\"],valid ,[other^=something ], name-* ,[val = 'cool'],extra", + new[] + { + plain("asp-route-", TagHelperRequiredAttributeNameComparison.PrefixMatch), + css("name", "hello", TagHelperRequiredAttributeValueComparison.FullMatch), + plain("valid", TagHelperRequiredAttributeNameComparison.FullMatch), + css("other", "something", TagHelperRequiredAttributeValueComparison.PrefixMatch), + plain("name-", TagHelperRequiredAttributeNameComparison.PrefixMatch), + css("val", "cool", TagHelperRequiredAttributeValueComparison.FullMatch), + plain("extra", TagHelperRequiredAttributeNameComparison.FullMatch), + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredAttributeParserData))] + public void RequiredAttributeParser_ParsesRequiredAttributesCorrectly( + string requiredAttributes, + IEnumerable expectedDescriptors) + { + // Arrange + var parser = new DefaultTagHelperDescriptorFactory.RequiredAttributeParser(requiredAttributes); + var errorSink = new ErrorSink(); + IEnumerable descriptors; + + // Act + var parsedCorrectly = parser.TryParse(errorSink, out descriptors); + + // Assert + Assert.True(parsedCorrectly); + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperRequiredAttributeDescriptorComparer.Default); + } + + public static TheoryData IsEnumData + { + get + { + var attributeDescriptors = new[] + { + new TagHelperAttributeDescriptor + { + Name = "non-enum-property", + PropertyName = nameof(EnumTagHelper.NonEnumProperty), + TypeName = typeof(int).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "enum-property", + PropertyName = nameof(EnumTagHelper.EnumProperty), + TypeName = typeof(CustomEnum).FullName, + IsEnum = true + }, + }; + + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(EnumTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "enum", + TypeName = typeof(EnumTagHelper).FullName, + AssemblyName = AssemblyName, + Attributes = attributeDescriptors + } + } + }, + { + typeof(MultiEnumTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(MultiEnumTagHelper).FullName, + AssemblyName = AssemblyName, + Attributes = attributeDescriptors + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiEnumTagHelper).FullName, + AssemblyName = AssemblyName, + Attributes = attributeDescriptors + } + } + } + }; + } + } + + [Theory] + [MemberData(nameof(IsEnumData))] + public void CreateDescriptors_IsEnumIsSetCorrectly( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData RequiredParentData + { + get + { + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(RequiredParentTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(RequiredParentTagHelper).FullName, + AssemblyName = AssemblyName, + RequiredParent = "div" + } + } + }, + { + typeof(MultiSpecifiedRequiredParentTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(MultiSpecifiedRequiredParentTagHelper).FullName, + AssemblyName = AssemblyName, + RequiredParent = "section" + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiSpecifiedRequiredParentTagHelper).FullName, + AssemblyName = AssemblyName, + RequiredParent = "div" + } + } + }, + { + typeof(MultiWithUnspecifiedRequiredParentTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(MultiWithUnspecifiedRequiredParentTagHelper).FullName, + AssemblyName = AssemblyName, + RequiredParent = "div" + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiWithUnspecifiedRequiredParentTagHelper).FullName, + AssemblyName = AssemblyName + } + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(RequiredParentData))] + public void CreateDescriptors_CreatesDesignTimeDescriptorsWithRequiredParent( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData RestrictChildrenData + { + get + { + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(RestrictChildrenTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "restrict-children", + TypeName = typeof(RestrictChildrenTagHelper).FullName, + AssemblyName = AssemblyName, + AllowedChildren = new[] { "p" }, + } + } + }, + { + typeof(DoubleRestrictChildrenTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "double-restrict-children", + TypeName = typeof(DoubleRestrictChildrenTagHelper).FullName, + AssemblyName = AssemblyName, + AllowedChildren = new[] { "p", "strong" }, + } + } + }, + { + typeof(MultiTargetRestrictChildrenTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "div", + TypeName = typeof(MultiTargetRestrictChildrenTagHelper).FullName, + AssemblyName = AssemblyName, + AllowedChildren = new[] { "p", "strong" }, + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiTargetRestrictChildrenTagHelper).FullName, + AssemblyName = AssemblyName, + AllowedChildren = new[] { "p", "strong" }, + } + } + }, + }; + } + } + + + [Theory] + [MemberData(nameof(RestrictChildrenData))] + public void CreateDescriptors_CreatesDescriptorsWithAllowedChildren( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData TagStructureData + { + get + { + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(TagStructureTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(TagStructureTagHelper).FullName, + AssemblyName = AssemblyName, + TagStructure = TagStructure.WithoutEndTag + } + } + }, + { + typeof(MultiSpecifiedTagStructureTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(MultiSpecifiedTagStructureTagHelper).FullName, + AssemblyName = AssemblyName, + TagStructure = TagStructure.WithoutEndTag + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiSpecifiedTagStructureTagHelper).FullName, + AssemblyName = AssemblyName, + TagStructure = TagStructure.NormalOrSelfClosing + } + } + }, + { + typeof(MultiWithUnspecifiedTagStructureTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "input", + TypeName = typeof(MultiWithUnspecifiedTagStructureTagHelper).FullName, + AssemblyName = AssemblyName, + TagStructure = TagStructure.WithoutEndTag + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MultiWithUnspecifiedTagStructureTagHelper).FullName, + AssemblyName = AssemblyName + } + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagStructureData))] + public void CreateDescriptors_CreatesDesignTimeDescriptorsWithTagStructure( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData EditorBrowsableData + { + get + { + // tagHelperType, designTime, expectedDescriptors + return new TheoryData + { + { + typeof(InheritedEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "inherited-editor-browsable", + typeName: typeof(InheritedEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(InheritedEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { typeof(EditorBrowsableTagHelper), true, new TagHelperDescriptor[0] }, + { + typeof(EditorBrowsableTagHelper), + false, + new[] + { + CreateTagHelperDescriptor( + tagName: "editor-browsable", + typeName: typeof(EditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(EditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(HiddenPropertyEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "hidden-property-editor-browsable", + typeName: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new TagHelperAttributeDescriptor[0]) + } + }, + { + typeof(HiddenPropertyEditorBrowsableTagHelper), + false, + new[] + { + CreateTagHelperDescriptor( + tagName: "hidden-property-editor-browsable", + typeName: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(HiddenPropertyEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(OverriddenEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "overridden-editor-browsable", + typeName: typeof(OverriddenEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(OverriddenEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(MultiPropertyEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "multi-property-editor-browsable", + typeName: typeof(MultiPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property2", + PropertyName = nameof(MultiPropertyEditorBrowsableTagHelper.Property2), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(MultiPropertyEditorBrowsableTagHelper), + false, + new[] + { + CreateTagHelperDescriptor( + tagName: "multi-property-editor-browsable", + typeName: typeof(MultiPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(MultiPropertyEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "property2", + PropertyName = nameof(MultiPropertyEditorBrowsableTagHelper.Property2), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(OverriddenPropertyEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "overridden-property-editor-browsable", + typeName: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new TagHelperAttributeDescriptor[0]) + } + }, + { + typeof(OverriddenPropertyEditorBrowsableTagHelper), + false, + new[] + { + CreateTagHelperDescriptor( + tagName: "overridden-property-editor-browsable", + typeName: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property2", + PropertyName = nameof(OverriddenPropertyEditorBrowsableTagHelper.Property2), + TypeName = typeof(int).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(OverriddenPropertyEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { + typeof(DefaultEditorBrowsableTagHelper), + true, + new[] + { + CreateTagHelperDescriptor( + tagName: "default-editor-browsable", + typeName: typeof(DefaultEditorBrowsableTagHelper).FullName, + assemblyName: AssemblyName, + attributes: new[] + { + new TagHelperAttributeDescriptor + { + Name = "property", + PropertyName = nameof(DefaultEditorBrowsableTagHelper.Property), + TypeName = typeof(int).FullName + } + }) + } + }, + { typeof(MultiEditorBrowsableTagHelper), true, new TagHelperDescriptor[0] } + }; + } + } + + [Theory] + [MemberData(nameof(EditorBrowsableData))] + public void CreateDescriptors_UnderstandsEditorBrowsableAttribute( + Type tagHelperType, + bool designTime, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData AttributeTargetData + { + get + { + var attributes = Enumerable.Empty(); + + // tagHelperType, expectedDescriptors + return new TheoryData> + { + { + typeof(AttributeTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(AttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) + } + }, + { + typeof(MultiAttributeTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(MultiAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) + } + }, + { + typeof(MultiAttributeAttributeTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(MultiAttributeAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "custom" } + }), + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(MultiAttributeAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) + } + }, + { + typeof(InheritedAttributeTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(InheritedAttributeTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) + } + }, + { + typeof(RequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "input", + typeof(RequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) + } + }, + { + typeof(InheritedRequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "div", + typeof(InheritedRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) + } + }, + { + typeof(MultiAttributeRequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "div", + typeof(MultiAttributeRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }), + CreateTagHelperDescriptor( + "input", + typeof(MultiAttributeRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) + } + }, + { + typeof(MultiAttributeSameTagRequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "input", + typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }), + CreateTagHelperDescriptor( + "input", + typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" } + }) + } + }, + { + typeof(MultiRequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "input", + typeof(MultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }) + } + }, + { + typeof(MultiTagMultiRequiredAttributeTagHelper), + new[] + { + CreateTagHelperDescriptor( + "div", + typeof(MultiTagMultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }), + CreateTagHelperDescriptor( + "input", + typeof(MultiTagMultiRequiredAttributeTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] { + new TagHelperRequiredAttributeDescriptor { Name = "class" }, + new TagHelperRequiredAttributeDescriptor { Name = "style" } + }), + } + }, + { + typeof(AttributeWildcardTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(AttributeWildcardTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + } + }) + } + }, + { + typeof(MultiAttributeWildcardTargetingTagHelper), + new[] + { + CreateTagHelperDescriptor( + TagHelperDescriptorProvider.ElementCatchAllTarget, + typeof(MultiAttributeWildcardTargetingTagHelper).FullName, + AssemblyName, + attributes, + requiredAttributes: new[] + { + new TagHelperRequiredAttributeDescriptor + { + Name = "class", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + }, + new TagHelperRequiredAttributeDescriptor + { + Name = "style", + NameComparison = TagHelperRequiredAttributeNameComparison.PrefixMatch, + } + }) + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(AttributeTargetData))] + public void CreateDescriptors_ReturnsExpectedDescriptors( + Type tagHelperType, + IEnumerable expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy( + descriptor => CaseSensitiveTagHelperDescriptorComparer.Default.GetHashCode(descriptor)).ToArray(); + expectedDescriptors = expectedDescriptors.OrderBy( + descriptor => CaseSensitiveTagHelperDescriptorComparer.Default.GetHashCode(descriptor)).ToArray(); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + public static TheoryData HtmlCaseData + { + get + { + // tagHelperType, expectedTagName, expectedAttributeName + return new TheoryData + { + { typeof(SingleAttributeTagHelper), "single-attribute", "int-attribute" }, + { typeof(ALLCAPSTAGHELPER), "allcaps", "allcapsattribute" }, + { typeof(CAPSOnOUTSIDETagHelper), "caps-on-outside", "caps-on-outsideattribute" }, + { typeof(capsONInsideTagHelper), "caps-on-inside", "caps-on-insideattribute" }, + { typeof(One1Two2Three3TagHelper), "one1-two2-three3", "one1-two2-three3-attribute" }, + { typeof(ONE1TWO2THREE3TagHelper), "one1two2three3", "one1two2three3-attribute" }, + { typeof(First_Second_ThirdHiTagHelper), "first_second_third-hi", "first_second_third-attribute" }, + { typeof(UNSuffixedCLASS), "un-suffixed-class", "un-suffixed-attribute" }, + }; + } + } + + [Theory] + [MemberData(nameof(HtmlCaseData))] + public void CreateDescriptors_HtmlCasesTagNameAndAttributeName( + Type tagHelperType, + string expectedTagName, + string expectedAttributeName) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedTagName, descriptor.TagName, StringComparer.Ordinal); + var attributeDescriptor = Assert.Single(descriptor.Attributes); + Assert.Equal(expectedAttributeName, attributeDescriptor.Name); + } + + [Fact] + public void CreateDescriptors_OverridesAttributeNameFromAttribute() + { + // Arrange + var errorSink = new ErrorSink(); + var validProperty1 = typeof(OverriddenAttributeTagHelper).GetProperty( + nameof(OverriddenAttributeTagHelper.ValidAttribute1)); + var validProperty2 = typeof(OverriddenAttributeTagHelper).GetProperty( + nameof(OverriddenAttributeTagHelper.ValidAttribute2)); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "overridden-attribute", + typeof(OverriddenAttributeTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("SomethingElse", validProperty1), + CreateTagHelperAttributeDescriptor("Something-Else", validProperty2) + }) + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(OverriddenAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_DoesNotInheritOverridenAttributeName() + { + // Arrange + var errorSink = new ErrorSink(); + var validProperty1 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( + nameof(InheritedOverriddenAttributeTagHelper.ValidAttribute1)); + var validProperty2 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( + nameof(InheritedOverriddenAttributeTagHelper.ValidAttribute2)); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "inherited-overridden-attribute", + typeof(InheritedOverriddenAttributeTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("valid-attribute1", validProperty1), + CreateTagHelperAttributeDescriptor("Something-Else", validProperty2) + }) + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedOverriddenAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_AllowsOverridenAttributeNameOnUnimplementedVirtual() + { + // Arrange + var errorSink = new ErrorSink(); + var validProperty1 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( + nameof(InheritedNotOverriddenAttributeTagHelper.ValidAttribute1)); + var validProperty2 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( + nameof(InheritedNotOverriddenAttributeTagHelper.ValidAttribute2)); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "inherited-not-overridden-attribute", + typeof(InheritedNotOverriddenAttributeTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("SomethingElse", validProperty1), + CreateTagHelperAttributeDescriptor("Something-Else", validProperty2) + }) + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedNotOverriddenAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_BuildsDescriptorsWithInheritedProperties() + { + // Arrange + var errorSink = new ErrorSink(); + var expectedDescriptor = CreateTagHelperDescriptor( + "inherited-single-attribute", + typeof(InheritedSingleAttributeTagHelper).FullName, + AssemblyName, + new[] + { + new TagHelperAttributeDescriptor + { + Name = "int-attribute", + PropertyName = nameof(InheritedSingleAttributeTagHelper.IntAttribute), + TypeName = typeof(int).FullName + } + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedSingleAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_BuildsDescriptorsWithConventionNames() + { + // Arrange + var errorSink = new ErrorSink(); + var intProperty = typeof(SingleAttributeTagHelper).GetProperty(nameof(SingleAttributeTagHelper.IntAttribute)); + var expectedDescriptor = CreateTagHelperDescriptor( + "single-attribute", + typeof(SingleAttributeTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("int-attribute", intProperty) + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(SingleAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: new ErrorSink()); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_OnlyAcceptsPropertiesWithGetAndSet() + { + // Arrange + var errorSink = new ErrorSink(); + var validProperty = typeof(MissingAccessorTagHelper).GetProperty( + nameof(MissingAccessorTagHelper.ValidAttribute)); + var expectedDescriptor = CreateTagHelperDescriptor( + "missing-accessor", + typeof(MissingAccessorTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("valid-attribute", validProperty) + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MissingAccessorTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_OnlyAcceptsPropertiesWithPublicGetAndSet() + { + // Arrange + var errorSink = new ErrorSink(); + var validProperty = typeof(NonPublicAccessorTagHelper).GetProperty( + nameof(NonPublicAccessorTagHelper.ValidAttribute)); + var expectedDescriptor = CreateTagHelperDescriptor( + "non-public-accessor", + typeof(NonPublicAccessorTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("valid-attribute", validProperty) + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(NonPublicAccessorTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_DoesNotIncludePropertiesWithNotBound() + { + // Arrange + var errorSink = new ErrorSink(); + var expectedDescriptor = CreateTagHelperDescriptor( + "not-bound-attribute", + typeof(NotBoundAttributeTagHelper).FullName, + AssemblyName, + new[] + { + new TagHelperAttributeDescriptor + { + Name = "bound-property", + PropertyName = nameof(NotBoundAttributeTagHelper.BoundProperty), + TypeName = typeof(object).FullName + } + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(NotBoundAttributeTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact(Skip = "#364")] + public void CreateDescriptors_AddsErrorForTagHelperWithDuplicateAttributeNames() + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(DuplicateAttributeNameTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(descriptors); + var error = Assert.Single(errorSink.Errors); + } + + [Fact] + public void CreateDescriptors_ResolvesMultipleTagHelperDescriptorsFromSingleType() + { + // Arrange + var errorSink = new ErrorSink(); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "div", + typeof(MultiTagTagHelper).FullName, + AssemblyName, + new[] + { + new TagHelperAttributeDescriptor + { + Name = "valid-attribute", + PropertyName = nameof(MultiTagTagHelper.ValidAttribute), + TypeName = typeof(string).FullName, + IsStringProperty = true + } + }), + CreateTagHelperDescriptor( + "p", + typeof(MultiTagTagHelper).FullName, + AssemblyName, + new[] + { + new TagHelperAttributeDescriptor + { + Name = "valid-attribute", + PropertyName = nameof(MultiTagTagHelper.ValidAttribute), + TypeName = typeof(string).FullName, + IsStringProperty = true + } + }) + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName).ToArray(); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_DoesNotResolveInheritedTagNames() + { + // Arrange + var errorSink = new ErrorSink(); + var validProp = typeof(InheritedMultiTagTagHelper).GetProperty(nameof(InheritedMultiTagTagHelper.ValidAttribute)); + var expectedDescriptor = CreateTagHelperDescriptor( + "inherited-multi-tag", + typeof(InheritedMultiTagTagHelper).FullName, + AssemblyName, + new[] + { + CreateTagHelperAttributeDescriptor("valid-attribute", validProp) + }); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedMultiTagTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_IgnoresDuplicateTagNamesFromAttribute() + { + // Arrange + var errorSink = new ErrorSink(); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "div", + typeof(DuplicateTagNameTagHelper).FullName, + AssemblyName), + CreateTagHelperDescriptor( + "p", + typeof(DuplicateTagNameTagHelper).FullName, + AssemblyName) + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(DuplicateTagNameTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName).ToArray(); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_OverridesTagNameFromAttribute() + { + // Arrange + var errorSink = new ErrorSink(); + var expectedDescriptors = new[] + { + CreateTagHelperDescriptor( + "data-condition", + typeof(OverrideNameTagHelper).FullName, + AssemblyName), + }; + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(OverrideNameTagHelper).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + // name, expectedErrorMessages + public static TheoryData InvalidNameData + { + get + { + Func onNameError = + (invalidText, invalidCharacter) => $"Tag helpers cannot target tag name '{ invalidText }' " + + $"because it contains a '{ invalidCharacter }' character."; + var whitespaceErrorString = "Tag name cannot be null or whitespace."; + + var data = GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError: null); + data.Add(string.Empty, new[] { whitespaceErrorString }); + + return data; + } + } + + [Theory] + [MemberData(nameof(InvalidNameData))] + public void ValidHtmlTargetElementAttributeNames_CreatesErrorOnInvalidNames( + string name, string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); + var text = $@" +[{typeof(AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute).FullName}(""{name}"")] +public class DynamicTestTagHelper : {typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName} +{{ +}}"; + var syntaxTree = CSharpSyntaxTree.ParseText(text); + var compilation = TestCompilation.Create(syntaxTree); + var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); + var attribute = tagHelperType.GetAttributes().Single(); + + // Act + DefaultTagHelperDescriptorFactory.ValidHtmlTargetElementAttributeNames(attribute, errorSink); + + // Assert + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + Assert.Equal(0, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + } + + public static TheoryData ValidNameData + { + get + { + // name, expectedNames + return new TheoryData> + { + { "p", new[] { "p" } }, + { " p", new[] { "p" } }, + { "p ", new[] { "p" } }, + { " p ", new[] { "p" } }, + { "p,div", new[] { "p", "div" } }, + { " p,div", new[] { "p", "div" } }, + { "p ,div", new[] { "p", "div" } }, + { " p ,div", new[] { "p", "div" } }, + { "p, div", new[] { "p", "div" } }, + { "p,div ", new[] { "p", "div" } }, + { "p, div ", new[] { "p", "div" } }, + { " p, div ", new[] { "p", "div" } }, + { " p , div ", new[] { "p", "div" } }, + }; + } + } + + public static TheoryData InvalidTagHelperAttributeDescriptorData + { + get + { + var errorFormat = "Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML " + + "attributes with name '{2}' because name starts with 'data-'."; + + // type, expectedAttributeDescriptors, expectedErrors + return new TheoryData, string[]> + { + { + typeof(InvalidBoundAttribute), + Enumerable.Empty(), + new[] + { + string.Format( + errorFormat, + typeof(InvalidBoundAttribute).FullName, + nameof(InvalidBoundAttribute.DataSomething), + "data-something") + } + }, + { + typeof(InvalidBoundAttributeWithValid), + new[] + { + CreateTagHelperAttributeDescriptor( + "int-attribute", + typeof(InvalidBoundAttributeWithValid) + .GetProperty(nameof(InvalidBoundAttributeWithValid.IntAttribute))) + }, + new[] + { + string.Format( + errorFormat, + typeof(InvalidBoundAttributeWithValid).FullName, + nameof(InvalidBoundAttributeWithValid.DataSomething), + "data-something") + } + }, + { + typeof(OverriddenInvalidBoundAttributeWithValid), + new[] + { + CreateTagHelperAttributeDescriptor( + "valid-something", + typeof(OverriddenInvalidBoundAttributeWithValid) + .GetProperty(nameof(OverriddenInvalidBoundAttributeWithValid.DataSomething))) + }, + new string[0] + }, + { + typeof(OverriddenValidBoundAttributeWithInvalid), + Enumerable.Empty(), + new[] + { + string.Format( + errorFormat, + typeof(OverriddenValidBoundAttributeWithInvalid).FullName, + nameof(OverriddenValidBoundAttributeWithInvalid.ValidSomething), + "data-something") + } + }, + { + typeof(OverriddenValidBoundAttributeWithInvalidUpperCase), + Enumerable.Empty(), + new[] + { + string.Format( + errorFormat, + typeof(OverriddenValidBoundAttributeWithInvalidUpperCase).FullName, + nameof(OverriddenValidBoundAttributeWithInvalidUpperCase.ValidSomething), + "DATA-SOMETHING") + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidTagHelperAttributeDescriptorData))] + public void CreateDescriptors_DoesNotAllowDataDashAttributes( + Type type, + IEnumerable expectedAttributeDescriptors, + string[] expectedErrors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(type.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + var actualErrors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrors.Length, actualErrors.Length); + + for (var i = 0; i < actualErrors.Length; i++) + { + var actualError = actualErrors[i]; + Assert.Equal(0, actualError.Length); + Assert.Equal(SourceLocation.Zero, actualError.Location); + Assert.Equal(expectedErrors[i], actualError.Message, StringComparer.Ordinal); + } + + var actualDescriptor = Assert.Single(descriptors); + Assert.Equal( + expectedAttributeDescriptors, + actualDescriptor.Attributes, + TagHelperAttributeDescriptorComparer.Default); + } + + // tagTelperType, expectedAttributeDescriptors, expectedErrorMessages + public static TheoryData, string[]> TagHelperWithPrefixData + { + get + { + Func onError = (typeName, propertyName) => + $"Invalid tag helper bound property '{ typeName }.{ propertyName }'. " + + $"'{ typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName }." + + $"{ nameof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute.DictionaryAttributePrefix) }' must be null unless " + + "property type implements 'IDictionary'."; + var dictionaryNamespace = typeof(IDictionary<,>).FullName; + dictionaryNamespace = dictionaryNamespace.Substring(0, dictionaryNamespace.IndexOf('`')); + + // tagTelperType, expectedAttributeDescriptors, expectedErrorMessages + return new TheoryData, string[]> + { + { + typeof(DefaultValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor + { + Name = "dictionary-property", + PropertyName = nameof(DefaultValidHtmlAttributePrefix.DictionaryProperty), + TypeName = $"{dictionaryNamespace}" + }, + new TagHelperAttributeDescriptor + { + Name = "dictionary-property-", + PropertyName = nameof(DefaultValidHtmlAttributePrefix.DictionaryProperty), + TypeName = typeof(string).FullName, + IsIndexer = true + } + }, + new string[0] + }, + { + typeof(SingleValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor + { + Name = "valid-name", + PropertyName = nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), + TypeName = $"{dictionaryNamespace}" + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name-", + PropertyName = nameof(SingleValidHtmlAttributePrefix.DictionaryProperty), + TypeName = typeof(string).FullName, + IsIndexer = true + } + }, + new string[0] + }, + { + typeof(MultipleValidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor + { + Name = "valid-name1", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionaryProperty), + TypeName = $"{typeof(Dictionary<,>).Namespace}.Dictionary" + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name2", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionarySubclassProperty), + TypeName = typeof(DictionarySubclass).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name3", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionaryWithoutParameterlessConstructorProperty), + TypeName = typeof(DictionaryWithoutParameterlessConstructor).FullName + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name4", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.GenericDictionarySubclassProperty), + TypeName = typeof(GenericDictionarySubclass).Namespace + ".GenericDictionarySubclass" + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name5", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.SortedDictionaryProperty), + TypeName = typeof(SortedDictionary).Namespace + ".SortedDictionary" + }, + new TagHelperAttributeDescriptor + { + Name = "valid-name6", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.StringProperty), + TypeName = typeof(string).FullName, + IsStringProperty = true, + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix1-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionaryProperty), + TypeName = typeof(object).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix2-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionarySubclassProperty), + TypeName = typeof(string).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix3-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.DictionaryWithoutParameterlessConstructorProperty), + TypeName = typeof(string).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix4-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.GenericDictionarySubclassProperty), + TypeName = typeof(object).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix5-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.SortedDictionaryProperty), + TypeName = typeof(int).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "get-only-dictionary-property-", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryProperty), + TypeName = typeof(int).FullName, + IsIndexer = true + }, + new TagHelperAttributeDescriptor + { + Name = "valid-prefix6", + PropertyName = nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributePrefix), + TypeName = typeof(string).FullName, + IsIndexer = true + } + }, + new string[0] + }, + { + typeof(SingleInvalidHtmlAttributePrefix), + Enumerable.Empty(), + new[] + { + onError( + typeof(SingleInvalidHtmlAttributePrefix).FullName, + nameof(SingleInvalidHtmlAttributePrefix.StringProperty)), + } + }, + { + typeof(MultipleInvalidHtmlAttributePrefix), + new[] + { + new TagHelperAttributeDescriptor + { + Name = "valid-name1", + PropertyName = nameof(MultipleInvalidHtmlAttributePrefix.LongProperty), + TypeName = typeof(long).FullName + } + }, + new[] + { + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.ReadOnlyDictionaryProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.IntProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)), + onError( + typeof(MultipleInvalidHtmlAttributePrefix).FullName, + nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix)), + $"Invalid tag helper bound property '{ typeof(MultipleInvalidHtmlAttributePrefix).FullName }." + + $"{ nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName) }'. " + + $"'{ typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName }." + + $"{ nameof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute.Name) }' must be null or empty if property has " + + "no public setter.", + } + }, + }; + } + } + + public static TheoryData ValidAttributeNameData + { + get + { + return new TheoryData + { + "data", + "dataa-", + "ValidName", + "valid-name", + "--valid--name--", + ",,--__..oddly.valid::;;", + }; + } + } + + [Theory] + [MemberData(nameof(ValidAttributeNameData))] + public void ValidateTagHelperAttributeDescriptor_WithValidName_ReturnsTrue(string name) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor + { + Name = name, + PropertyName = "ValidProperty", + TypeName = "PropertyType" + }; + var errorSink = new ErrorSink(); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + + // Act + var result = DefaultTagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeSymbol, + errorSink); + + // Assert + Assert.True(result); + Assert.Empty(errorSink.Errors); + } + + public static TheoryData ValidAttributePrefixData + { + get + { + return new TheoryData + { + string.Empty, + "data", + "dataa-", + "ValidName", + "valid-name", + "--valid--name--", + ",,--__..oddly.valid::;;", + }; + } + } + + [Theory] + [MemberData(nameof(ValidAttributePrefixData))] + public void ValidateTagHelperAttributeDescriptor_WithValidPrefix_ReturnsTrue(string prefix) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor + { + Name = prefix, + PropertyName = "ValidProperty", + TypeName = "PropertyType", + IsIndexer = true + }; + var errorSink = new ErrorSink(); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + + // Act + var result = DefaultTagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeSymbol, + errorSink); + + // Assert + Assert.True(result); + Assert.Empty(errorSink.Errors); + } + + // name, expectedErrorMessages + public static TheoryData InvalidAttributeNameData + { + get + { + Func onNameError = (invalidText, invalidCharacter) => "Invalid tag helper " + + $"bound property '{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot " + + $"bind to HTML attributes with name '{ invalidText }' because name contains a " + + $"'{ invalidCharacter }' character."; + var whitespaceErrorString = "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + "attributes with a whitespace name."; + Func onDataError = invalidText => "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + $"attributes with name '{ invalidText }' because name starts with 'data-'."; + + return GetInvalidNameOrPrefixData(onNameError, whitespaceErrorString, onDataError); + } + } + + [Theory] + [MemberData(nameof(InvalidAttributeNameData))] + public void ValidateTagHelperAttributeDescriptor_WithInvalidName_AddsExpectedErrors( + string name, + string[] expectedErrorMessages) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor + { + Name = name, + PropertyName = "InvalidProperty", + TypeName = "PropertyType" + }; + var errorSink = new ErrorSink(); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + + // Act + var result = DefaultTagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeSymbol, + errorSink); + + // Assert + Assert.False(result); + + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + Assert.Equal(0, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + } + + // prefix, expectedErrorMessages + public static TheoryData InvalidAttributePrefixData + { + get + { + Func onPrefixError = (invalidText, invalidCharacter) => "Invalid tag helper " + + $"bound property '{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot " + + $"bind to HTML attributes with prefix '{ invalidText }' because prefix contains a " + + $"'{ invalidCharacter }' character."; + var whitespaceErrorString = "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + "attributes with a whitespace prefix."; + Func onDataError = invalidText => "Invalid tag helper bound property " + + $"'{ typeof(MultiTagTagHelper).FullName }.InvalidProperty'. Tag helpers cannot bind to HTML " + + $"attributes with prefix '{ invalidText }' because prefix starts with 'data-'."; + + return GetInvalidNameOrPrefixData(onPrefixError, whitespaceErrorString, onDataError); + } + } + + [Theory] + [MemberData(nameof(InvalidAttributePrefixData))] + public void ValidateTagHelperAttributeDescriptor_WithInvalidPrefix_AddsExpectedErrors( + string prefix, + string[] expectedErrorMessages) + { + // Arrange + var descriptor = new TagHelperAttributeDescriptor + { + Name = prefix, + PropertyName = "InvalidProperty", + TypeName = "ValuesType", + IsIndexer = true + }; + var errorSink = new ErrorSink(); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + + // Act + var result = DefaultTagHelperDescriptorFactory.ValidateTagHelperAttributeDescriptor( + descriptor, + typeSymbol, + errorSink); + + // Assert + Assert.False(result); + + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + for (var i = 0; i < expectedErrorMessages.Length; i++) + { + Assert.Equal(0, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + } + + public static TheoryData InvalidRestrictChildrenNameData + { + get + { + var nullOrWhiteSpaceError = + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( + typeof(AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute).FullName, + "SomeTagHelper"); + + return GetInvalidNameOrPrefixData( + onNameError: (invalidInput, invalidCharacter) => + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( + typeof(AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute).FullName, + invalidInput, + "SomeTagHelper", + invalidCharacter), + whitespaceErrorString: nullOrWhiteSpaceError, + onDataError: null); + } + } + + [Theory] + [MemberData(nameof(InvalidRestrictChildrenNameData))] + public void GetValidAllowedChildren_AddsExpectedErrors(string name, string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + var expectedErrors = expectedErrorMessages.Select( + message => new RazorError(message, SourceLocation.Zero, 0)); + + // Act + DefaultTagHelperDescriptorFactory.GetValidAllowedChildren(new[] { name }, "SomeTagHelper", errorSink); + + // Assert + Assert.Equal(expectedErrors, errorSink.Errors); + } + + public static TheoryData InvalidParentTagData + { + get + { + var nullOrWhiteSpaceError = + Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace( + Resources.TagHelperDescriptorFactory_ParentTag); + + return GetInvalidNameOrPrefixData( + onNameError: (invalidInput, invalidCharacter) => + Resources.FormatHtmlTargetElementAttribute_InvalidName( + Resources.TagHelperDescriptorFactory_ParentTag.ToLower(), + invalidInput, + invalidCharacter), + whitespaceErrorString: nullOrWhiteSpaceError, + onDataError: null); + } + } + + [Theory] + [MemberData(nameof(InvalidParentTagData))] + public void ValidateParentTagName_AddsExpectedErrors(string name, string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + var expectedErrors = expectedErrorMessages.Select( + message => new RazorError(message, SourceLocation.Zero, 0)); + + // Act + DefaultTagHelperDescriptorFactory.ValidateParentTagName(name, errorSink); + + // Assert + Assert.Equal(expectedErrors, errorSink.Errors); + } + + [Fact] + public void CreateDescriptors_BuildsDescriptorsFromSimpleTypes() + { + // Arrange + var errorSink = new ErrorSink(); + var objectAssemblyName = typeof(object).GetTypeInfo().Assembly.GetName().Name; + var expectedDescriptor = + CreateTagHelperDescriptor("object", "System.Object", objectAssemblyName); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(typeof(object).FullName); + + // Act + var descriptors = factory.CreateDescriptors( + objectAssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Theory] + [MemberData(nameof(TagHelperWithPrefixData))] + public void CreateDescriptors_WithPrefixes_ReturnsExpectedAttributeDescriptors( + Type tagHelperType, + IEnumerable expectedAttributeDescriptors, + string[] expectedErrorMessages) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: false); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + var errors = errorSink.Errors.ToArray(); + Assert.Equal(expectedErrorMessages.Length, errors.Length); + + for (var i = 0; i < errors.Length; i++) + { + Assert.Equal(0, errors[i].Length); + Assert.Equal(SourceLocation.Zero, errors[i].Location); + Assert.Equal(expectedErrorMessages[i], errors[i].Message, StringComparer.Ordinal); + } + + var descriptor = Assert.Single(descriptors); + Assert.Equal( + expectedAttributeDescriptors, + descriptor.Attributes, + TagHelperAttributeDescriptorComparer.Default); + } + + public static TheoryData HtmlConversionData + { + get + { + return new TheoryData + { + { "SomeThing", "some-thing" }, + { "someOtherThing", "some-other-thing" }, + { "capsONInside", "caps-on-inside" }, + { "CAPSOnOUTSIDE", "caps-on-outside" }, + { "ALLCAPS", "allcaps" }, + { "One1Two2Three3", "one1-two2-three3" }, + { "ONE1TWO2THREE3", "one1two2three3" }, + { "First_Second_ThirdHi", "first_second_third-hi" } + }; + } + } + + [Theory] + [MemberData(nameof(HtmlConversionData))] + public void ToHtmlCase_ReturnsExpectedConversions(string input, string expectedOutput) + { + // Arrange, Act + var output = DefaultTagHelperDescriptorFactory.ToHtmlCase(input); + + // Assert + Assert.Equal(output, expectedOutput); + } + + public static TheoryData OutputElementHintData + { + get + { + // tagHelperType, expectedDescriptors + return new TheoryData + { + { + typeof(MulitpleDescriptorTagHelperWithOutputElementHint), + new[] + { + new TagHelperDescriptor + { + TagName = "a", + TypeName = typeof(MulitpleDescriptorTagHelperWithOutputElementHint).FullName, + AssemblyName = AssemblyName, + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + OutputElementHint = "div" + } + }, + new TagHelperDescriptor + { + TagName = "p", + TypeName = typeof(MulitpleDescriptorTagHelperWithOutputElementHint).FullName, + AssemblyName = AssemblyName, + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + OutputElementHint = "div" + } + } + } + }, + { + typeof(InheritedOutputElementHintTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "inherited-output-element-hint", + TypeName = typeof(InheritedOutputElementHintTagHelper).FullName, + AssemblyName = AssemblyName, + DesignTimeDescriptor = null + } + } + }, + { + typeof(OutputElementHintTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "output-element-hint", + TypeName = typeof(OutputElementHintTagHelper).FullName, + AssemblyName = AssemblyName, + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + OutputElementHint = "hinted-value" + } + } + } + }, + { + typeof(OverriddenOutputElementHintTagHelper), + new[] + { + new TagHelperDescriptor + { + TagName = "overridden-output-element-hint", + TypeName = typeof(OverriddenOutputElementHintTagHelper).FullName, + AssemblyName = AssemblyName, + DesignTimeDescriptor = new TagHelperDesignTimeDescriptor + { + OutputElementHint = "overridden" + } + } + } + }, + }; + } + } + + [Theory] + [MemberData(nameof(OutputElementHintData))] + public void CreateDescriptors_CreatesDesignTimeDescriptorsWithOutputElementHint( + Type tagHelperType, + TagHelperDescriptor[] expectedDescriptors) + { + // Arrange + var errorSink = new ErrorSink(); + var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime: true); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + + // We don't care about order. Mono returns reflected attributes differently so we need to ensure order + // doesn't matter by sorting. + descriptors = descriptors.OrderBy(descriptor => descriptor.TagName); + + Assert.Equal(expectedDescriptors, descriptors, CaseSensitiveTagHelperDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_CapturesRemarksAndSummaryOnTagHelperClass() + { + // Arrange + var errorSink = new ErrorSink(); + var sytnaxTree = CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Razor.TagHelpers; + +/// +/// The summary for . +/// +/// +/// Inherits from . +/// +[" + typeof(AspNetCore.Razor.TagHelpers.OutputElementHintAttribute).FullName + @"(""p"")] +public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHelper).Name + @" +{ +}"); + var compilation = TestCompilation.Create(sytnaxTree); + var factory = new DefaultTagHelperDescriptorFactory(compilation, designTime: true); + var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper"); + var expectedDescriptor = new TagHelperDesignTimeDescriptor() + { + OutputElementHint = "p", + Summary = "The summary for .", + Remarks = "Inherits from .", + }; + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var descriptor = Assert.Single(descriptors); + Assert.Equal(expectedDescriptor, descriptor.DesignTimeDescriptor, TagHelperDesignTimeDescriptorComparer.Default); + } + + [Fact] + public void CreateDescriptors_CapturesRemarksAndSummaryOnTagHelperProperties() + { + // Arrange + var errorSink = new ErrorSink(); + var sytnaxTree = CSharpSyntaxTree.ParseText(@" +using System.Collections.Generic; + +public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName + @" +{ + /// + /// This is of type . + /// + public string SummaryProperty { get; set; } + + /// + /// The may be null. + /// + public int RemarksProperty { get; set; } + + /// + /// This is a complex . + /// + /// + /// + /// + public List RemarksAndSummaryProperty { get; set; } +}"); + var compilation = TestCompilation.Create(sytnaxTree); + var factory = new DefaultTagHelperDescriptorFactory(compilation, designTime: true); + var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper"); + var expectedDescriptors = new[] + { + new TagHelperAttributeDesignTimeDescriptor() + { + Summary = "This is of type .", + }, + new TagHelperAttributeDesignTimeDescriptor() + { + Remarks = "The may be null.", + }, + new TagHelperAttributeDesignTimeDescriptor() + { + Summary = "This is a complex .", + Remarks = "", + } + }; + + // Act + var descriptors = factory.CreateDescriptors( + AssemblyName, + typeSymbol, + errorSink: errorSink); + + // Assert + Assert.Empty(errorSink.Errors); + var attributeDescriptors = descriptors + .SelectMany(descriptor => descriptor.Attributes) + .Select(descriptor => descriptor.DesignTimeDescriptor); + Assert.Equal(expectedDescriptors, attributeDescriptors, TagHelperAttributeDesignTimeDescriptorComparer.Default); + } + + private static TheoryData GetInvalidNameOrPrefixData( + Func onNameError, + string whitespaceErrorString, + Func onDataError) + { + // name, expectedErrorMessages + var data = new TheoryData + { + { "!", new[] { onNameError("!", "!") } }, + { "hello!", new[] { onNameError("hello!", "!") } }, + { "!hello", new[] { onNameError("!hello", "!") } }, + { "he!lo", new[] { onNameError("he!lo", "!") } }, + { + "!he!lo!", + new[] + { + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!"), + onNameError("!he!lo!", "!"), + } + }, + { "@", new[] { onNameError("@", "@") } }, + { "hello@", new[] { onNameError("hello@", "@") } }, + { "@hello", new[] { onNameError("@hello", "@") } }, + { "he@lo", new[] { onNameError("he@lo", "@") } }, + { + "@he@lo@", + new[] + { + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@"), + onNameError("@he@lo@", "@"), + } + }, + { "/", new[] { onNameError("/", "/") } }, + { "hello/", new[] { onNameError("hello/", "/") } }, + { "/hello", new[] { onNameError("/hello", "/") } }, + { "he/lo", new[] { onNameError("he/lo", "/") } }, + { + "/he/lo/", + new[] + { + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/"), + onNameError("/he/lo/", "/"), + } + }, + { "<", new[] { onNameError("<", "<") } }, + { "hello<", new[] { onNameError("hello<", "<") } }, + { "", new[] { onNameError(">", ">") } }, + { "hello>", new[] { onNameError("hello>", ">") } }, + { ">hello", new[] { onNameError(">hello", ">") } }, + { "he>lo", new[] { onNameError("he>lo", ">") } }, + { + ">he>lo>", + new[] + { + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">"), + onNameError(">he>lo>", ">"), + } + }, + { "]", new[] { onNameError("]", "]") } }, + { "hello]", new[] { onNameError("hello]", "]") } }, + { "]hello", new[] { onNameError("]hello", "]") } }, + { "he]lo", new[] { onNameError("he]lo", "]") } }, + { + "]he]lo]", + new[] + { + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]"), + onNameError("]he]lo]", "]"), + } + }, + { "=", new[] { onNameError("=", "=") } }, + { "hello=", new[] { onNameError("hello=", "=") } }, + { "=hello", new[] { onNameError("=hello", "=") } }, + { "he=lo", new[] { onNameError("he=lo", "=") } }, + { + "=he=lo=", + new[] + { + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "="), + onNameError("=he=lo=", "="), + } + }, + { "\"", new[] { onNameError("\"", "\"") } }, + { "hello\"", new[] { onNameError("hello\"", "\"") } }, + { "\"hello", new[] { onNameError("\"hello", "\"") } }, + { "he\"lo", new[] { onNameError("he\"lo", "\"") } }, + { + "\"he\"lo\"", + new[] + { + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\""), + onNameError("\"he\"lo\"", "\""), + } + }, + { "'", new[] { onNameError("'", "'") } }, + { "hello'", new[] { onNameError("hello'", "'") } }, + { "'hello", new[] { onNameError("'hello", "'") } }, + { "he'lo", new[] { onNameError("he'lo", "'") } }, + { + "'he'lo'", + new[] + { + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'"), + onNameError("'he'lo'", "'"), + } + }, + { "hello*", new[] { onNameError("hello*", "*") } }, + { "*hello", new[] { onNameError("*hello", "*") } }, + { "he*lo", new[] { onNameError("he*lo", "*") } }, + { + "*he*lo*", + new[] + { + onNameError("*he*lo*", "*"), + onNameError("*he*lo*", "*"), + onNameError("*he*lo*", "*"), + } + }, + { Environment.NewLine, new[] { whitespaceErrorString } }, + { "\t", new[] { whitespaceErrorString } }, + { " \t ", new[] { whitespaceErrorString } }, + { " ", new[] { whitespaceErrorString } }, + { Environment.NewLine + " ", new[] { whitespaceErrorString } }, + { + "! \t\r\n@/<>?[]=\"'*", + new[] + { + 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<>?[]=\"'*", + new[] + { + 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 != null) + { + data.Add("data-", new[] { onDataError("data-") }); + data.Add("data-something", new[] { onDataError("data-something") }); + data.Add("Data-Something", new[] { onDataError("Data-Something") }); + data.Add("DATA-SOMETHING", new[] { onDataError("DATA-SOMETHING") }); + } + + return data; + } + + protected static TagHelperDescriptor CreateTagHelperDescriptor( + string tagName, + string typeName, + string assemblyName, + IEnumerable attributes = null, + IEnumerable requiredAttributes = null) + { + return new TagHelperDescriptor + { + TagName = tagName, + TypeName = typeName, + AssemblyName = assemblyName, + Attributes = attributes ?? Enumerable.Empty(), + RequiredAttributes = requiredAttributes ?? Enumerable.Empty() + }; + } + + private static TagHelperAttributeDescriptor CreateTagHelperAttributeDescriptor( + string name, + PropertyInfo propertyInfo) + { + return new TagHelperAttributeDescriptor + { + Name = name, + PropertyName = propertyInfo.Name, + TypeName = propertyInfo.PropertyType.FullName, + IsStringProperty = propertyInfo.PropertyType.FullName == typeof(string).FullName + }; + } + } + + [AspNetCore.Razor.TagHelpers.OutputElementHint("hinted-value")] + public class OutputElementHintTagHelper : AspNetCore.Razor.TagHelpers.TagHelper + { + } + + public class InheritedOutputElementHintTagHelper : OutputElementHintTagHelper + { + } + + [AspNetCore.Razor.TagHelpers.OutputElementHint("overridden")] + public class OverriddenOutputElementHintTagHelper : OutputElementHintTagHelper + { + } +} \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperResolverTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperResolverTest.cs new file mode 100644 index 0000000000..32295caa52 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperResolverTest.cs @@ -0,0 +1,125 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces +{ + public class DefaultTagHelperResolverTest + { + private static Compilation Compilation { get; } = TestCompilation.Create(); + + private static INamedTypeSymbol ITagHelperSymbol { get; } = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); + + private DefaultTagHelperResolver.Visitor TestVisitor => new DefaultTagHelperResolver.Visitor(ITagHelperSymbol, new List()); + + [Fact] + public void IsTagHelper_PlainTagHelper_ReturnsTrue() + { + // Arrange + var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainTagHelper).FullName); + + // Act + var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol); + + // Assert + Assert.True(isTagHelper); + } + + [Fact] + public void IsTagHelper_InheritedTagHelper_ReturnsTrue() + { + // Arrange + var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedTagHelper).FullName); + + // Act + var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol); + + // Assert + Assert.True(isTagHelper); + } + + [Fact] + public void IsTagHelper_AbstractTagHelper_ReturnsFalse() + { + // Arrange + var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractTagHelper).FullName); + + // Act + var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol); + + // Assert + Assert.False(isTagHelper); + } + + [Fact] + public void IsTagHelper_GenericTagHelper_ReturnsFalse() + { + // Arrange + var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericTagHelper<>).FullName); + + // Act + var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol); + + // Assert + Assert.False(isTagHelper); + } + + [Fact] + public void IsTagHelper_InternalTagHelper_ReturnsFalse() + { + // Arrange + var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalTagHelper).FullName); + + // Act + var isTagHelper = TestVisitor.IsTagHelper(tagHelperSymbol); + + // Assert + Assert.False(isTagHelper); + } + + [Fact] + public void GetTagHelpers_NestedTagHelpersAreNotFound() + { + // Arrange + var resolver = new DefaultTagHelperResolver(designTime: false); + + // Act + var descriptors = resolver.GetTagHelpers(Compilation); + + // Assert + var matchingDescriptors = descriptors + .Where(descriptor => string.Equals(descriptor.TypeName, typeof(Invalid_NestedPublicTagHelper).FullName, StringComparison.Ordinal)); + Assert.Empty(matchingDescriptors); + } + + public class Invalid_NestedPublicTagHelper : TagHelper + { + } + } + + public abstract class Invalid_AbstractTagHelper : TagHelper + { + } + + public class Invalid_GenericTagHelper : TagHelper + { + } + + internal class Invalid_InternalTagHelper : TagHelper + { + } + + public class Valid_PlainTagHelper : TagHelper + { + } + + public class Valid_InheritedTagHelper : Valid_PlainTagHelper + { + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj index 1be8fc02bf..07cd715c21 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj @@ -1,6 +1,8 @@ - + + true + $(NoWarn);CS1591 netcoreapp1.1;net451 @@ -14,8 +16,11 @@ + + + @@ -23,4 +28,7 @@ + + + \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TagHelperDescriptorFactoryTagHelpers.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TagHelperDescriptorFactoryTagHelpers.cs new file mode 100644 index 0000000000..3363ad9081 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TagHelperDescriptorFactoryTagHelpers.cs @@ -0,0 +1,452 @@ +// 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.Generic; +using System.ComponentModel; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test +{ + public enum CustomEnum + { + FirstValue, + SecondValue + } + + public class EnumTagHelper : TagHelper + { + public int NonEnumProperty { get; set; } + + public CustomEnum EnumProperty { get; set; } + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("input")] + public class MultiEnumTagHelper : EnumTagHelper + { + } + + [HtmlTargetElement("input", ParentTag = "div")] + public class RequiredParentTagHelper : TagHelper + { + } + + [HtmlTargetElement("p", ParentTag = "div")] + [HtmlTargetElement("input", ParentTag = "section")] + public class MultiSpecifiedRequiredParentTagHelper : TagHelper + { + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("input", ParentTag = "div")] + public class MultiWithUnspecifiedRequiredParentTagHelper : TagHelper + { + } + + + [RestrictChildren("p")] + public class RestrictChildrenTagHelper + { + } + + [RestrictChildren("p", "strong")] + public class DoubleRestrictChildrenTagHelper + { + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [RestrictChildren("p", "strong")] + public class MultiTargetRestrictChildrenTagHelper + { + } + + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class TagStructureTagHelper : TagHelper + { + } + + [HtmlTargetElement("p", TagStructure = TagStructure.NormalOrSelfClosing)] + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class MultiSpecifiedTagStructureTagHelper : TagHelper + { + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class MultiWithUnspecifiedTagStructureTagHelper : TagHelper + { + } + + [EditorBrowsable(EditorBrowsableState.Always)] + public class DefaultEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Always)] + public int Property { get; set; } + } + + public class HiddenPropertyEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public int Property { get; set; } + } + + public class MultiPropertyEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public int Property { get; set; } + + public virtual int Property2 { get; set; } + } + + public class OverriddenPropertyEditorBrowsableTagHelper : MultiPropertyEditorBrowsableTagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public override int Property2 { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public class EditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual int Property { get; set; } + } + + public class InheritedEditorBrowsableTagHelper : EditorBrowsableTagHelper + { + public override int Property { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public class OverriddenEditorBrowsableTagHelper : EditorBrowsableTagHelper + { + [EditorBrowsable(EditorBrowsableState.Advanced)] + public override int Property { get; set; } + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class MultiEditorBrowsableTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "class*")] + public class AttributeWildcardTargetingTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "class*,style*")] + public class MultiAttributeWildcardTargetingTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "class")] + public class AttributeTargetingTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "class,style")] + public class MultiAttributeTargetingTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "custom")] + [HtmlTargetElement(Attributes = "class,style")] + public class MultiAttributeAttributeTargetingTagHelper : TagHelper + { + } + + [HtmlTargetElement(Attributes = "style")] + public class InheritedAttributeTargetingTagHelper : AttributeTargetingTagHelper + { + } + + [HtmlTargetElement("input", Attributes = "class")] + public class RequiredAttributeTagHelper : TagHelper + { + } + + [HtmlTargetElement("div", Attributes = "class")] + public class InheritedRequiredAttributeTagHelper : RequiredAttributeTagHelper + { + } + + [HtmlTargetElement("div", Attributes = "class")] + [HtmlTargetElement("input", Attributes = "class")] + public class MultiAttributeRequiredAttributeTagHelper : TagHelper + { + } + + [HtmlTargetElement("input", Attributes = "style")] + [HtmlTargetElement("input", Attributes = "class")] + public class MultiAttributeSameTagRequiredAttributeTagHelper : TagHelper + { + } + + [HtmlTargetElement("input", Attributes = "class,style")] + public class MultiRequiredAttributeTagHelper : TagHelper + { + } + + [HtmlTargetElement("div", Attributes = "style")] + public class InheritedMultiRequiredAttributeTagHelper : MultiRequiredAttributeTagHelper + { + } + + [HtmlTargetElement("div", Attributes = "class,style")] + [HtmlTargetElement("input", Attributes = "class,style")] + public class MultiTagMultiRequiredAttributeTagHelper : TagHelper + { + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + public class MultiTagTagHelper + { + public string ValidAttribute { get; set; } + } + + public class InheritedMultiTagTagHelper : MultiTagTagHelper + { + } + + [HtmlTargetElement("p")] + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [HtmlTargetElement("div")] + public class DuplicateTagNameTagHelper + { + } + + [HtmlTargetElement("data-condition")] + public class OverrideNameTagHelper + { + } + + public class InheritedSingleAttributeTagHelper : SingleAttributeTagHelper + { + } + + public class DuplicateAttributeNameTagHelper + { + public string MyNameIsLegion { get; set; } + + [HtmlAttributeName("my-name-is-legion")] + public string Fred { get; set; } + } + + public class NotBoundAttributeTagHelper + { + public object BoundProperty { get; set; } + + [HtmlAttributeNotBound] + public string NotBoundProperty { get; set; } + + [HtmlAttributeName("unused")] + [HtmlAttributeNotBound] + public string NamedNotBoundProperty { get; set; } + } + + public class OverriddenAttributeTagHelper + { + [HtmlAttributeName("SomethingElse")] + public virtual string ValidAttribute1 { get; set; } + + [HtmlAttributeName("Something-Else")] + public string ValidAttribute2 { get; set; } + } + + public class InheritedOverriddenAttributeTagHelper : OverriddenAttributeTagHelper + { + public override string ValidAttribute1 { get; set; } + } + + public class InheritedNotOverriddenAttributeTagHelper : OverriddenAttributeTagHelper + { + } + + public class ALLCAPSTAGHELPER : TagHelper + { + public int ALLCAPSATTRIBUTE { get; set; } + } + + public class CAPSOnOUTSIDETagHelper : TagHelper + { + public int CAPSOnOUTSIDEATTRIBUTE { get; set; } + } + + public class capsONInsideTagHelper : TagHelper + { + public int capsONInsideattribute { get; set; } + } + + public class One1Two2Three3TagHelper : TagHelper + { + public int One1Two2Three3Attribute { get; set; } + } + + public class ONE1TWO2THREE3TagHelper : TagHelper + { + public int ONE1TWO2THREE3Attribute { get; set; } + } + + public class First_Second_ThirdHiTagHelper : TagHelper + { + public int First_Second_ThirdAttribute { get; set; } + } + + public class UNSuffixedCLASS : TagHelper + { + public int UNSuffixedATTRIBUTE { get; set; } + } + + public class InvalidBoundAttribute : TagHelper + { + public string DataSomething { get; set; } + } + + public class InvalidBoundAttributeWithValid : SingleAttributeTagHelper + { + public string DataSomething { get; set; } + } + + public class OverriddenInvalidBoundAttributeWithValid : TagHelper + { + [HtmlAttributeName("valid-something")] + public string DataSomething { get; set; } + } + + public class OverriddenValidBoundAttributeWithInvalid : TagHelper + { + [HtmlAttributeName("data-something")] + public string ValidSomething { get; set; } + } + + public class OverriddenValidBoundAttributeWithInvalidUpperCase : TagHelper + { + [HtmlAttributeName("DATA-SOMETHING")] + public string ValidSomething { get; set; } + } + + public class DefaultValidHtmlAttributePrefix : TagHelper + { + public IDictionary DictionaryProperty { get; set; } + } + + public class SingleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name")] + public IDictionary DictionaryProperty { get; set; } + } + + public class MultipleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1", DictionaryAttributePrefix = "valid-prefix1-")] + public Dictionary DictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public DictionarySubclass DictionarySubclassProperty { get; set; } + + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public DictionaryWithoutParameterlessConstructor DictionaryWithoutParameterlessConstructorProperty { get; set; } + + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public GenericDictionarySubclass GenericDictionarySubclassProperty { get; set; } + + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public SortedDictionary SortedDictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name6")] + public string StringProperty { get; set; } + + public IDictionary GetOnlyDictionaryProperty { get; } + + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryPropertyWithAttributePrefix { get; } + } + + public class SingleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] + public string StringProperty { get; set; } + } + + public class MultipleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1")] + public long LongProperty { get; set; } + + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public Dictionary DictionaryOfIntProperty { get; set; } + + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public IReadOnlyDictionary ReadOnlyDictionaryProperty { get; set; } + + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public int IntProperty { get; set; } + + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } + + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryAttributePrefix { get; } + + [HtmlAttributeName("invalid-name7")] + public IDictionary GetOnlyDictionaryPropertyWithAttributeName { get; } + } + + public class DictionarySubclass : Dictionary + { + } + + public class DictionaryWithoutParameterlessConstructor : Dictionary + { + public DictionaryWithoutParameterlessConstructor(int count) + : base() + { + } + } + + public class DictionaryOfIntSubclass : Dictionary + { + } + + public class GenericDictionarySubclass : Dictionary + { + } + + [OutputElementHint("strong")] + public class OutputElementHintTagHelper : TagHelper + { + } + + [HtmlTargetElement("a")] + [HtmlTargetElement("p")] + [OutputElementHint("div")] + public class MulitpleDescriptorTagHelperWithOutputElementHint : TagHelper + { + } + + public class NonPublicAccessorTagHelper : TagHelper + { + public string ValidAttribute { get; set; } + public string InvalidPrivateSetAttribute { get; private set; } + public string InvalidPrivateGetAttribute { private get; set; } + protected string InvalidProtectedAttribute { get; set; } + internal string InvalidInternalAttribute { get; set; } + protected internal string InvalidProtectedInternalAttribute { get; set; } + } + + public class SingleAttributeTagHelper : TagHelper + { + public int IntAttribute { get; set; } + } + + public class MissingAccessorTagHelper : TagHelper + { + public string ValidAttribute { get; set; } + public string InvalidNoGetAttribute { set { } } + public string InvalidNoSetAttribute { get { return string.Empty; } } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TestCompilation.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TestCompilation.cs new file mode 100644 index 0000000000..b6457fdbf1 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/TestCompilation.cs @@ -0,0 +1,30 @@ +// 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.Linq; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test +{ + public static class TestCompilation + { + public static Compilation Create(SyntaxTree syntaxTree = null) + { + IEnumerable syntaxTrees = null; + + if (syntaxTree != null) + { + syntaxTrees = new[] { syntaxTree }; + } + var references = AppDomain.CurrentDomain + .GetAssemblies() + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)); + + var compilation = CSharpCompilation.Create("TestAssembly", syntaxTrees, references); + + return compilation; + } + } +} diff --git a/tooling/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs b/tooling/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs index e4268d984b..6b39c158d9 100644 --- a/tooling/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs +++ b/tooling/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs @@ -22,7 +22,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor if (client == null) { // The OOP host is turned off, so let's do this in process. - var resolver = new CodeAnalysis.Razor.DefaultTagHelperResolver(); + var resolver = new CodeAnalysis.Razor.DefaultTagHelperResolver(designTime: true); return await resolver.GetTagHelpersAsync(project, CancellationToken.None).ConfigureAwait(false); }