Make single line single file scoped directives automatically import.

- Added an inner pass inside of the intermediate lowering phase to determine which directives get flowed to the final document. There were many ways to accomplish this but in order to keep the last wins mechanic for non-auto imported directives I had to let the directives get created and then removed based on if they were inherited.
- Added error case if a user attempts to import a block directive with a `FileScopedSinglyOccurring` directive usage.
- Added test cases that validate directives are properly inherited at the intermediate lowering phase.
- Updated a few tests that had incorrect assumptions.
- Left the default directive passes alone in regards to determining the "imported" directive to enable users to add their own model, inherits, etc. directives that take precedence.
- Normalized the passes in the intermediate lowering phase to handle directives identically (we don't conditionally lower anymore).

#1376
This commit is contained in:
N. Taylor Mullen 2017-06-23 00:50:18 -07:00
parent 2d90ae47f9
commit 09ac126ecf
7 changed files with 399 additions and 115 deletions

View File

@ -73,6 +73,8 @@ namespace Microsoft.AspNetCore.Razor.Language
builder.Insert(i++, @using);
}
ImportDirectives(document);
// The document should contain all errors that currently exist in the system. This involves
// adding the errors from the primary and imported syntax trees.
@ -96,6 +98,56 @@ namespace Microsoft.AspNetCore.Razor.Language
codeDocument.SetDocumentIntermediateNode(document);
}
private void ImportDirectives(DocumentIntermediateNode document)
{
var visitor = new DirectiveVisitor();
visitor.VisitDocument(document);
var seenDirectives = new HashSet<DirectiveDescriptor>();
for (var i = visitor.Directives.Count - 1; i >= 0; i--)
{
var reference = visitor.Directives[i];
var directive = (DirectiveIntermediateNode)reference.Node;
var descriptor = directive.Descriptor;
var seenDirective = !seenDirectives.Add(descriptor);
var imported = ReferenceEquals(directive.Annotations[CommonAnnotations.Imported], CommonAnnotations.Imported);
if (!imported)
{
continue;
}
switch (descriptor.Kind)
{
case DirectiveKind.SingleLine:
if (seenDirective && descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
{
// This directive has been overridden, it should be removed from the document.
break;
}
continue;
case DirectiveKind.RazorBlock:
case DirectiveKind.CodeBlock:
if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
{
// A block directive cannot be imported.
document.Diagnostics.Add(
RazorDiagnosticFactory.CreateDirective_BlockDirectiveCannotBeImported(descriptor.Directive));
}
break;
default:
throw new InvalidOperationException(Resources.FormatUnexpectedDirectiveKind(typeof(DirectiveKind).FullName));
}
// Overridden and invalid imported directives make it to here. They should be removed from the document.
reference.Remove();
}
}
private RazorCodeGenerationOptions CreateCodeGenerationOptions()
{
var builder = new DefaultRazorCodeGenerationOptionsBuilder();
@ -122,6 +174,50 @@ namespace Microsoft.AspNetCore.Razor.Language
public string FilePath { get; set; }
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = span.Content,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(span),
});
}
public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
VisitDefault(block);
_builder.Pop();
}
public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span)
{
var namespaceImport = chunkGenerator.Namespace.Trim();
@ -264,73 +360,6 @@ namespace Microsoft.AspNetCore.Razor.Language
}
}
private class ImportsVisitor : LoweringVisitor
{
// Imports only supports usings and single-line directives. We only want to include directive tokens
// when we're inside a single line directive. Also single line directives can't nest which makes
// this simple.
private bool _insideLineDirective;
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces)
: base(document, builder, namespaces)
{
}
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
if (_insideLineDirective)
{
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = span.Content,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(span),
});
}
}
public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
if (chunkGenerator.Descriptor.Kind == DirectiveKind.SingleLine)
{
_insideLineDirective = true;
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
base.VisitDirectiveBlock(chunkGenerator, block);
_builder.Pop();
_insideLineDirective = false;
}
}
}
private class MainSourceVisitor : LoweringVisitor
{
private DeclareTagHelperFieldsIntermediateNode _tagHelperFields;
@ -342,50 +371,6 @@ namespace Microsoft.AspNetCore.Razor.Language
_tagHelperPrefix = tagHelperPrefix;
}
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
_builder.Add(new DirectiveTokenIntermediateNode()
{
Content = span.Content,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(span),
});
}
public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
IntermediateNode directiveNode;
if (IsMalformed(chunkGenerator.Diagnostics))
{
directiveNode = new MalformedDirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
else
{
directiveNode = new DirectiveIntermediateNode()
{
Name = chunkGenerator.Descriptor.Directive,
Descriptor = chunkGenerator.Descriptor,
Source = BuildSourceSpanFromNode(block),
};
}
for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++)
{
directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]);
}
_builder.Push(directiveNode);
VisitDefault(block);
_builder.Pop();
}
// Example
// <input` checked="hello-world @false"`/>
// Name=checked
@ -783,6 +768,60 @@ namespace Microsoft.AspNetCore.Razor.Language
}
}
private class ImportsVisitor : LoweringVisitor
{
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces)
: base(document, new ImportBuilder(builder), namespaces)
{
}
private class ImportBuilder : IntermediateNodeBuilder
{
private readonly IntermediateNodeBuilder _innerBuilder;
public ImportBuilder(IntermediateNodeBuilder innerBuilder)
{
_innerBuilder = innerBuilder;
}
public override IntermediateNode Current => _innerBuilder.Current;
public override void Add(IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Add(node);
}
public override IntermediateNode Build() => _innerBuilder.Build();
public override void Insert(int index, IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Insert(index, node);
}
public override IntermediateNode Pop() => _innerBuilder.Pop();
public override void Push(IntermediateNode node)
{
node.Annotations[CommonAnnotations.Imported] = CommonAnnotations.Imported;
_innerBuilder.Push(node);
}
}
}
private class DirectiveVisitor : IntermediateNodeWalker
{
public List<IntermediateNodeReference> Directives = new List<IntermediateNodeReference>();
public override void VisitDirective(DirectiveIntermediateNode node)
{
Directives.Add(new IntermediateNodeReference(Parent, node));
base.VisitDirective(node);
}
}
private static bool IsMalformed(List<RazorDiagnostic> diagnostics)
=> diagnostics.Count > 0 && diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error);
}

