From 09ac126ecf8099504c3891be10dbf6b859b82295 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Fri, 23 Jun 2017 00:50:18 -0700 Subject: [PATCH] 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 --- ...faultRazorIntermediateNodeLoweringPhase.cs | 261 ++++++++++-------- .../Intermediate/CommonAnnotations.cs | 2 + .../Properties/Resources.Designer.cs | 28 ++ .../RazorDiagnosticFactory.cs | 10 + .../Resources.resx | 6 + ...mediateNodeLoweringPhaseIntegrationTest.cs | 12 +- ...tRazorIntermediateNodeLoweringPhaseTest.cs | 195 ++++++++++++- 7 files changed, 399 insertions(+), 115 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs index 3e65c31776..48394b103c 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -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(); + 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 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 // // Name=checked @@ -783,6 +768,60 @@ namespace Microsoft.AspNetCore.Razor.Language } } + private class ImportsVisitor : LoweringVisitor + { + public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary 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 Directives = new List(); + + public override void VisitDirective(DirectiveIntermediateNode node) + { + Directives.Add(new IntermediateNodeReference(Parent, node)); + + base.VisitDirective(node); + } + } + private static bool IsMalformed(List diagnostics) => diagnostics.Count > 0 && diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error); } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/CommonAnnotations.cs b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/CommonAnnotations.cs index b721520bfe..cccd97fb85 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/CommonAnnotations.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/CommonAnnotations.cs @@ -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"; diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index f03e6bd554..54aeab2417 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -542,6 +542,34 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatDirectiveMustExistBeforeMarkupOrCode(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("DirectiveMustExistBeforeMarkupOrCode"), p0); + /// + /// Block directive '{0}' cannot be imported. + /// + internal static string BlockDirectiveCannotBeImported + { + get => GetString("BlockDirectiveCannotBeImported"); + } + + /// + /// Block directive '{0}' cannot be imported. + /// + internal static string FormatBlockDirectiveCannotBeImported(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("BlockDirectiveCannotBeImported"), p0); + + /// + /// Unreachable code. This can happen when a new {0} is introduced. + /// + internal static string UnexpectedDirectiveKind + { + get => GetString("UnexpectedDirectiveKind"); + } + + /// + /// Unreachable code. This can happen when a new {0} is introduced. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs index b0e44ee42f..52d7bb2997 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorDiagnosticFactory.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index 3298994894..b0b16e933f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -231,4 +231,10 @@ The '{0}' directive must exist prior to markup or code. + + Block directive '{0}' cannot be imported. + + + Unreachable code. This can happen when a new {0} is introduced. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest.cs index 10beea43c6..6024fa1bdb 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest.cs @@ -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("

Hi!

"); @@ -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("

Hi!

", n)); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseTest.cs index 63e9b87435..e613c42b03 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorIntermediateNodeLoweringPhaseTest.cs @@ -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("

NonDirective

"); + 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("

NonDirective

"); + 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("

NonDirective

"); + 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(); + 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("

NonDirective

"); + 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(); + 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("

NonDirective

"); + 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(); + 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("