Better support for _Imports.razor (dotnet/aspnetcore-tooling#357)

* Better support for _Imports.razor

* Special case component imports when generating code

* Prevent a future VS crash

* Rebased and updated

* update

* Removed unnecessary newline
\n\nCommit migrated from abbfe00bdc
This commit is contained in:
Ajay Bhargav Baaskaran 2019-03-26 17:58:37 -07:00 committed by GitHub
parent 784596aba6
commit 730f3cdc6b
21 changed files with 220 additions and 79 deletions

View File

@ -223,7 +223,7 @@ namespace Microsoft.AspNetCore.Razor.Language {
}
/// <summary>
/// Looks up a localized string similar to The &apos;@{0}&apos; directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.
/// Looks up a localized string similar to The &apos;@{0}&apos; directive specified in {1} file will not be imported. The directive must appear at the top of each Razor file.
/// </summary>
internal static string PageDirectiveCannotBeImported {
get {

View File

@ -163,7 +163,7 @@
<value>TypeName</value>
</data>
<data name="PageDirectiveCannotBeImported" xml:space="preserve">
<value>The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file</value>
<value>The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor file</value>
</data>
<data name="PageDirective_Description" xml:space="preserve">
<value>Mark the page as a routable component.</value>

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
public static readonly RazorDiagnosticDescriptor UnsupportedTagHelperDirective = new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}9978",
() =>
"The directives @addTagHelper, @removeTagHelper and @tagHelperPrefix are not valid in a component document." +
"The directives @addTagHelper, @removeTagHelper and @tagHelperPrefix are not valid in a component document. " +
"Use '@using <namespace>' directive instead.",
RazorDiagnosticSeverity.Error);
@ -321,5 +321,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
return RazorDiagnostic.Create(ChildContentHasInvalidParameterOnComponent, source ?? SourceSpan.Undefined, attribute, element);
}
public static readonly RazorDiagnosticDescriptor UnsupportedComponentImportContent =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}10003",
() => "Markup, code and block directives are not valid in component imports.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnsupportedComponentImportContent(SourceSpan? source)
{
return RazorDiagnostic.Create(UnsupportedComponentImportContent, source ?? SourceSpan.Undefined);
}
}
}

View File

