diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_DesignTime.ir.txt b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_DesignTime.ir.txt index 7cc88c4c21..cc83c0d2a6 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_DesignTime.ir.txt +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/BasicComponent_DesignTime.ir.txt @@ -8,7 +8,6 @@ Document - UsingDirective - (102:4,1 [37] ) - Microsoft.AspNetCore.Components ClassDeclaration - - public - AspNetCore_d3c3d6059615673cb46fc4974164d61eabadb890 - Microsoft.AspNetCore.Components.ComponentBase - IDisposable DesignTimeDirective - - DirectiveToken - (14:0,14 [36] ) - "*, Microsoft.AspNetCore.Components" DirectiveToken - (12:0,12 [11] BasicComponent.cshtml) - IDisposable CSharpCode - IntermediateToken - - CSharp - #pragma warning disable 0414 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs index 62c1fab448..9a3fd27c1e 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDesignTimeNodeWriter.cs @@ -457,6 +457,30 @@ namespace Microsoft.AspNetCore.Razor.Language.Components context.CodeWriter.Write(");"); context.CodeWriter.WriteLine(); } + + // We want to generate something that references the Component type to avoid + // the "usings directive is unnecessary" message. + // Looks like: + // __o = typeof(SomeNamespace.SomeComponent); + using (context.CodeWriter.BuildLinePragma(node.Source.Value)) + { + context.CodeWriter.Write(DesignTimeVariable); + context.CodeWriter.Write(" = "); + context.CodeWriter.Write("typeof("); + context.CodeWriter.Write(node.TagName); + if (node.Component.IsGenericTypedComponent()) + { + context.CodeWriter.Write("<"); + var typeArgumentCount = node.Component.GetTypeParameters().Count(); + for (var i = 1; i < typeArgumentCount; i++) + { + context.CodeWriter.Write(","); + } + context.CodeWriter.Write(">"); + } + context.CodeWriter.Write(");"); + context.CodeWriter.WriteLine(); + } } public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeIntermediateNode node) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs index 06902eb7e2..ba2f30fa2b 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDiagnosticFactory.cs @@ -12,9 +12,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Components { internal static class ComponentDiagnosticFactory { - public static readonly RazorDiagnosticDescriptor CodeBlockInAttribute = - new RazorDiagnosticDescriptor( - "BL9979", + private const string DiagnosticPrefix = "RZ"; + + public static readonly RazorDiagnosticDescriptor UnsupportedTagHelperDirective = new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}9978", + () => + "The directives @addTagHelper, @removeTagHelper and @tagHelperPrefix are not valid in a component document." + + "Use '@using ' directive instead.", + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic Create_UnsupportedTagHelperDirective(SourceSpan? source) + { + return RazorDiagnostic.Create(UnsupportedTagHelperDirective, source ?? SourceSpan.Undefined); + } + + public static readonly RazorDiagnosticDescriptor CodeBlockInAttribute = new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}9979", () => "Code blocks delimited by '@{...}' like '@{{ {0} }}' for attributes are no longer supported " + "These features have been changed to use attribute syntax. " + @@ -31,7 +44,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor UnclosedTag = new RazorDiagnosticDescriptor( - "BL9980", + $"{DiagnosticPrefix}9980", () => "Unclosed tag '{0}' with no matching end tag.", RazorDiagnosticSeverity.Error); @@ -41,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor UnexpectedClosingTag = new RazorDiagnosticDescriptor( - "BL9981", + $"{DiagnosticPrefix}9981", () => "Unexpected closing tag '{0}' with no matching start tag.", RazorDiagnosticSeverity.Error); @@ -51,7 +64,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor UnexpectedClosingTagForVoidElement = new RazorDiagnosticDescriptor( - "BL9983", + $"{DiagnosticPrefix}9983", () => "Unexpected closing tag '{0}'. The element '{0}' is a void element, and should be used without a closing tag.", RazorDiagnosticSeverity.Error); @@ -61,7 +74,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor InvalidHtmlContent = new RazorDiagnosticDescriptor( - "BL9984", + $"{DiagnosticPrefix}9984", () => "Found invalid HTML content. Text '{0}'", RazorDiagnosticSeverity.Error); @@ -71,7 +84,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor MultipleComponents = new RazorDiagnosticDescriptor( - "BL9985", + $"{DiagnosticPrefix}9985", () => "Multiple components use the tag '{0}'. Components: {1}", RazorDiagnosticSeverity.Error); @@ -81,7 +94,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor UnsupportedComplexContent = new RazorDiagnosticDescriptor( - "BL9986", + $"{DiagnosticPrefix}9986", () => "Component attributes do not support complex content (mixed C# and markup). Attribute: '{0}', text '{1}'", RazorDiagnosticSeverity.Error); @@ -93,7 +106,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor PageDirective_CannotBeImported = new RazorDiagnosticDescriptor( - "BL9987", + $"{DiagnosticPrefix}9987", () => ComponentResources.PageDirectiveCannotBeImported, RazorDiagnosticSeverity.Error); @@ -107,7 +120,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor PageDirective_MustSpecifyRoute = new RazorDiagnosticDescriptor( - "BL9988", + $"{DiagnosticPrefix}9988", () => "The @page directive must specify a route template. The route template must be enclosed in quotes and begin with the '/' character.", RazorDiagnosticSeverity.Error); @@ -119,7 +132,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor BindAttribute_Duplicates = new RazorDiagnosticDescriptor( - "BL9989", + $"{DiagnosticPrefix}9989", () => "The attribute '{0}' was matched by multiple bind attributes. Duplicates:{1}", RazorDiagnosticSeverity.Error); @@ -135,7 +148,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor EventHandler_Duplicates = new RazorDiagnosticDescriptor( - "BL9990", + $"{DiagnosticPrefix}9990", () => "The attribute '{0}' was matched by multiple event handlers attributes. Duplicates:{1}", RazorDiagnosticSeverity.Error); @@ -151,7 +164,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor BindAttribute_InvalidSyntax = new RazorDiagnosticDescriptor( - "BL9991", + $"{DiagnosticPrefix}9991", () => "The attribute names could not be inferred from bind attribute '{0}'. Bind attributes should be of the form" + "'bind', 'bind-value' or 'bind-value-change'", RazorDiagnosticSeverity.Error); @@ -166,7 +179,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components } public static readonly RazorDiagnosticDescriptor DisallowedScriptTag = new RazorDiagnosticDescriptor( - "BL9992", + $"{DiagnosticPrefix}9992", () => "Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the 'index.html' file or another static location. For more information see https://go.microsoft.com/fwlink/?linkid=872131", RazorDiagnosticSeverity.Error); @@ -180,7 +193,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor TemplateInvalidLocation = new RazorDiagnosticDescriptor( - "BL9994", + $"{DiagnosticPrefix}9994", () => "Razor templates cannot be used in attributes.", RazorDiagnosticSeverity.Error); @@ -191,7 +204,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentSetByAttributeAndBody = new RazorDiagnosticDescriptor( - "BL9995", + $"{DiagnosticPrefix}9995", () => "The child content property '{0}' is set by both the attribute and the element contents.", RazorDiagnosticSeverity.Error); @@ -202,7 +215,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentMixedWithExplicitChildContent = new RazorDiagnosticDescriptor( - "BL9996", + $"{DiagnosticPrefix}9996", () => "Unrecognized child content inside component '{0}'. The component '{0}' accepts child content through the " + "following top-level items: {1}.", RazorDiagnosticSeverity.Error); @@ -215,7 +228,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidAttribute = new RazorDiagnosticDescriptor( - "BL9997", + $"{DiagnosticPrefix}9997", () => "Unrecognized attribute '{0}' on child content element '{1}'.", RazorDiagnosticSeverity.Error); @@ -226,7 +239,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameter = new RazorDiagnosticDescriptor( - "BL9998", + $"{DiagnosticPrefix}9998", () => "Invalid parameter name. The parameter name attribute '{0}' on child content element '{1}' can only include literal text.", RazorDiagnosticSeverity.Error); @@ -237,7 +250,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentRepeatedParameterName = new RazorDiagnosticDescriptor( - "BL9999", + $"{DiagnosticPrefix}9999", () => "The child content element '{0}' of component '{1}' uses the same parameter name ('{2}') as enclosing child content " + "element '{3}' of component '{4}'. Specify the parameter name like: '<{0} Context=\"another_name\"> to resolve the ambiguity", RazorDiagnosticSeverity.Error); @@ -265,7 +278,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor GenericComponentMissingTypeArgument = new RazorDiagnosticDescriptor( - "BL10000", + $"{DiagnosticPrefix}10000", () => "The component '{0}' is missing required type arguments. Specify the missing types using the attributes: {1}.", RazorDiagnosticSeverity.Error); @@ -282,7 +295,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor GenericComponentTypeInferenceUnderspecified = new RazorDiagnosticDescriptor( - "BL10001", + $"{DiagnosticPrefix}10001", () => "The type of component '{0}' cannot be inferred based on the values provided. Consider specifying the type arguments " + "directly using the following attributes: {1}.", RazorDiagnosticSeverity.Error); @@ -300,7 +313,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly RazorDiagnosticDescriptor ChildContentHasInvalidParameterOnComponent = new RazorDiagnosticDescriptor( - "BL10002", + $"{DiagnosticPrefix}10002", () => "Invalid parameter name. The parameter name attribute '{0}' on component '{1}' can only include literal text.", RazorDiagnosticSeverity.Error); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs index 9c4a7a9f2a..1fa21843f7 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentDocumentClassifierPass.cs @@ -14,8 +14,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Components { public static readonly string ComponentDocumentKind = "component.1.0"; private static readonly object BuildRenderTreeBaseCallAnnotation = new object(); - private static readonly char[] PathSeparators = new char[] { '/', '\\' }; - private static readonly char[] NamespaceSeparators = new char[] { '.' }; /// /// The fallback value of the root namespace. Only used if the fallback root namespace @@ -57,13 +55,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components ClassDeclarationIntermediateNode @class, MethodDeclarationIntermediateNode method) { - var options = codeDocument.GetDocumentIntermediateNode().Options; - if (!TryComputeNamespaceAndClass( - options, - codeDocument.Source.FilePath, - codeDocument.Source.RelativePath, - out var computedNamespace, - out var computedClass)) + if (!codeDocument.TryComputeNamespaceAndClass(out var computedNamespace, out var computedClass)) { // If we can't compute a nice namespace (no relative path) then just generate something // mangled. @@ -74,7 +66,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components if (MangleClassNames) { - computedClass = "__" + computedClass; + computedClass = ComponentMetadata.MangleClassName(computedClass); } @namespace.Content = computedNamespace; @@ -121,59 +113,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Components method.Children.Insert(0, callBase); } - // In general documents will have a relative path (relative to the project root). - // We can only really compute a nice class/namespace when we know a relative path. - // - // However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't - // set up correctly. - private bool TryComputeNamespaceAndClass( - RazorCodeGenerationOptions options, - string filePath, - string relativePath, - out string @namespace, - out string @class) - { - if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length) - { - @namespace = null; - @class = null; - return false; - } - - var rootNamespace = options.RootNamespace; - if (string.IsNullOrEmpty(rootNamespace)) - { - @namespace = null; - @class = null; - return false; - } - - var builder = new StringBuilder(); - - // Sanitize the base namespace, but leave the dots. - var segments = rootNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries); - builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[0])); - for (var i = 1; i < segments.Length; i++) - { - builder.Append('.'); - builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); - } - - segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries); - - // Skip the last segment because it's the FileName. - for (var i = 0; i < segments.Length - 1; i++) - { - builder.Append('.'); - builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); - } - - @namespace = builder.ToString(); - @class = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(relativePath)); - - return true; - } - internal static bool IsBuildRenderTreeBaseCall(CSharpCodeIntermediateNode node) => node.Annotations[BuildRenderTreeBaseCallAnnotation] != null; } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentImportProjectFeature.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentImportProjectFeature.cs index 367bca910d..2e689044de 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentImportProjectFeature.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentImportProjectFeature.cs @@ -40,28 +40,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components var imports = new List() { new VirtualProjectItem(DefaultUsingImportContent), - new VirtualProjectItem(@"@addTagHelper ""*, Microsoft.AspNetCore.Components"""), }; - // Try and infer a namespace from the project directory. We don't yet have the ability to pass - // the namespace through from the project. - if (projectItem.PhysicalPath != null && projectItem.FilePath != null) - { - // Avoiding the path-specific APIs here, we want to handle all styles of paths - // on all platforms - var trimLength = projectItem.FilePath.Length + (projectItem.FilePath.StartsWith("/") ? 0 : 1); - if (projectItem.PhysicalPath.Length > trimLength) - { - var baseDirectory = projectItem.PhysicalPath.Substring(0, projectItem.PhysicalPath.Length - trimLength); - var lastSlash = baseDirectory.LastIndexOfAny(PathSeparators); - var baseNamespace = lastSlash == -1 ? baseDirectory : baseDirectory.Substring(lastSlash + 1); - if (!string.IsNullOrEmpty(baseNamespace)) - { - imports.Add(new VirtualProjectItem($@"@addTagHelper ""*, {baseNamespace}""")); - } - } - } - // We add hierarchical imports second so any default directive imports can be overridden. imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem)); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs index ad3a7cd44d..17966db799 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/ComponentMetadata.cs @@ -1,11 +1,15 @@ // 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; + namespace Microsoft.AspNetCore.Razor.Language.Components { // Metadata used for Components interactions with the tag helper system internal static class ComponentMetadata { + private static readonly string MangledClassNamePrefix = "__generated__"; + // There's a bug in the 15.7 preview 1 Razor that prevents 'Kind' from being serialized // this affects both tooling and build. For now our workaround is to ignore 'Kind' and // use our own metadata entry to denote non-Component tag helpers. @@ -13,6 +17,35 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public static readonly string ImportsFileName = "_Imports.razor"; + public static bool IsComponentTagHelperKind(string tagHelperKind) + { + return tagHelperKind == Component.TagHelperKind || + tagHelperKind == ChildContent.TagHelperKind || + tagHelperKind == EventHandler.TagHelperKind || + tagHelperKind == Bind.TagHelperKind || + tagHelperKind == Ref.TagHelperKind; + } + + public static string MangleClassName(string className) + { + if (string.IsNullOrEmpty(className)) + { + return string.Empty; + } + + return MangledClassNamePrefix + className; + } + + public static bool IsMangledClass(string className) + { + if (string.IsNullOrEmpty(className)) + { + return false; + } + + return className.StartsWith(MangledClassNamePrefix, StringComparison.Ordinal); + } + public static class Bind { public static readonly string RuntimeName = "Components.None"; @@ -68,6 +101,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Components public readonly static string GenericTypedKey = "Components.GenericTyped"; public readonly static string TypeParameterKey = "Components.TypeParameter"; + + public readonly static string NameMatchKey = "Components.NameMatch"; + + public readonly static string FullyQualifiedNameMatch = "Components.FullyQualifiedNameMatch"; } public static class EventHandler diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs index 005da7cccd..1bbd8d8ace 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Components/TagHelperDescriptorExtensions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.AspNetCore.Razor.Language.Components { @@ -152,6 +151,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Components string.Equals(ComponentMetadata.Ref.TagHelperKind, kind); } + /// + /// Gets whether the component matches a tag with a fully qualified name. + /// + /// The . + public static bool IsComponentFullyQualifiedNameMatch(this TagHelperDescriptor tagHelper) + { + if (tagHelper == null) + { + throw new ArgumentNullException(nameof(tagHelper)); + } + + return + tagHelper.Metadata.TryGetValue(ComponentMetadata.Component.NameMatchKey, out var matchType) && + string.Equals(ComponentMetadata.Component.FullyQualifiedNameMatch, matchType); + } + public static string GetEventArgsType(this TagHelperDescriptor tagHelper) { if (tagHelper == null) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptions.cs index 6bb6d2c284..b6156641b5 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptions.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Razor.Language { internal class DefaultRazorParserOptions : RazorParserOptions { - public DefaultRazorParserOptions(DirectiveDescriptor[] directives, bool designTime, bool parseLeadingDirectives, RazorLanguageVersion version) + public DefaultRazorParserOptions(DirectiveDescriptor[] directives, bool designTime, bool parseLeadingDirectives, RazorLanguageVersion version, string fileKind) { if (directives == null) { @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Razor.Language ParseLeadingDirectives = parseLeadingDirectives; Version = version; FeatureFlags = RazorParserFeatureFlags.Create(Version); + FileKind = fileKind; } public override bool DesignTime { get; } @@ -30,6 +31,8 @@ namespace Microsoft.AspNetCore.Razor.Language public override RazorLanguageVersion Version { get; } + internal override string FileKind { get; } + internal override RazorParserFeatureFlags FeatureFlags { get; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsBuilder.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsBuilder.cs index 1f3deb7213..e19a4d1660 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsBuilder.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsBuilder.cs @@ -23,10 +23,11 @@ namespace Microsoft.AspNetCore.Razor.Language FileKind = fileKind; } - public DefaultRazorParserOptionsBuilder(bool designTime, RazorLanguageVersion version) + public DefaultRazorParserOptionsBuilder(bool designTime, RazorLanguageVersion version, string fileKind) { _designTime = designTime; LanguageVersion = version; + FileKind = fileKind; } public override RazorConfiguration Configuration { get; } @@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.Razor.Language public override RazorParserOptions Build() { - return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives, LanguageVersion); + return new DefaultRazorParserOptions(Directives.ToArray(), DesignTime, ParseLeadingDirectives, LanguageVersion, FileKind); } public override void SetDesignTime(bool designTime) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsFeature.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsFeature.cs index eecb9bd9cc..956d63ff46 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsFeature.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorParserOptionsFeature.cs @@ -11,12 +11,14 @@ namespace Microsoft.AspNetCore.Razor.Language { private readonly bool _designTime; private readonly RazorLanguageVersion _version; + private readonly string _fileKind; private IConfigureRazorParserOptionsFeature[] _configureOptions; - public DefaultRazorParserOptionsFeature(bool designTime, RazorLanguageVersion version) + public DefaultRazorParserOptionsFeature(bool designTime, RazorLanguageVersion version, string fileKind) { _designTime = designTime; _version = version; + _fileKind = fileKind; } protected override void OnInitialized() @@ -26,7 +28,7 @@ namespace Microsoft.AspNetCore.Razor.Language public RazorParserOptions GetOptions() { - var builder = new DefaultRazorParserOptionsBuilder(_designTime, _version); + var builder = new DefaultRazorParserOptionsBuilder(_designTime, _version, _fileKind); for (var i = 0; i < _configureOptions.Length; i++) { _configureOptions[i].Configure(builder); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperBinderPhase.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperBinderPhase.cs index c15aaff04e..34a2d77613 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperBinderPhase.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/DefaultRazorTagHelperBinderPhase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; @@ -11,6 +12,8 @@ namespace Microsoft.AspNetCore.Razor.Language { internal class DefaultRazorTagHelperBinderPhase : RazorEnginePhaseBase, IRazorTagHelperBinderPhase { + private static readonly char[] NamespaceSeparators = new char[] { '.' }; + protected override void ExecuteCore(RazorCodeDocument codeDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); @@ -33,20 +36,31 @@ namespace Microsoft.AspNetCore.Razor.Language // // The imports come logically before the main razor file and are in the order they // should be processed. - var visitor = new DirectiveVisitor(descriptors); + DirectiveVisitor visitor = null; + if (FileKinds.IsComponent(codeDocument.GetFileKind())) + { + codeDocument.TryComputeNamespaceAndClass(out var currentNamespace, out var _); + visitor = new ComponentDirectiveVisitor(codeDocument.Source.FilePath, descriptors, currentNamespace); + } + else + { + visitor = new TagHelperDirectiveVisitor(descriptors); + } var imports = codeDocument.GetImportSyntaxTrees(); if (imports != null) { for (var i = 0; i < imports.Count; i++) { var import = imports[i]; - visitor.Visit(import.Root); + visitor.Visit(import); } } - visitor.Visit(syntaxTree.Root); + visitor.Visit(syntaxTree); + // This will always be null for a component document. var tagHelperPrefix = visitor.TagHelperPrefix; + descriptors = visitor.Matches.ToArray(); var context = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors); @@ -86,20 +100,38 @@ namespace Microsoft.AspNetCore.Razor.Language return string.Equals(descriptor.Name, typePattern, StringComparison.Ordinal); } - internal class DirectiveVisitor : SyntaxRewriter + + + internal abstract class DirectiveVisitor : SyntaxWalker + { + public abstract HashSet Matches { get; } + + public abstract string TagHelperPrefix { get; } + + public abstract void Visit(RazorSyntaxTree tree); + } + + internal class TagHelperDirectiveVisitor : DirectiveVisitor { private IReadOnlyList _tagHelpers; + private string _tagHelperPrefix; - public DirectiveVisitor(IReadOnlyList tagHelpers) + public TagHelperDirectiveVisitor(IReadOnlyList tagHelpers) { - _tagHelpers = tagHelpers; + // We don't want to consider components in a view document. + _tagHelpers = tagHelpers.Where(t => !ComponentMetadata.IsComponentTagHelperKind(t.Kind)).ToList(); } - public string TagHelperPrefix { get; private set; } + public override string TagHelperPrefix => _tagHelperPrefix; - public HashSet Matches { get; } = new HashSet(); + public override HashSet Matches { get; } = new HashSet(); - public override SyntaxNode VisitRazorDirective(RazorDirectiveSyntax node) + public override void Visit(RazorSyntaxTree tree) + { + Visit(tree.Root); + } + + public override void VisitRazorDirective(RazorDirectiveSyntax node) { var descendantLiterals = node.DescendantNodes(); foreach (var child in descendantLiterals) @@ -167,12 +199,10 @@ namespace Microsoft.AspNetCore.Razor.Language if (!string.IsNullOrEmpty(tagHelperPrefix.DirectiveText)) { // We only expect to see a single one of these per file, but that's enforced at another level. - TagHelperPrefix = tagHelperPrefix.DirectiveText; + _tagHelperPrefix = tagHelperPrefix.DirectiveText; } } } - - return base.VisitRazorDirective(node); } private bool AssemblyContainsTagHelpers(string assemblyName, IReadOnlyList tagHelpers) @@ -188,5 +218,231 @@ namespace Microsoft.AspNetCore.Razor.Language return false; } } + + internal class ComponentDirectiveVisitor : DirectiveVisitor + { + private IReadOnlyList _tagHelpers; + private string _filePath; + private RazorSourceDocument _source; + + public ComponentDirectiveVisitor(string filePath, IReadOnlyList tagHelpers, string currentNamespace) + { + _filePath = filePath; + + // We don't want to consider non-component tag helpers in a component document. + _tagHelpers = tagHelpers.Where(t => ComponentMetadata.IsComponentTagHelperKind(t.Kind) && !IsTagHelperFromMangledClass(t)).ToList(); + + for (var i = 0; i < _tagHelpers.Count; i++) + { + var tagHelper = _tagHelpers[i]; + if (tagHelper.IsComponentFullyQualifiedNameMatch()) + { + // If the component descriptor matches for a fully qualified name, using directives shouldn't matter. + Matches.Add(tagHelper); + continue; + } + + var typeName = tagHelper.GetTypeName(); + if (tagHelper.IsChildContentTagHelper()) + { + // If this is a child content tag helper, we want to add it if it's original type is in scope. + // E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in scope. + TrySplitNamespaceAndType(typeName, out typeName, out var _); + } + + if (currentNamespace != null && IsTypeInScope(typeName, currentNamespace)) + { + // Also, if the type is already in scope of the document's namespace, using isn't necessary. + Matches.Add(tagHelper); + } + } + } + + public override HashSet Matches { get; } = new HashSet(); + + // There is no support for tag helper prefix in component documents. + public override string TagHelperPrefix => null; + + public override void Visit(RazorSyntaxTree tree) + { + _source = tree.Source; + Visit(tree.Root); + } + + public override void VisitRazorDirective(RazorDirectiveSyntax node) + { + var descendantLiterals = node.DescendantNodes(); + foreach (var child in descendantLiterals) + { + if (!(child is CSharpStatementLiteralSyntax literal)) + { + continue; + } + + var context = literal.GetSpanContext(); + if (context == null) + { + // We can't find a chunk generator. + continue; + } + else if (context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelper) + { + // Make sure this node exists in the file we're parsing and not in its imports. + if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) + { + addTagHelper.Diagnostics.Add( + ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); + } + } + else if (context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelper) + { + // Make sure this node exists in the file we're parsing and not in its imports. + if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) + { + removeTagHelper.Diagnostics.Add( + ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); + } + } + else if (context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefix) + { + // Make sure this node exists in the file we're parsing and not in its imports. + if (_filePath.Equals(_source.FilePath, StringComparison.Ordinal)) + { + tagHelperPrefix.Diagnostics.Add( + ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective(node.GetSourceSpan(_source))); + } + } + else if (context.ChunkGenerator is AddImportChunkGenerator usingStatement && !usingStatement.IsStatic) + { + // Get the namespace from the using statement. + var @namespace = usingStatement.ParsedNamespace; + if (@namespace.Contains('=')) + { + // We don't support usings with alias. + continue; + } + + for (var i = 0; i < _tagHelpers.Count; i++) + { + var tagHelper = _tagHelpers[i]; + if (tagHelper.IsComponentFullyQualifiedNameMatch()) + { + // We've already added these to our list of matches. + continue; + } + + var typeName = tagHelper.GetTypeName(); + if (typeName != null && IsTypeInNamespace(typeName, @namespace)) + { + // If the type is at the top-level or if the type's namespace matches the using's namespace, add it. + Matches.Add(tagHelper); + } + } + } + } + } + + internal static bool IsTypeInNamespace(string typeName, string @namespace) + { + if (!TrySplitNamespaceAndType(typeName, out var typeNamespace, out var _) || typeNamespace == string.Empty) + { + // Either the typeName is not the full type name or this type is at the top level. + return true; + } + + return typeNamespace.Equals(@namespace, StringComparison.Ordinal); + } + + // Check if the given type is already in scope given the namespace of the current document. + // E.g, + // If the namespace of the document is `MyComponents.Components.Shared`, + // then the types `MyComponents.FooComponent`, `MyComponents.Components.BarComponent`, `MyComponents.Components.Shared.BazComponent` are all in scope. + // Whereas `MyComponents.SomethingElse.OtherComponent` is not in scope. + internal static bool IsTypeInScope(string typeName, string currentNamespace) + { + if (!TrySplitNamespaceAndType(typeName, out var typeNamespace, out var _) || typeNamespace == string.Empty) + { + // Either the typeName is not the full type name or this type is at the top level. + return true; + } + + var typeNamespaceSegments = typeNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries); + var currentNamespaceSegments = currentNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries); + if (typeNamespaceSegments.Length > currentNamespaceSegments.Length) + { + return false; + } + + for (var i = 0; i < typeNamespaceSegments.Length; i++) + { + if (!typeNamespaceSegments[i].Equals(currentNamespaceSegments[i], StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + // We need to filter out the duplicate tag helper descriptors that come from the + // open file in the editor. We mangle the class name for its generated code, so using that here to filter these out. + internal static bool IsTagHelperFromMangledClass(TagHelperDescriptor tagHelper) + { + if (!TrySplitNamespaceAndType(tagHelper.GetTypeName(), out var _, out var className)) + { + return false; + } + + return ComponentMetadata.IsMangledClass(className); + } + + // Internal for testing. + internal static bool TrySplitNamespaceAndType(string fullTypeName, out string @namespace, out string typeName) + { + @namespace = string.Empty; + typeName = string.Empty; + + if (string.IsNullOrEmpty(fullTypeName)) + { + return false; + } + + var nestingLevel = 0; + var splitLocation = -1; + for (var i = fullTypeName.Length - 1; i >= 0; i--) + { + var c = fullTypeName[i]; + if (c == Type.Delimiter && nestingLevel == 0) + { + splitLocation = i; + break; + } + else if (c == '>') + { + nestingLevel++; + } + else if (c == '<') + { + nestingLevel--; + } + } + + if (splitLocation == -1) + { + typeName = fullTypeName; + return true; + } + + @namespace = fullTypeName.Substring(0, splitLocation); + + var typeNameStartLocation = splitLocation + 1; + if (typeNameStartLocation < fullTypeName.Length) + { + typeName = fullTypeName.Substring(typeNameStartLocation, fullTypeName.Length - typeNameStartLocation); + } + + return true; + } + } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/AddImportChunkGenerator.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/AddImportChunkGenerator.cs index 4cee1abe3e..bb585eaa53 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/AddImportChunkGenerator.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/AddImportChunkGenerator.cs @@ -7,13 +7,19 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class AddImportChunkGenerator : SpanChunkGenerator { - public AddImportChunkGenerator(string ns) + public AddImportChunkGenerator(string usingContent, string parsedNamespace, bool isStatic) { - Namespace = ns; + Namespace = usingContent; + ParsedNamespace = parsedNamespace; + IsStatic = isStatic; } public string Namespace { get; } + public string ParsedNamespace { get; } + + public bool IsStatic { get; } + public override string ToString() { return "Import:" + Namespace + ";"; diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs index a6e048f189..6852e76534 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs @@ -2066,11 +2066,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var directiveBuilder = pooledResult.Builder; Assert(CSharpKeyword.Using); AcceptAndMoveNext(); + var isStatic = false; + var nonNamespaceTokenCount = TokenBuilder.Count; AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); var start = CurrentStart; if (At(SyntaxKind.Identifier)) { // non-static using + nonNamespaceTokenCount = TokenBuilder.Count; TryParseNamespaceOrTypeName(directiveBuilder); var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); if (At(SyntaxKind.Assign)) @@ -2094,15 +2097,24 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy else if (At(CSharpKeyword.Static)) { // static using + isStatic = true; AcceptAndMoveNext(); AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + nonNamespaceTokenCount = TokenBuilder.Count; TryParseNamespaceOrTypeName(directiveBuilder); } + var usingStatementTokens = TokenBuilder.ToList().Nodes; + var usingContentTokens = usingStatementTokens.Skip(1); + var parsedNamespaceTokens = usingStatementTokens + .Skip(nonNamespaceTokenCount) + .Where(s => s.Kind != SyntaxKind.CSharpComment && s.Kind != SyntaxKind.Whitespace && s.Kind != SyntaxKind.NewLine); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AnyExceptNewline; - SpanContext.ChunkGenerator = new AddImportChunkGenerator(new LocationTagged( - string.Concat(TokenBuilder.ToList().Nodes.Skip(1).Select(s => s.Content)), - start)); + SpanContext.ChunkGenerator = new AddImportChunkGenerator( + string.Concat(usingContentTokens.Select(s => s.Content)), + string.Concat(parsedNamespaceTokens.Select(s => s.Content)), + isStatic); // Optional ";" if (EnsureCurrent()) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorCodeDocumentExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorCodeDocumentExtensions.cs index 5d31bfcf73..e1cd262573 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorCodeDocumentExtensions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorCodeDocumentExtensions.cs @@ -3,12 +3,17 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language { public static class RazorCodeDocumentExtensions { + private static readonly char[] PathSeparators = new char[] { '/', '\\' }; + private static readonly char[] NamespaceSeparators = new char[] { '.' }; + public static TagHelperDocumentContext GetTagHelperContext(this RazorCodeDocument document) { if (document == null) @@ -189,6 +194,71 @@ namespace Microsoft.AspNetCore.Razor.Language document.Items[typeof(FileKinds)] = fileKind; } + // In general documents will have a relative path (relative to the project root). + // We can only really compute a nice class/namespace when we know a relative path. + // + // However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't + // set up correctly. + internal static bool TryComputeNamespaceAndClass(this RazorCodeDocument document, out string @namespace, out string @class) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + var filePath = document.Source.FilePath; + var relativePath = document.Source.RelativePath; + if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length) + { + @namespace = null; + @class = null; + return false; + } + + filePath = NormalizePath(filePath); + relativePath = NormalizePath(relativePath); + var options = document.GetCodeGenerationOptions() ?? document.GetDocumentIntermediateNode()?.Options; + var rootNamespace = options?.RootNamespace; + if (string.IsNullOrEmpty(rootNamespace)) + { + @namespace = null; + @class = null; + return false; + } + + var builder = new StringBuilder(); + + // Sanitize the base namespace, but leave the dots. + var segments = rootNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[0])); + for (var i = 1; i < segments.Length; i++) + { + builder.Append('.'); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); + } + + segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries); + + // Skip the last segment because it's the FileName. + for (var i = 0; i < segments.Length - 1; i++) + { + builder.Append('.'); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); + } + + @namespace = builder.ToString(); + @class = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(relativePath)); + + return true; + } + + private static string NormalizePath(string path) + { + path = path.Replace('\\', '/'); + + return path; + } + private class ImportSyntaxTreesHolder { public ImportSyntaxTreesHolder(IReadOnlyList syntaxTrees) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorEngine.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorEngine.cs index 4ed9ae75e6..cb8a10389a 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorEngine.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorEngine.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Razor.Language private static void AddDefaultRuntimeFeatures(RazorConfiguration configuration, ICollection features) { // Configure options - features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: configuration.LanguageVersion)); + features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: configuration.LanguageVersion, fileKind: null)); features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false)); // Intermediate Node Passes @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Razor.Language private static void AddDefaultDesignTimeFeatures(RazorConfiguration configuration, ICollection features) { // Configure options - features.Add(new DefaultRazorParserOptionsFeature(designTime: true, version: configuration.LanguageVersion)); + features.Add(new DefaultRazorParserOptionsFeature(designTime: true, version: configuration.LanguageVersion, fileKind: null)); features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: true)); features.Add(new SuppressChecksumOptionsFeature()); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorParserOptions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorParserOptions.cs index 3438cdfbe3..cb0781b666 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorParserOptions.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorParserOptions.cs @@ -14,17 +14,23 @@ namespace Microsoft.AspNetCore.Razor.Language Array.Empty(), designTime: false, parseLeadingDirectives: false, - version: RazorLanguageVersion.Latest); + version: RazorLanguageVersion.Latest, + fileKind: null); } public static RazorParserOptions Create(Action configure) + { + return Create(configure, fileKind: null); + } + + public static RazorParserOptions Create(Action configure, string fileKind) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } - var builder = new DefaultRazorParserOptionsBuilder(designTime: false, version: RazorLanguageVersion.Latest); + var builder = new DefaultRazorParserOptionsBuilder(designTime: false, version: RazorLanguageVersion.Latest, fileKind); configure(builder); var options = builder.Build(); @@ -32,13 +38,18 @@ namespace Microsoft.AspNetCore.Razor.Language } public static RazorParserOptions CreateDesignTime(Action configure) + { + return CreateDesignTime(configure, fileKind: null); + } + + public static RazorParserOptions CreateDesignTime(Action configure, string fileKind) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } - var builder = new DefaultRazorParserOptionsBuilder(designTime: true, version: RazorLanguageVersion.Latest); + var builder = new DefaultRazorParserOptionsBuilder(designTime: true, version: RazorLanguageVersion.Latest, fileKind); configure(builder); var options = builder.Build(); @@ -61,6 +72,8 @@ namespace Microsoft.AspNetCore.Razor.Language public virtual RazorLanguageVersion Version { get; } = RazorLanguageVersion.Latest; + internal virtual string FileKind { get; } + internal virtual RazorParserFeatureFlags FeatureFlags { get; } } } diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs index 39afa6b0ae..f2269e692e 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorProjectEngine.cs @@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Razor.Language // Legacy options features // // These features are obsolete as of 2.1. Our code will resolve this but not invoke them. - features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorLanguageVersion.Version_2_0)); + features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: RazorLanguageVersion.Version_2_0, fileKind: null)); features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false)); // Syntax Tree passes diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs index e3c28d488b..cd1b335103 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/src/BindTagHelperDescriptorProvider.cs @@ -445,6 +445,11 @@ namespace Microsoft.CodeAnalysis.Razor attribute.SetPropertyName(valueAttribute.GetPropertyName()); }); + if (tagHelper.IsComponentFullyQualifiedNameMatch()) + { + builder.Metadata[ComponentMetadata.Component.NameMatchKey] = ComponentMetadata.Component.FullyQualifiedNameMatch; + } + results.Add(builder.Build()); } } diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs index f6f9bca9ab..57b64c6abc 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/src/ComponentTagHelperDescriptorProvider.cs @@ -63,13 +63,20 @@ namespace Microsoft.CodeAnalysis.Razor for (var i = 0; i < types.Count; i++) { var type = types[i]; - var descriptor = CreateDescriptor(symbols, type); - context.Results.Add(descriptor); - foreach (var childContent in descriptor.GetChildContentProperties()) + // Components have very simple matching rules. + // 1. The type name (short) matches the tag name. + // 2. The fully qualified name matches the tag name. + var shortNameMatchingDescriptor = CreateShortNameMatchingDescriptor(symbols, type); + context.Results.Add(shortNameMatchingDescriptor); + var fullyQualifiedNameMatchingDescriptor = CreateFullyQualifiedNameMatchingDescriptor(symbols, type); + context.Results.Add(fullyQualifiedNameMatchingDescriptor); + + foreach (var childContent in shortNameMatchingDescriptor.GetChildContentProperties()) { // Synthesize a separate tag helper for each child content property that's declared. - context.Results.Add(CreateChildContentDescriptor(symbols, descriptor, childContent)); + context.Results.Add(CreateChildContentDescriptor(symbols, shortNameMatchingDescriptor, childContent)); + context.Results.Add(CreateChildContentDescriptor(symbols, fullyQualifiedNameMatchingDescriptor, childContent)); } } } @@ -81,7 +88,26 @@ namespace Microsoft.CodeAnalysis.Razor return compilation.WithOptions(newCompilationOptions); } - private TagHelperDescriptor CreateDescriptor(ComponentSymbols symbols, INamedTypeSymbol type) + private TagHelperDescriptor CreateShortNameMatchingDescriptor(ComponentSymbols symbols, INamedTypeSymbol type) + { + var builder = CreateDescriptorBuilder(symbols, type); + builder.TagMatchingRule(r => r.TagName = type.Name); + + return builder.Build(); + } + + private TagHelperDescriptor CreateFullyQualifiedNameMatchingDescriptor(ComponentSymbols symbols, INamedTypeSymbol type) + { + var builder = CreateDescriptorBuilder(symbols, type); + var containingNamespace = type.ContainingNamespace.ToDisplayString(); + var fullName = $"{containingNamespace}.{type.Name}"; + builder.TagMatchingRule(r => r.TagName = fullName); + builder.Metadata[ComponentMetadata.Component.NameMatchKey] = ComponentMetadata.Component.FullyQualifiedNameMatch; + + return builder.Build(); + } + + private TagHelperDescriptorBuilder CreateDescriptorBuilder(ComponentSymbols symbols, INamedTypeSymbol type) { var typeName = type.ToDisplayString(FullNameTypeDisplayFormat); var assemblyName = type.ContainingAssembly.Identity.Name; @@ -113,9 +139,6 @@ namespace Microsoft.CodeAnalysis.Razor builder.Documentation = xml; } - // Components have very simple matching rules. The type name (short) matches the tag name. - builder.TagMatchingRule(r => r.TagName = type.Name); - foreach (var property in GetProperties(symbols, type)) { if (property.kind == PropertyKind.Ignored) @@ -135,8 +158,7 @@ namespace Microsoft.CodeAnalysis.Razor CreateContextParameter(builder, childContentName: null); } - var descriptor = builder.Build(); - return descriptor; + return builder; } private void CreateProperty(TagHelperDescriptorBuilder builder, IPropertySymbol property, PropertyKind kind) @@ -270,6 +292,11 @@ namespace Microsoft.CodeAnalysis.Razor CreateContextParameter(builder, attribute.Name); } + if (component.IsComponentFullyQualifiedNameMatch()) + { + builder.Metadata[ComponentMetadata.Component.NameMatchKey] = ComponentMetadata.Component.FullyQualifiedNameMatch; + } + var descriptor = builder.Build(); return descriptor; diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs index 5896f2d333..d8fd1bcbeb 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs @@ -1,18 +1,18 @@ // 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.Language; -using Microsoft.CodeAnalysis; +using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Extensions.DependencyModel; +using Xunit; namespace Microsoft.CodeAnalysis.Razor { - public abstract class BaseTagHelperDescriptorProviderTest + public abstract class TagHelperDescriptorProviderTestBase { - static BaseTagHelperDescriptorProviderTest() + static TagHelperDescriptorProviderTestBase() { BaseCompilation = TestCompilation.Create(typeof(ComponentTagHelperDescriptorProviderTest).Assembly); CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3); @@ -39,5 +39,28 @@ namespace Microsoft.CodeAnalysis.Razor .ToArray(); } + protected static TagHelperDescriptor[] AssertAndExcludeFullyQualifiedNameMatchComponents( + TagHelperDescriptor[] components, + int expectedCount) + { + var componentLookup = new Dictionary>(); + var fullyQualifiedNameMatchComponents = components.Where(c => c.IsComponentFullyQualifiedNameMatch()).ToArray(); + Assert.Equal(expectedCount, fullyQualifiedNameMatchComponents.Length); + + var shortNameMatchComponents = components.Where(c => !c.IsComponentFullyQualifiedNameMatch()).ToArray(); + + // For every fully qualified name component, we want to make sure we have a corresponding short name component. + foreach (var fullNameComponent in fullyQualifiedNameMatchComponents) + { + Assert.Contains(shortNameMatchComponents, component => + { + return component.Name == fullNameComponent.Name && + component.Kind == fullNameComponent.Kind && + component.BoundAttributes.SequenceEqual(fullNameComponent.BoundAttributes, BoundAttributeDescriptorComparer.Default); + }); + } + + return shortNameMatchComponents; + } } } diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs index 8864268033..bb8025c2bc 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/BindTagHelperDescriptorProviderTest.cs @@ -8,7 +8,7 @@ using Xunit; namespace Microsoft.CodeAnalysis.Razor { - public class BindTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest + public class BindTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase { [Fact] public void Execute_FindsBindTagHelperOnComponentType_Delegate_CreatesDescriptor() @@ -55,6 +55,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 1); var bind = Assert.Single(matches); // These are features Bind Tags Helpers don't use. Verifying them once here and @@ -170,6 +171,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 1); var bind = Assert.Single(matches); // These are features Bind Tags Helpers don't use. Verifying them once here and @@ -283,6 +285,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); Assert.Empty(matches); } @@ -314,6 +317,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); // These are features Bind Tags Helpers don't use. Verifying them once here and @@ -448,6 +452,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); Assert.Equal("myprop", bind.Metadata[ComponentMetadata.Bind.ValueAttribute]); @@ -502,6 +507,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); Assert.Equal("myprop", bind.Metadata[ComponentMetadata.Bind.ValueAttribute]); @@ -557,6 +563,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); Assert.Equal("myprop", bind.Metadata[ComponentMetadata.Bind.ValueAttribute]); @@ -624,6 +631,7 @@ namespace Test // Assert var matches = GetBindTagHelpers(context); + matches = AssertAndExcludeFullyQualifiedNameMatchComponents(matches, expectedCount: 0); var bind = Assert.Single(matches); Assert.Equal("myprop", bind.Metadata[ComponentMetadata.Bind.ValueAttribute]); diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs index 8592486139..ecb2ec6939 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs @@ -8,7 +8,7 @@ using Xunit; namespace Microsoft.CodeAnalysis.Razor { - public class ComponentTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest + public class ComponentTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase { [Fact] public void Execute_FindsIComponentType_CreatesDescriptor() @@ -45,6 +45,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); // These are features Components don't use. Verifying them once here and @@ -159,6 +160,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -222,6 +224,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -263,6 +266,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -315,6 +319,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -361,6 +366,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -424,6 +430,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -498,6 +505,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -547,6 +555,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -608,6 +617,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -657,6 +667,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -711,6 +722,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); @@ -773,6 +785,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -829,6 +842,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -903,6 +917,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -974,6 +989,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -1055,6 +1071,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -1136,6 +1153,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -1221,6 +1239,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 2); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -1306,6 +1325,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 4); var component = Assert.Single(components, c => c.IsComponentTagHelper()); Assert.Equal("TestAssembly", component.AssemblyName); @@ -1397,6 +1417,7 @@ namespace Test // Assert var components = ExcludeBuiltInComponents(context); + components = AssertAndExcludeFullyQualifiedNameMatchComponents(components, expectedCount: 1); var component = Assert.Single(components); Assert.Equal("TestAssembly", component.AssemblyName); diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs index 86d045195b..19e2e6555d 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/EventHandlerTagHelperDescriptorProviderTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace Microsoft.CodeAnalysis.Razor { - public class EventHandlerTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest + public class EventHandlerTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase { [Fact] public void Execute_EventHandler_CreatesDescriptor() diff --git a/src/Razor/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs b/src/Razor/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs index 349f0f5cee..fa091512b5 100644 --- a/src/Razor/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs +++ b/src/Razor/Microsoft.CodeAnalysis.Razor/test/RefTagHelperDescriptorProviderTest.cs @@ -8,7 +8,7 @@ using Xunit; namespace Microsoft.CodeAnalysis.Razor { - public class RefTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest + public class RefTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase { [Fact] public void Execute_CreatesDescriptor()