From 03549bb5428e81f3f74dd3bb07a5aeb780ae0414 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 29 Nov 2016 23:34:03 -0800 Subject: [PATCH] Add end-to-end support for extensible directives This change adds a way to actually configure the RazorEngine to use extensible directives (previously buried behind legacy API). As part of this feature adds the RazorParserOptions class to encapsulate anything else that becomes a parser options (ahem taghelpers). Now we have a pattern for this when we get there. Options are propagated as part of the RazorSyntaxTree for testability/sanity and this was actually responsible for the bulk of the changes. Also added some extension methods for adding directives to the IRazorEngineBuilder and an end to end integration test. --- .../DefaultRazorDirectiveFeature.cs | 26 ++++++++++ .../DefaultRazorParsingPhase.cs | 17 ++++++- .../DefaultRazorSyntaxTree.cs | 5 +- .../HtmlNodeOptimizationPass.cs | 2 +- .../IRazorConfigureParserFeature.cs | 12 +++++ .../IRazorDirectiveFeature.cs | 12 +++++ .../Legacy/ParserContext.cs | 8 --- .../Legacy/RazorParser.cs | 29 +++++++---- .../RazorEngineBuilderExtensions.cs | 41 ++++++++++++++++ .../RazorParserOptions.cs | 24 +++++++++ .../RazorSyntaxTree.cs | 26 ++++++++-- .../TagHelperBinderSyntaxTreePass.cs | 4 +- .../DefaultRazorParsingPhaseTest.cs | 35 +++++++++++++ .../IntegrationTest.cs | 34 +++++++++++++ .../Legacy/ParserTestBase.cs | 29 ++++++++--- .../RazorEngineBuilderExtensionsTest.cs | 49 +++++++++++++++++++ .../TagHelperBinderSyntaxTreePassTest.cs | 4 +- 17 files changed, 320 insertions(+), 37 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorConfigureParserFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveFeature.cs new file mode 100644 index 0000000000..5fb597984f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveFeature.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorDirectiveFeature : IRazorDirectiveFeature, IRazorConfigureParserFeature + { + public ICollection Directives { get; } = new List(); + + public RazorEngine Engine { get; set; } + + public int Order => 100; + + void IRazorConfigureParserFeature.Configure(RazorParserOptions options) + { + options.Directives.Clear(); + + foreach (var directive in Directives) + { + options.Directives.Add(directive); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs index 81caab0fd8..bd1cba4fa5 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs @@ -1,15 +1,28 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; +using System.Linq; namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultRazorParsingPhase : RazorEnginePhaseBase, IRazorParsingPhase { + private IRazorConfigureParserFeature[] _parserOptionsCallbacks; + + protected override void OnIntialized() + { + _parserOptionsCallbacks = Engine.Features.OfType().ToArray(); + } + protected override void ExecuteCore(RazorCodeDocument codeDocument) { - var syntaxTree = RazorSyntaxTree.Parse(codeDocument.Source); + var options = RazorParserOptions.CreateDefaultOptions(); + for (var i = 0; i < _parserOptionsCallbacks.Length; i++) + { + _parserOptionsCallbacks[i].Configure(options); + } + + var syntaxTree = RazorSyntaxTree.Parse(codeDocument.Source, options); codeDocument.SetSyntaxTree(syntaxTree); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs index 2890b981a3..81f7e264b9 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs @@ -8,14 +8,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultRazorSyntaxTree : RazorSyntaxTree { - public DefaultRazorSyntaxTree(Block root, IReadOnlyList diagnostics) + public DefaultRazorSyntaxTree(Block root, IReadOnlyList diagnostics, RazorParserOptions options) { Root = root; Diagnostics = diagnostics; + Options = options; } internal override IReadOnlyList Diagnostics { get; } + public override RazorParserOptions Options { get; } + internal override Block Root { get; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/HtmlNodeOptimizationPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/HtmlNodeOptimizationPass.cs index 21562dbfa6..78d913322b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/HtmlNodeOptimizationPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/HtmlNodeOptimizationPass.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var whitespaceRewriter = new WhiteSpaceRewriter(); rewritten = whitespaceRewriter.Rewrite(rewritten); - var rewrittenSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Diagnostics); + var rewrittenSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Diagnostics, syntaxTree.Options); return rewrittenSyntaxTree; } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorConfigureParserFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorConfigureParserFeature.cs new file mode 100644 index 0000000000..588a5516d6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorConfigureParserFeature.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal interface IRazorConfigureParserFeature : IRazorEngineFeature + { + int Order { get; } + + void Configure(RazorParserOptions options); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveFeature.cs new file mode 100644 index 0000000000..e337c3579c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public interface IRazorDirectiveFeature : IRazorEngineFeature + { + ICollection Directives { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserContext.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserContext.cs index 6ef35d4177..33fa08c550 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserContext.cs @@ -38,14 +38,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy { get { return Source.Peek() == -1; } } - - public RazorSyntaxTree BuildRazorSyntaxTree() - { - var syntaxTree = Builder.Build(); - var razorSyntaxTree = RazorSyntaxTree.Create(syntaxTree, ErrorSink.Errors); - - return razorSyntaxTree; - } } // Debug Helpers diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/RazorParser.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/RazorParser.cs index 1549dc6382..ab0cda7fe0 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/RazorParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/RazorParser.cs @@ -1,6 +1,7 @@ // 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.IO; namespace Microsoft.AspNetCore.Razor.Evolution.Legacy @@ -8,10 +9,21 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy internal class RazorParser { public RazorParser() + : this(RazorParserOptions.CreateDefaultOptions()) { } - public bool DesignTimeMode { get; set; } + public RazorParser(RazorParserOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + Options = options; + } + + public RazorParserOptions Options { get; } public virtual RazorSyntaxTree Parse(TextReader input) => Parse(input.ReadToEnd()); @@ -23,22 +35,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy private RazorSyntaxTree ParseCore(ITextDocument input) { - var context = new ParserContext(input, DesignTimeMode); + var context = new ParserContext(input, Options.DesignTimeMode); - var codeParser = new CSharpCodeParser(context); + var codeParser = new CSharpCodeParser(Options.Directives, context); var markupParser = new HtmlMarkupParser(context); codeParser.HtmlParser = markupParser; markupParser.CodeParser = codeParser; - // Execute the parse markupParser.ParseDocument(); - - // Get the result - var razorSyntaxTree = context.BuildRazorSyntaxTree(); - - // Return the new result - return razorSyntaxTree; + + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; + return RazorSyntaxTree.Create(root, diagnostics, Options); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs new file mode 100644 index 0000000000..f0f6fe9751 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public static class RazorEngineBuilderExtensions + { + public static IRazorEngineBuilder AddDirective(this IRazorEngineBuilder builder, DirectiveDescriptor directive) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(directive)); + } + + if (directive == null) + { + throw new ArgumentNullException(nameof(directive)); + } + + var directiveFeature = GetDirectiveFeature(builder); + directiveFeature.Directives.Add(directive); + + return builder; + } + + private static IRazorDirectiveFeature GetDirectiveFeature(IRazorEngineBuilder builder) + { + var directiveFeature = builder.Features.OfType().FirstOrDefault(); + if (directiveFeature == null) + { + directiveFeature = new DefaultRazorDirectiveFeature(); + builder.Features.Add(directiveFeature); + } + + return directiveFeature; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs new file mode 100644 index 0000000000..f57d9d3f98 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorParserOptions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public sealed class RazorParserOptions + { + public static RazorParserOptions CreateDefaultOptions() + { + return new RazorParserOptions(); + } + + private RazorParserOptions() + { + Directives = new List(); + } + + public bool DesignTimeMode { get; set; } + + public ICollection Directives { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs index dbbf6171e6..ed5432f7e6 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs @@ -9,7 +9,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution { public abstract class RazorSyntaxTree { - internal static RazorSyntaxTree Create(Block root, IEnumerable diagnostics) + internal static RazorSyntaxTree Create( + Block root, + IEnumerable diagnostics, + RazorParserOptions options) { if (root == null) { @@ -21,7 +24,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution throw new ArgumentNullException(nameof(diagnostics)); } - return new DefaultRazorSyntaxTree(root, new List(diagnostics)); + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return new DefaultRazorSyntaxTree(root, new List(diagnostics), options); } public static RazorSyntaxTree Parse(RazorSourceDocument source) @@ -31,7 +39,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution throw new ArgumentNullException(nameof(source)); } - var parser = new RazorParser(); + return Parse(source, options: null); + } + + public static RazorSyntaxTree Parse(RazorSourceDocument source, RazorParserOptions options) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var parser = new RazorParser(options ?? RazorParserOptions.CreateDefaultOptions()); var sourceContent = new char[source.Length]; source.CopyTo(0, sourceContent, 0, source.Length); @@ -40,6 +58,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution internal abstract IReadOnlyList Diagnostics { get; } + public abstract RazorParserOptions Options { get; } + internal abstract Block Root { get; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs index fcfd115a79..fe1262db5a 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution if (errorSink.Errors.Count > 0) { var combinedErrors = CombineErrors(syntaxTree.Diagnostics, errorSink.Errors); - var erroredTree = RazorSyntaxTree.Create(syntaxTree.Root, combinedErrors); + var erroredTree = RazorSyntaxTree.Create(syntaxTree.Root, combinedErrors, syntaxTree.Options); return erroredTree; } @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution diagnostics = CombineErrors(diagnostics, errorSink.Errors); } - var newSyntaxTree = RazorSyntaxTree.Create(rewrittenRoot, diagnostics); + var newSyntaxTree = RazorSyntaxTree.Create(rewrittenRoot, diagnostics, syntaxTree.Options); return newSyntaxTree; } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs index 39e505cabf..4eef9e3e1c 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs @@ -1,6 +1,7 @@ // 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 Xunit; namespace Microsoft.AspNetCore.Razor.Evolution @@ -22,5 +23,39 @@ namespace Microsoft.AspNetCore.Razor.Evolution // Assert Assert.NotNull(codeDocument.GetSyntaxTree()); } + + [Fact] + public void Execute_UsesConfigureParserFeatures() + { + // Arrange + var phase = new DefaultRazorParsingPhase(); + var engine = RazorEngine.CreateEmpty((b) => + { + b.Phases.Add(phase); + b.Features.Add(new MyConfigureParserOptions()); + }); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act + phase.Execute(codeDocument); + + // Assert + var syntaxTree = codeDocument.GetSyntaxTree(); + var directive = Assert.Single(syntaxTree.Options.Directives); + Assert.Equal("test_directive", directive.Name); + } + + private class MyConfigureParserOptions : IRazorConfigureParserFeature + { + public RazorEngine Engine { get; set; } + + public int Order { get; } + + public void Configure(RazorParserOptions options) + { + options.Directives.Add(DirectiveDescriptorBuilder.Create("test_directive").Build()); + } + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs index e85cc4b72b..f8cecc27bf 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; using Xunit; namespace Microsoft.AspNetCore.Razor.Evolution @@ -20,6 +22,38 @@ namespace Microsoft.AspNetCore.Razor.Evolution // Assert Assert.NotNull(document.GetSyntaxTree()); + Assert.NotNull(document.GetIRDocument()); + } + + [Fact] + public void Process_CustomDirective() + { + // Arrange + var engine = RazorEngine.Create(b => + { + b.AddDirective(DirectiveDescriptorBuilder.Create("test_directive").Build()); + }); + + var document = RazorCodeDocument.Create(TestRazorSourceDocument.Create("@test_directive")); + + // Act + engine.Process(document); + + // Assert + var syntaxTree = document.GetSyntaxTree(); + + // This is fragile for now, but we don't want to invest in the legacy API until we're ready + // to replace it properly. + var directiveBlock = (Block)syntaxTree.Root.Children[1]; + var directiveSpan = (Span)directiveBlock.Children[1]; + Assert.Equal("test_directive", directiveSpan.Content); + + var irDocument = document.GetIRDocument(); + var irNamespace = irDocument.Children[0]; + var irClass = irNamespace.Children[0]; + var irMethod = irClass.Children[0]; + var irDirective = (DirectiveIRNode)irMethod.Children[1]; + Assert.Equal("test_directive", irDirective.Name); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs index c1f6d55f82..bc99aedb31 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs @@ -29,10 +29,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy { using (var reader = new SeekableTextReader(document)) { - var parser = new RazorParser() - { - DesignTimeMode = designTime, - }; + var options = RazorParserOptions.CreateDefaultOptions(); + options.DesignTimeMode = designTime; + + var parser = new RazorParser(options); return parser.Parse((ITextDocument)reader); } @@ -52,9 +52,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy parser.ParseBlock(); - var razorSyntaxTree = context.BuildRazorSyntaxTree(); + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; + var options = RazorParserOptions.CreateDefaultOptions(); + options.DesignTimeMode = designTime; - return razorSyntaxTree; + return RazorSyntaxTree.Create(root, diagnostics, options); } } @@ -80,9 +83,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy parser.ParseBlock(); - var razorSyntaxTree = context.BuildRazorSyntaxTree(); + var root = context.Builder.Build(); + var diagnostics = context.ErrorSink.Errors; - return razorSyntaxTree; + var options = RazorParserOptions.CreateDefaultOptions(); + options.DesignTimeMode = designTime; + + options.Directives.Clear(); + foreach (var directive in descriptors) + { + options.Directives.Add(directive); + } + + return RazorSyntaxTree.Create(root, diagnostics, options); } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs new file mode 100644 index 0000000000..1078789e74 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs @@ -0,0 +1,49 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class RazorEngineBuilderExtensionsTest + { + [Fact] + public void AddDirective_ExistingFeature_UsesFeature() + { + // Arrange + var expected = new DefaultRazorDirectiveFeature(); + var engine = RazorEngine.CreateEmpty(b => + { + b.Features.Add(expected); + + // Act + b.AddDirective(DirectiveDescriptorBuilder.Create("test_directive").Build()); + }); + + // Assert + var actual = Assert.Single(engine.Features.OfType()); + Assert.Same(expected, actual); + + var directive = Assert.Single(actual.Directives); + Assert.Equal("test_directive", directive.Name); + } + + public void AddDirective_NoFeature_CreatesFeature() + { + // Arrange + var engine = RazorEngine.CreateEmpty(b => + { + // Act + b.AddDirective(DirectiveDescriptorBuilder.Create("test_directive").Build()); + }); + + // Assert + var actual = Assert.Single(engine.Features.OfType()); + Assert.IsType(actual); + + var directive = Assert.Single(actual.Directives); + Assert.Equal("test_directive", directive.Name); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs index fa40761991..73dcd91246 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var codeDocument = RazorCodeDocument.Create(sourceDocument); var originalTree = RazorSyntaxTree.Parse(sourceDocument); var initialError = new RazorError("Initial test error", SourceLocation.Zero, length: 1); - var erroredOriginalTree = RazorSyntaxTree.Create(originalTree.Root, new[] { initialError }); + var erroredOriginalTree = RazorSyntaxTree.Create(originalTree.Root, new[] { initialError }, originalTree.Options); // Act var outputTree = pass.Execute(codeDocument, erroredOriginalTree); @@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution LegacyResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper("form"), new SourceLocation(Environment.NewLine.Length * 2 + 30, 2, 1), length: 4); - var erroredOriginalTree = RazorSyntaxTree.Create(originalTree.Root, new[] { initialError }); + var erroredOriginalTree = RazorSyntaxTree.Create(originalTree.Root, new[] { initialError }, originalTree.Options); // Act var outputTree = pass.Execute(codeDocument, erroredOriginalTree);