@ -70,47 +70,63 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
}
@namespace.Content = computedNamespace;
@class.BaseType = ComponentsApi.ComponentBase.FullTypeName;
@class.ClassName = computedClass;
@class.Modifiers.Clear();
@class.Modifiers.Add("public");
var documentNode = codeDocument.GetDocumentIntermediateNode();
var typeParamReferences = documentNode.FindDirectiveReferences(ComponentTypeParamDirective.Directive);
for (var i = 0; i < typeParamReferences.Count; i++)
if (FileKinds.IsComponentImport(codeDocument.GetFileKind()))
{
var typeParamNode = (DirectiveIntermediateNode)typeParamReferences[i].Node;
if (typeParamNode.HasDiagnostics)
// We don't want component imports to be considered as real component.
// But we still want to generate code for it so we can get diagnostics.
@class.BaseType = typeof(object).FullName;
method.ReturnType = "void";
method.MethodName = "Execute";
method.Modifiers.Clear();
method.Modifiers.Add("protected");
method.Parameters.Clear();
}
else
{
@class.BaseType = ComponentsApi.ComponentBase.FullTypeName;
var documentNode = codeDocument.GetDocumentIntermediateNode();
var typeParamReferences = documentNode.FindDirectiveReferences(ComponentTypeParamDirective.Directive);
for (var i = 0; i < typeParamReferences.Count; i++)
{
continue;
var typeParamNode = (DirectiveIntermediateNode)typeParamReferences[i].Node;
if (typeParamNode.HasDiagnostics)
{
continue;
}
@class.TypeParameters.Add(new TypeParameter() { ParameterName = typeParamNode.Tokens.First().Content, });
}
@class.TypeParameters.Add(new TypeParameter() { ParameterName = typeParamNode.Tokens.First().Content, });
method.ReturnType = "void";
method.MethodName = ComponentsApi.ComponentBase.BuildRenderTree;
method.Modifiers.Clear();
method.Modifiers.Add("protected");
method.Modifiers.Add("override");
method.Parameters.Clear();
method.Parameters.Add(new MethodParameter()
{
ParameterName = "builder",
TypeName = ComponentsApi.RenderTreeBuilder.FullTypeName,
});
// We need to call the 'base' method as the first statement.
var callBase = new CSharpCodeIntermediateNode();
callBase.Annotations.Add(BuildRenderTreeBaseCallAnnotation, true);
callBase.Children.Add(new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $"base.{ComponentsApi.ComponentBase.BuildRenderTree}(builder);"
});
method.Children.Insert(0, callBase);
}
method.ReturnType = "void";
method.MethodName = ComponentsApi.ComponentBase.BuildRenderTree;
method.Modifiers.Clear();
method.Modifiers.Add("protected");
method.Modifiers.Add("override");
method.Parameters.Clear();
method.Parameters.Add(new MethodParameter()
{
ParameterName = "builder",
TypeName = ComponentsApi.RenderTreeBuilder.FullTypeName,
});
// We need to call the 'base' method as the first statement.
var callBase = new CSharpCodeIntermediateNode();
callBase.Annotations.Add(BuildRenderTreeBaseCallAnnotation, true);
callBase.Children.Add(new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $"base.{ComponentsApi.ComponentBase.BuildRenderTree}(builder);"
});
method.Children.Insert(0, callBase);
}
internal static bool IsBuildRenderTreeBaseCall(CSharpCodeIntermediateNode node)

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(FileKinds.Component, Directive);
builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
builder.Features.Add(new ComponentInjectDirectivePass());
}
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(FileKinds.Component, Directive);
builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
builder.Features.Add(new ComponentLayoutDirectivePass());
}
}

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
attributeNode.Children.Add(new IntermediateToken()
{
Kind = TokenKind.CSharp,
Content = $"[{ComponentsApi.LayoutAttribute.FullTypeName}(typeof({token.Content}))]" + Environment.NewLine,
Content = $"[{ComponentsApi.LayoutAttribute.FullTypeName}(typeof({token.Content}))]",
});
// Insert the new attribute on top of the class

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(FileKinds.Component, Directive);
builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
builder.Features.Add(new ComponentPageDirectivePass());
return builder;
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
for (var i = 0; i < directives.Count; i++)
{
var directive = directives[i];
if (directive.Node.IsImported())
if (FileKinds.IsComponentImport(codeDocument.GetFileKind()) || directive.Node.IsImported())
{
directive.Node.Diagnostics.Add(ComponentDiagnosticFactory.CreatePageDirective_CannotBeImported(directive.Node.Source.Value));
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(FileKinds.Component, Directive);
builder.AddDirective(Directive, FileKinds.Component, FileKinds.ComponentImport);
return builder;
}
}

View File