View File

@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
{
internal static class CommonAnnotations
{
public static readonly object Imported = "Imported";
public static readonly object PrimaryClass = "PrimaryClass";
public static readonly object PrimaryMethod = "PrimaryMethod";

View File

@ -542,6 +542,34 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatDirectiveMustExistBeforeMarkupOrCode(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("DirectiveMustExistBeforeMarkupOrCode"), p0);
/// <summary>
/// Block directive '{0}' cannot be imported.
/// </summary>
internal static string BlockDirectiveCannotBeImported
{
get => GetString("BlockDirectiveCannotBeImported");
}
/// <summary>
/// Block directive '{0}' cannot be imported.
/// </summary>
internal static string FormatBlockDirectiveCannotBeImported(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("BlockDirectiveCannotBeImported"), p0);
/// <summary>
/// Unreachable code. This can happen when a new {0} is introduced.
/// </summary>
internal static string UnexpectedDirectiveKind
{
get => GetString("UnexpectedDirectiveKind");
}
/// <summary>
/// Unreachable code. This can happen when a new {0} is introduced.
/// </summary>
internal static string FormatUnexpectedDirectiveKind(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("UnexpectedDirectiveKind"), p0);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -13,6 +13,16 @@ namespace Microsoft.AspNetCore.Razor.Language
// General Errors ID Offset = 0
public static readonly RazorDiagnosticDescriptor Directive_BlockDirectiveCannotBeImported =
new RazorDiagnosticDescriptor(
$"{DiagnosticPrefix}0000",
() => Resources.BlockDirectiveCannotBeImported,
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic CreateDirective_BlockDirectiveCannotBeImported(string directive)
{
return RazorDiagnostic.Create(Directive_BlockDirectiveCannotBeImported, SourceSpan.Undefined, directive);
}
#endregion
#region Language Errors

View File

@ -231,4 +231,10 @@
<data name="DirectiveMustExistBeforeMarkupOrCode" xml:space="preserve">
<value>The '{0}' directive must exist prior to markup or code.</value>
</data>
<data name="BlockDirectiveCannotBeImported" xml:space="preserve">
<value>Block directive '{0}' cannot be imported.</value>
</data>
<data name="UnexpectedDirectiveKind" xml:space="preserve">
<value>Unreachable code. This can happen when a new {0} is introduced.</value>
</data>
</root>

View File

@ -380,7 +380,7 @@ namespace Microsoft.AspNetCore.Razor.Language
}
[Fact]
public void Lower_WithImports_Directive()
public void Lower_WithMultipleImports_SingleLineFileScopedSinglyOccurring()
{
// Arrange
var source = TestRazorSourceDocument.Create("<p>Hi!</p>");
@ -395,13 +395,19 @@ namespace Microsoft.AspNetCore.Razor.Language
// Act
var documentNode = Lower(codeDocument, b =>
{
b.AddDirective(DirectiveDescriptor.CreateDirective("test", DirectiveKind.SingleLine, d => d.AddMemberToken()));
b.AddDirective(DirectiveDescriptor.CreateDirective(
"test",
DirectiveKind.SingleLine,
builder =>
{
builder.AddMemberToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
}));
});
// Assert
Children(
documentNode,
n => Directive("test", n, c => DirectiveToken(DirectiveTokenKind.Member, "value1", c)),
n => Directive("test", n, c => DirectiveToken(DirectiveTokenKind.Member, "value2", c)),
n => Html("<p>Hi!</p>", n));
}

View File

@ -2,6 +2,8 @@
// 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.Language.Intermediate;
using Microsoft.AspNetCore.Testing;
using Xunit;
@ -9,6 +11,198 @@ namespace Microsoft.AspNetCore.Razor.Language
{
public class DefaultRazorIntermediateNodeLoweringPhaseTest
{
[Fact]
public void Execute_AutomaticallyImportsSingleLineSinglyOccurringDirective()
{
// Arrange
var directive = DirectiveDescriptor.CreateSingleLineDirective(
"custom",
builder =>
{
builder.AddStringToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(directive);
});
var options = RazorParserOptions.Create(new[] { directive }, designTime: false);
var importSource = TestRazorSourceDocument.Create("@custom \"hello\"", fileName: "import.cshtml");
var codeDocument = TestRazorCodeDocument.Create("<p>NonDirective</p>");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource, options) });
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var customDirectives = documentNode.FindDirectiveReferences(directive);
var customDirective = (DirectiveIntermediateNode)Assert.Single(customDirectives).Node;
var stringToken = Assert.Single(customDirective.Tokens);
Assert.Equal("\"hello\"", stringToken.Content);
}
[Fact]
public void Execute_AutomaticallyOverridesImportedSingleLineSinglyOccurringDirective_MainDocument()
{
// Arrange
var directive = DirectiveDescriptor.CreateSingleLineDirective(
"custom",
builder =>
{
builder.AddStringToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(directive);
});
var options = RazorParserOptions.Create(new[] { directive }, designTime: false);
var importSource = TestRazorSourceDocument.Create("@custom \"hello\"", fileName: "import.cshtml");
var codeDocument = TestRazorCodeDocument.Create("@custom \"world\"");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource, options) });
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var customDirectives = documentNode.FindDirectiveReferences(directive);
var customDirective = (DirectiveIntermediateNode)Assert.Single(customDirectives).Node;
var stringToken = Assert.Single(customDirective.Tokens);
Assert.Equal("\"world\"", stringToken.Content);
}
[Fact]
public void Execute_AutomaticallyOverridesImportedSingleLineSinglyOccurringDirective_MultipleImports()
{
// Arrange
var directive = DirectiveDescriptor.CreateSingleLineDirective(
"custom",
builder =>
{
builder.AddStringToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(directive);
});
var options = RazorParserOptions.Create(new[] { directive }, designTime: false);
var importSource1 = TestRazorSourceDocument.Create("@custom \"hello\"", fileName: "import1.cshtml");
var importSource2 = TestRazorSourceDocument.Create("@custom \"world\"", fileName: "import2.cshtml");
var codeDocument = TestRazorCodeDocument.Create("<p>NonDirective</p>");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource1, options), RazorSyntaxTree.Parse(importSource2, options) });
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var customDirectives = documentNode.FindDirectiveReferences(directive);
var customDirective = (DirectiveIntermediateNode)Assert.Single(customDirectives).Node;
var stringToken = Assert.Single(customDirective.Tokens);
Assert.Equal("\"world\"", stringToken.Content);
}
[Fact]
public void Execute_DoesNotImportNonFileScopedSinglyOccurringDirectives_Block()
{
// Arrange
var codeBlockDirective = DirectiveDescriptor.CreateCodeBlockDirective("code", b => b.AddStringToken());
var razorBlockDirective = DirectiveDescriptor.CreateRazorBlockDirective("razor", b => b.AddStringToken());
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(codeBlockDirective);
b.AddDirective(razorBlockDirective);
});
var options = RazorParserOptions.Create(new[] { codeBlockDirective, razorBlockDirective }, designTime: false);
var importSource = TestRazorSourceDocument.Create(
@"@code ""code block"" { }
@razor ""razor block"" { }",
fileName: "testImports.cshtml");
var codeDocument = TestRazorCodeDocument.Create("<p>NonDirective</p>");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource, options) });
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var directives = documentNode.Children.OfType<DirectiveIntermediateNode>();
Assert.Empty(directives);
}
[Fact]
public void Execute_ErrorsForCodeBlockFileScopedSinglyOccurringDirectives()
{
// Arrange
var directive = DirectiveDescriptor.CreateCodeBlockDirective("custom", b => b.Usage = DirectiveUsage.FileScopedSinglyOccurring);
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(directive);
});
var options = RazorParserOptions.Create(new[] { directive }, designTime: false);
var importSource = TestRazorSourceDocument.Create("@custom { }", fileName: "import.cshtml");
var codeDocument = TestRazorCodeDocument.Create("<p>NonDirective</p>");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource, options) });
var expectedDiagnostic = RazorDiagnosticFactory.CreateDirective_BlockDirectiveCannotBeImported("custom");
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var directives = documentNode.Children.OfType<DirectiveIntermediateNode>();
Assert.Empty(directives);
var diagnostic = Assert.Single(documentNode.GetAllDiagnostics());
Assert.Equal(expectedDiagnostic, diagnostic);
}
[Fact]
public void Execute_ErrorsForRazorBlockFileScopedSinglyOccurringDirectives()
{
// Arrange
var directive = DirectiveDescriptor.CreateRazorBlockDirective("custom", b => b.Usage = DirectiveUsage.FileScopedSinglyOccurring);
var phase = new DefaultRazorIntermediateNodeLoweringPhase();
var engine = RazorEngine.CreateEmpty(b =>
{
b.Phases.Add(phase);
b.AddDirective(directive);
});
var options = RazorParserOptions.Create(new[] { directive }, designTime: false);
var importSource = TestRazorSourceDocument.Create("@custom { }", fileName: "import.cshtml");
var codeDocument = TestRazorCodeDocument.Create("<p>NonDirective</p>");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source, options));
codeDocument.SetImportSyntaxTrees(new[] { RazorSyntaxTree.Parse(importSource, options) });
var expectedDiagnostic = RazorDiagnosticFactory.CreateDirective_BlockDirectiveCannotBeImported("custom");
// Act
phase.Execute(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentIntermediateNode();
var directives = documentNode.Children.OfType<DirectiveIntermediateNode>();
Assert.Empty(directives);
var diagnostic = Assert.Single(documentNode.GetAllDiagnostics());
Assert.Equal(expectedDiagnostic, diagnostic);
}
[Fact]
public void Execute_ThrowsForMissingDependency_SyntaxTree()
{
@ -34,7 +228,6 @@ namespace Microsoft.AspNetCore.Razor.Language
var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase));
var codeDocument = TestRazorCodeDocument.Create("<p class=@(");
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source));
var options = RazorCodeGenerationOptions.CreateDefault();
// Act
phase.Execute(codeDocument);