@ -9,7 +9,21 @@ namespace Microsoft.AspNetCore.Razor.Language
{
internal class DefaultRazorDirectiveFeature : RazorEngineFeatureBase, IRazorDirectiveFeature, IConfigureRazorParserOptionsFeature
{
public ICollection<DirectiveDescriptor> Directives { get; } = new List<DirectiveDescriptor>();
// To maintain backwards compatibility, adding to this list will default to legacy file kind.
public ICollection<DirectiveDescriptor> Directives
{
get
{
ICollection<DirectiveDescriptor> result;
if (!DirectivesByFileKind.TryGetValue(FileKinds.Legacy, out result))
{
result = new List<DirectiveDescriptor>();
DirectivesByFileKind.Add(FileKinds.Legacy, result);
}
return result;
}
}
public IDictionary<string, ICollection<DirectiveDescriptor>> DirectivesByFileKind { get; } = new Dictionary<string, ICollection<DirectiveDescriptor>>(StringComparer.OrdinalIgnoreCase);
@ -24,22 +38,11 @@ namespace Microsoft.AspNetCore.Razor.Language
options.Directives.Clear();
foreach (var directive in Directives)
{
options.Directives.Add(directive);
}
if (options.FileKind != null && DirectivesByFileKind.TryGetValue(options.FileKind, out var directives))
var fileKind = options.FileKind ?? FileKinds.Legacy;
if (DirectivesByFileKind.TryGetValue(fileKind, out var directives))
{
foreach (var directive in directives)
{
// Replace any non-file-kind-specific directives
var replaces = options.Directives.Where(d => string.Equals(d.Directive, directive.Directive, StringComparison.Ordinal)).ToArray();
foreach (var replace in replaces)
{
options.Directives.Remove(replace);
}
options.Directives.Add(directive);
}
}

View File

@ -45,7 +45,17 @@ namespace Microsoft.AspNetCore.Razor.Language
// We need to decide up front if this document is a "component" file. This will affect how
// lowering behaves.
LoweringVisitor visitor;
if (FileKinds.IsComponent(codeDocument.GetFileKind()) &&
if (FileKinds.IsComponentImport(codeDocument.GetFileKind()) &&
syntaxTree.Options.FeatureFlags.AllowComponentFileKind)
{
visitor = new ComponentImportFileKindVisitor(document, builder, syntaxTree.Options.FeatureFlags)
{
SourceDocument = syntaxTree.Source,
};
visitor.Visit(syntaxTree.Root);
}
else if (FileKinds.IsComponent(codeDocument.GetFileKind()) &&
syntaxTree.Options.FeatureFlags.AllowComponentFileKind)
{
visitor = new ComponentFileKindVisitor(document, builder, syntaxTree.Options.FeatureFlags)
@ -1856,6 +1866,58 @@ namespace Microsoft.AspNetCore.Razor.Language
}
}
private class ComponentImportFileKindVisitor : LoweringVisitor
{
public ComponentImportFileKindVisitor(
DocumentIntermediateNode document,
IntermediateNodeBuilder builder,
RazorParserFeatureFlags featureFlags)
: base(document, builder, featureFlags)
{
}
public override void DefaultVisit(SyntaxNode node)
{
base.DefaultVisit(node);
}
public override void VisitMarkupElement(MarkupElementSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
public override void VisitCSharpStatement(CSharpStatementSyntax node)
{
_document.Diagnostics.Add(
ComponentDiagnosticFactory.Create_UnsupportedComponentImportContent(BuildSourceSpanFromNode(node)));
}
}
private class ImportsVisitor : LoweringVisitor
{
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserFeatureFlags featureFlags)

View File

@ -37,7 +37,9 @@ namespace Microsoft.AspNetCore.Razor.Language
// The imports come logically before the main razor file and are in the order they
// should be processed.
DirectiveVisitor visitor = null;
if (FileKinds.IsComponent(codeDocument.GetFileKind()))
var parserOptions = codeDocument.GetParserOptions();
if (FileKinds.IsComponent(codeDocument.GetFileKind()) &&
(parserOptions == null || parserOptions.FeatureFlags.AllowComponentFileKind))
{
codeDocument.TryComputeNamespaceAndClass(out var currentNamespace, out var _);
visitor = new ComponentDirectiveVisitor(codeDocument.Source.FilePath, descriptors, currentNamespace);

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.AddDirective(Directive, FileKinds.Legacy, FileKinds.Component);
builder.Features.Add(new FunctionsDirectivePass());
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.AddDirective(Directive, FileKinds.Legacy, FileKinds.Component, FileKinds.ComponentImport);
builder.Features.Add(new ImplementsDirectivePass());
}
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.AddDirective(Directive, FileKinds.Legacy, FileKinds.Component, FileKinds.ComponentImport);
builder.Features.Add(new InheritsDirectivePass());
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.AddDirective(Directive, FileKinds.Legacy, FileKinds.Component);
builder.Features.Add(new SectionDirectivePass());
builder.AddTargetExtension(new SectionTargetExtension());
}

View File

@ -27,6 +27,23 @@ namespace Microsoft.AspNetCore.Razor.Language
return string.Equals(fileKind, FileKinds.ComponentImport, StringComparison.OrdinalIgnoreCase);
}
public static string GetComponentFileKindFromFilePath(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
if (string.Equals(ComponentMetadata.ImportsFileName, Path.GetFileName(filePath), StringComparison.Ordinal))
{
return FileKinds.ComponentImport;
}
else
{
return FileKinds.Component;
}
}
public static string GetFileKindFromFilePath(string filePath)
{
if (filePath == null)

View File

@ -166,35 +166,38 @@ namespace Microsoft.AspNetCore.Razor.Language
/// Adds the specified <see cref="DirectiveDescriptor"/> for the provided file kind.
/// </summary>
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
/// <param name="fileKind">The file kind, for which to register the directive. See <see cref="FileKinds"/>.</param>
/// <param name="directive">The <see cref="DirectiveDescriptor"/> to add.</param>
/// <param name="fileKinds">The file kinds, for which to register the directive. See <see cref="FileKinds"/>.</param>
/// <returns>The <see cref="RazorProjectEngineBuilder"/>.</returns>
public static RazorProjectEngineBuilder AddDirective(this RazorProjectEngineBuilder builder, string fileKind, DirectiveDescriptor directive)
public static RazorProjectEngineBuilder AddDirective(this RazorProjectEngineBuilder builder, DirectiveDescriptor directive, params string[] fileKinds)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (fileKind == null)
{
throw new ArgumentNullException(nameof(fileKind));
}
if (directive == null)
{
throw new ArgumentNullException(nameof(directive));
}
var directiveFeature = GetDirectiveFeature(builder);
if (!directiveFeature.DirectivesByFileKind.TryGetValue(fileKind, out var directives))
if (fileKinds == null)
{
directives = new List<DirectiveDescriptor>();
directiveFeature.DirectivesByFileKind.Add(fileKind, directives);
throw new ArgumentNullException(nameof(fileKinds));
}
directives.Add(directive);
var directiveFeature = GetDirectiveFeature(builder);
foreach (var fileKind in fileKinds)
{
if (!directiveFeature.DirectivesByFileKind.TryGetValue(fileKind, out var directives))
{
directives = new List<DirectiveDescriptor>();
directiveFeature.DirectivesByFileKind.Add(fileKind, directives);
}
directives.Add(directive);
}
return builder;
}

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.Extensions.CommandLineUtils;
@ -286,6 +287,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
{
var outputPath = Path.Combine(projectDirectory, outputs[i]);
var fileKind = fileKinds.Count > 0 ? fileKinds[i] : "mvc";
if (Language.FileKinds.IsComponent(fileKind))
{
fileKind = Language.FileKinds.GetComponentFileKindFromFilePath(sources[i]);
}
items[i] = new SourceItem(sources[i], outputs[i], relativePath[i], fileKind);
}

View File

@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{
AdditionalSyntaxTrees = new List<SyntaxTree>();
AdditionalRazorItems = new List<RazorProjectItem>();
ImportItems = new List<RazorProjectItem>();
BaseCompilation = DefaultBaseCompilation;
Configuration = RazorConfiguration.Default;
@ -72,6 +73,8 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
internal List<RazorProjectItem> AdditionalRazorItems { get; }
internal List<RazorProjectItem> ImportItems { get; }
internal List<SyntaxTree> AdditionalSyntaxTrees { get; }
internal virtual CSharpCompilation BaseCompilation { get; }
@ -118,6 +121,8 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
// Turn off checksums, we're testing code generation.
b.Features.Add(new SuppressChecksum());
b.Features.Add(new TestImportProjectFeature(ImportItems));
if (LineEnding != null)
{
b.Phases.Insert(0, new ForceLineEndingPhase(LineEnding));
@ -135,7 +140,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
});
}
internal RazorProjectItem CreateProjectItem(string cshtmlRelativePath, string cshtmlContent)
internal RazorProjectItem CreateProjectItem(string cshtmlRelativePath, string cshtmlContent, string fileKind = null)
{
var fullPath = WorkingDirectory + PathSeparator + cshtmlRelativePath;
@ -156,7 +161,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
physicalPath: fullPath,
relativePhysicalPath: cshtmlRelativePath,
basePath: WorkingDirectory,
fileKind: FileKind)
fileKind: fileKind ?? FileKind)
{
Content = cshtmlContent.TrimStart(),
};
@ -167,7 +172,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
return CompileToCSharp(DefaultFileName, cshtmlContent, throwOnFailure);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent, bool throwOnFailure = true)
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent, bool throwOnFailure = true, string fileKind = null)
{
if (DeclarationOnly && DesignTime)
{
@ -197,7 +202,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
}
// Result of generating declarations
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent);
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent, fileKind);
codeDocument = projectEngine.ProcessDeclarationOnly(projectItem);
var declaration = new CompileToCSharpResult
{
@ -243,7 +248,7 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
// This will include the built-in components.
var projectEngine = CreateProjectEngine(Configuration, BaseCompilation.References.ToArray());
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent);
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent, fileKind);
RazorCodeDocument codeDocument;
if (DeclarationOnly)
@ -456,5 +461,22 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
codeDocument.Items[key] = LineEnding;
}
}
private class TestImportProjectFeature : IImportProjectFeature
{
private List<RazorProjectItem> _imports;
public TestImportProjectFeature(List<RazorProjectItem> imports)
{
_imports = imports;
}
public RazorProjectEngine ProjectEngine { get; set; }
public IReadOnlyList<RazorProjectItem> GetImports(RazorProjectItem projectItem)
{
return _imports;
}
}
}
}