diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs index eba2208017..cbb1a742eb 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs @@ -148,7 +148,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration Context.Writer.SetIndent(originalIndent); } Context.Writer.WriteLine("))();"); - } public override void VisitTemplate(TemplateIRNode node) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs index 58d7fcd543..557547a396 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs @@ -2,42 +2,89 @@ // 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.Evolution.Intermediate; using Microsoft.AspNetCore.Razor.Evolution.Legacy; namespace Microsoft.AspNetCore.Razor.Evolution { - internal class DefaultDirectiveIRPass : RazorIRPassBase + internal class DefaultDirectiveIRPass : RazorIRPassBase, IRazorDirectiveClassifierPass { - public override int Order => RazorIRPass.DefaultDirectiveClassifierOrder; - - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var parserOptions = irDocument.Options; var designTime = parserOptions.DesignTimeMode; - var walker = new DirectiveWalker(designTime); + var walker = new DirectiveWalker(); walker.VisitDocument(irDocument); - return irDocument; + var classNode = walker.ClassNode; + foreach (var node in walker.FunctionsDirectiveNodes) + { + node.Parent.Children.Remove(node); + + foreach (var child in node.Children.Except(node.Tokens)) + { + child.Parent = classNode; + classNode.Children.Add(child); + } + } + + foreach (var node in walker.InheritsDirectiveNodes.Reverse()) + { + node.Parent.Children.Remove(node); + + var token = node.Tokens.FirstOrDefault(); + if (token != null) + { + classNode.BaseType = token.Content; + break; + } + } + + foreach (var node in walker.SectionDirectiveNodes) + { + var sectionIndex = node.Parent.Children.IndexOf(node); + node.Parent.Children.Remove(node); + + var defineSectionEndStatement = new CSharpStatementIRNode() + { + Content = "});", + }; + node.Parent.Children.Insert(sectionIndex, defineSectionEndStatement); + + foreach (var child in node.Children.Except(node.Tokens).Reverse()) + { + node.Parent.Children.Insert(sectionIndex, child); + } + + var lambdaContent = designTime ? "__razor_section_writer" : string.Empty; + var sectionName = node.Tokens.FirstOrDefault()?.Content; + var defineSectionStartStatement = new CSharpStatementIRNode() + { + Content = /* ORIGINAL: DefineSectionMethodName */ $"DefineSection(\"{sectionName}\", async ({lambdaContent}) => {{", + }; + + node.Parent.Children.Insert(sectionIndex, defineSectionStartStatement); + } } private class DirectiveWalker : RazorIRNodeWalker { - private ClassDeclarationIRNode _classNode; - private readonly bool _designTime; + public ClassDeclarationIRNode ClassNode { get; private set; } - public DirectiveWalker(bool designTime) - { - _designTime = designTime; - } + public IList FunctionsDirectiveNodes { get; } = new List(); + + public IList InheritsDirectiveNodes { get; } = new List(); + + public IList SectionDirectiveNodes { get; } = new List(); public override void VisitClass(ClassDeclarationIRNode node) { - if (_classNode == null) + if (ClassNode == null) { - _classNode = node; + ClassNode = node; } VisitDefault(node); @@ -47,47 +94,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution { if (string.Equals(node.Name, CSharpCodeParser.FunctionsDirectiveDescriptor.Name, StringComparison.Ordinal)) { - foreach (var child in node.Children.Except(node.Tokens)) - { - child.Parent = _classNode; - _classNode.Children.Add(child); - } + FunctionsDirectiveNodes.Add(node); } else if (string.Equals(node.Name, CSharpCodeParser.InheritsDirectiveDescriptor.Name, StringComparison.Ordinal)) { - var token = node.Tokens.FirstOrDefault(); - - if (token != null) - { - _classNode.BaseType = token.Content; - } + InheritsDirectiveNodes.Add(node); } else if (string.Equals(node.Name, CSharpCodeParser.SectionDirectiveDescriptor.Name, StringComparison.Ordinal)) { - var sectionIndex = node.Parent.Children.IndexOf(node); - - var defineSectionEndStatement = new CSharpStatementIRNode() - { - Content = "});", - }; - node.Parent.Children.Insert(sectionIndex, defineSectionEndStatement); - - foreach (var child in node.Children.Except(node.Tokens).Reverse()) - { - node.Parent.Children.Insert(sectionIndex, child); - } - - var lambdaContent = _designTime ? "__razor_section_writer" : string.Empty; - var sectionName = node.Tokens.FirstOrDefault()?.Content; - var defineSectionStartStatement = new CSharpStatementIRNode() - { - Content = /* ORIGINAL: DefineSectionMethodName */ $"DefineSection(\"{sectionName}\", async ({lambdaContent}) => {{", - }; - - node.Parent.Children.Insert(sectionIndex, defineSectionStartStatement); + SectionDirectiveNodes.Add(node); } - - node.Parent.Children.Remove(node); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs index ccaac151c0..50b6403dae 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultDocumentClassifierPass : DocumentClassifierPassBase { - public override int Order => RazorIRPass.DefaultDocumentClassifierOrder; + public override int Order => RazorIRPass.DefaultFeatureOrder; protected override string DocumentKind => "default"; diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultInstrumentationPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultInstrumentationPass.cs index f49e296190..e42fe78b43 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultInstrumentationPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultInstrumentationPass.cs @@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - public class DefaultInstrumentationPass : RazorIRPassBase + public class DefaultInstrumentationPass : RazorIRPassBase, IRazorIROptimizationPass { - public override int Order => RazorIRPass.DefaultLoweringOrder; + public override int Order => RazorIRPass.DefaultFeatureOrder; - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new Visitor(); walker.VisitDocument(irDocument); @@ -22,8 +22,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution AddInstrumentation(node); } - - return irDocument; } private static void AddInstrumentation(InstrumentationItem item) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveClassifierPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveClassifierPhase.cs new file mode 100644 index 0000000000..b69517387a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDirectiveClassifierPhase.cs @@ -0,0 +1,30 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorDirectiveClassifierPhase : RazorEnginePhaseBase, IRazorDirectiveClassifierPhase + { + public IRazorDirectiveClassifierPass[] Passes { get; private set; } + + protected override void OnIntialized() + { + Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var irDocument = codeDocument.GetIRDocument(); + ThrowForMissingDependency(irDocument); + + foreach (var pass in Passes) + { + pass.Execute(codeDocument, irDocument); + } + + codeDocument.SetIRDocument(irDocument); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDocumentClassifierPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDocumentClassifierPhase.cs new file mode 100644 index 0000000000..9a7a3f177e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorDocumentClassifierPhase.cs @@ -0,0 +1,30 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorDocumentClassifierPhase : RazorEnginePhaseBase, IRazorDocumentClassifierPhase + { + public IRazorDocumentClassifierPass[] Passes { get; private set; } + + protected override void OnIntialized() + { + Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var irDocument = codeDocument.GetIRDocument(); + ThrowForMissingDependency(irDocument); + + foreach (var pass in Passes) + { + pass.Execute(codeDocument, irDocument); + } + + codeDocument.SetIRDocument(irDocument); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIROptimizationPhase.cs similarity index 66% rename from src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs rename to src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIROptimizationPhase.cs index e602949b87..025f330a8d 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIROptimizationPhase.cs @@ -5,13 +5,13 @@ using System.Linq; namespace Microsoft.AspNetCore.Razor.Evolution { - internal class DefaultRazorIRPhase : RazorEnginePhaseBase, IRazorIRPhase + internal class DefaultRazorIROptimizationPhase : RazorEnginePhaseBase, IRazorIROptimizationPhase { - public IRazorIRPass[] Passes { get; private set; } + public IRazorIROptimizationPass[] Passes { get; private set; } protected override void OnIntialized() { - Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); + Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); } protected override void ExecuteCore(RazorCodeDocument codeDocument) @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution foreach (var pass in Passes) { - irDocument = pass.Execute(codeDocument, irDocument); + pass.Execute(codeDocument, irDocument); } codeDocument.SetIRDocument(irDocument); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveRemovalIROptimizationPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveRemovalIROptimizationPass.cs new file mode 100644 index 0000000000..a770cfac52 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveRemovalIROptimizationPass.cs @@ -0,0 +1,34 @@ +// 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; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DirectiveRemovalIROptimizationPass : RazorIRPassBase, IRazorIROptimizationPass + { + public override int Order => RazorIRPass.DefaultFeatureOrder + 50; + + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + var visitor = new Visitor(); + visitor.VisitDocument(irDocument); + + foreach (var node in visitor.DirectiveNodes) + { + node.Parent.Children.Remove(node); + } + } + + private class Visitor : RazorIRNodeWalker + { + public IList DirectiveNodes { get; } = new List(); + + public override void VisitDirective(DirectiveIRNode node) + { + DirectiveNodes.Add(node); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs index 2d306aae96..166bdaecee 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs @@ -7,30 +7,26 @@ using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Evolution { - public abstract class DocumentClassifierPassBase : RazorIRPassBase + public abstract class DocumentClassifierPassBase : RazorIRPassBase, IRazorDocumentClassifierPass { protected abstract string DocumentKind { get; } - public override int Order => RazorIRPass.DocumentClassifierOrder; - - public sealed override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public sealed override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { if (irDocument.DocumentKind != null) { - return irDocument; + return; } if (!IsMatch(codeDocument, irDocument)) { - return irDocument; + return; } irDocument.DocumentKind = DocumentKind; irDocument.Target = CreateTarget(codeDocument, irDocument.Options); Rewrite(codeDocument, irDocument); - - return irDocument; } private void Rewrite(RazorCodeDocument codeDocument, DocumentIRNode irDocument) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs index 7ef32517d4..87c731c2de 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorCSharpLoweringPhase.cs @@ -3,6 +3,14 @@ namespace Microsoft.AspNetCore.Razor.Evolution { + /// + /// Generates C# code using the IR document. + /// + /// + /// After IR processing, the transforms the IR document into generated C# code. + /// At this time any directives or IR constructs that cannot be understood by code generation will result + /// in an error. + /// public interface IRazorCSharpLoweringPhase : IRazorEnginePhase { } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPass.cs new file mode 100644 index 0000000000..2407c5a7d3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPass.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public interface IRazorDirectiveClassifierPass : IRazorEngineFeature + { + int Order { get; } + + void Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPhase.cs new file mode 100644 index 0000000000..0cf333a4bc --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDirectiveClassifierPhase.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. + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + /// + /// Understands directive IR nodes and performs the necessary modifications to the IR document. + /// + /// + /// + /// The second phase of IR processing is directive classification. IR passes in this phase should interpret + /// directives and processing them accordingly by transforming IR nodes or adding diagnostics to the IR. At + /// this time the document kind has been identified, so any directive that can't be applied should trigger + /// errors. If implementing a document kind that diverges from the standard structure of Razor documents + /// it may be necessary to reimplement processing of default directives. + /// + /// + /// objects are executed according to an ascending ordering of the + /// property. The default configuration of + /// prescribes a logical ordering of specific phases of IR processing. + /// + /// + public interface IRazorDirectiveClassifierPhase : IRazorEnginePhase + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPass.cs new file mode 100644 index 0000000000..3f05b59e2d --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPass.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public interface IRazorDocumentClassifierPass : IRazorEngineFeature + { + int Order { get; } + + void Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPhase.cs new file mode 100644 index 0000000000..db2adfa0b1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorDocumentClassifierPhase.cs @@ -0,0 +1,29 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + /// + /// Modifies the IR document to a desired structure. + /// + /// + /// + /// The first phase of IR procesing is document classification. IR passes in this phase should classify the + /// document according to any relevant criteria (project configuration, file extension, directive) and modify + /// the IR tree to suit the desired document shape. Document classifiers should also set + /// to prevent other classifiers from running. If no classifier + /// matches the document, then it will be classified as "generic" and processed according to set + /// of reasonable defaults. + /// + /// + /// objects are executed according to an ascending ordering of the + /// property. The default configuration of + /// prescribes a logical ordering of specific phases of IR processing. + /// + /// + public interface IRazorDocumentClassifierPhase : IRazorEnginePhase + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRLoweringPhase.cs index 556f43e358..8807757ed0 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRLoweringPhase.cs @@ -3,6 +3,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution { + /// + /// Generates the IR document from . + /// + /// + /// The IR document is first produced by . At this point no IR passes have + /// been executed. The default will perform a mechanical transformation + /// of the syntax tree to IR resulting in a mostly flat structure. It is up to later phases to give the document + /// structure and semantics according to a document kind. The default is + /// also responsible for synthesizing IR nodes for global cross-current concerns such as checksums or global settings. + /// public interface IRazorIRLoweringPhase : IRazorEnginePhase { } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPass.cs similarity index 68% rename from src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs rename to src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPass.cs index 5b98c93b26..d5e5ac0e5c 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPass.cs @@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - public interface IRazorIRPass : IRazorEngineFeature + public interface IRazorIROptimizationPass : IRazorEngineFeature { int Order { get; } - DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + void Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPhase.cs new file mode 100644 index 0000000000..cf73334a24 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIROptimizationPhase.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. + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + /// + /// Performs necessary modifications to the IR document to optimize code generation. + /// + /// + /// + /// The last phase of IR processing is lowering. IR passes in this phase perform some kind of transformation + /// on the IR that optimizes the generated code. The key distinction here is that information may be discarded + /// during this phase. + /// + /// + /// objects are executed according to an ascending ordering of the + /// property. The default configuration of + /// prescribes a logical ordering of specific phases of IR processing. + /// + /// + public interface IRazorIROptimizationPhase : IRazorEnginePhase + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs deleted file mode 100644 index 0351bbbc39..0000000000 --- a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// 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 -{ - public interface IRazorIRPhase : IRazorEnginePhase - { - } -} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs index c1331bfcb6..45a7e6d1ad 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs @@ -6,18 +6,17 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - internal class RazorDesignTimeIRPass : RazorIRPassBase + internal class RazorDesignTimeIRPass : RazorIRPassBase, IRazorDirectiveClassifierPass { internal const string DesignTimeVariable = "__o"; - public override int Order => RazorIRPass.DirectiveClassifierOrder; + // This needs to run before other directive classifiers. + public override int Order => -10; - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new DesignTimeHelperWalker(); walker.VisitDocument(irDocument); - - return irDocument; } internal class DesignTimeHelperWalker : RazorIRNodeWalker @@ -26,7 +25,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution public override void VisitClass(ClassDeclarationIRNode node) { - var designTimeHelperDeclaration = new CSharpStatementIRNode() { Content = $"private static {typeof(object).FullName} {DesignTimeVariable} = null;", diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index 335a780f3c..56c91966be 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -51,7 +51,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Phases.Add(new DefaultRazorParsingPhase()); builder.Phases.Add(new DefaultRazorSyntaxTreePhase()); builder.Phases.Add(new DefaultRazorIRLoweringPhase()); - builder.Phases.Add(new DefaultRazorIRPhase()); + builder.Phases.Add(new DefaultRazorDocumentClassifierPhase()); + builder.Phases.Add(new DefaultRazorDirectiveClassifierPhase()); + builder.Phases.Add(new DefaultRazorIROptimizationPhase()); builder.Phases.Add(new DefaultRazorCSharpLoweringPhase()); // Syntax Tree passes @@ -62,6 +64,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution // IR Passes builder.Features.Add(new DefaultDocumentClassifierPass()); builder.Features.Add(new DefaultDirectiveIRPass()); + builder.Features.Add(new DirectiveRemovalIROptimizationPass()); } internal static void AddRuntimeDefaults(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs index cd35d6ea92..a155508ce5 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs @@ -1,87 +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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; - namespace Microsoft.AspNetCore.Razor.Evolution { - /// - /// Provides constants for ordering of objects. When implementing an - /// , choose a value for according to - /// the logical task that must be performed. - /// - /// - /// - /// objects are executed according to an ascending ordering of the - /// property. The default configuration of - /// prescribes a logical ordering of specific phases of IR processing. - /// - /// - /// The IR document is first produced by . At this point no IR passes have - /// been executed. The default will perform a mechanical transformation - /// of the syntax tree to IR resulting in a mostly flat structure. It is up to later phases to give the document - /// structure and semantics according to a document kind. The default is - /// also responsible for synthesizing IR nodes for global cross-current concerns such as checksums or global settings. - /// - /// - /// The first phase of IR procesing is document classification. IR passes in this phase should classify the - /// document according to any relevant criteria (project configuration, file extension, directive) and modify - /// the IR tree to suit the desired document shape. Document classifiers should also set - /// to prevent other classifiers from running. If no classifier - /// matches the document, then it will be classified as "generic" and processed according to set - /// of reasonable defaults. - /// - /// - /// The second phase of IR processing is directive classification. IR passes in this phase should interpret - /// directives and processing them accordingly by transforming IR nodes or adding diagnostics to the IR. At - /// this time the document kind has been identified, so any directive that can't be applied should trigger - /// errors. If implementing a document kind that diverges from the standard structure of Razor documents - /// it may be necessary to reimplement processing of default directives. - /// - /// - /// The last phase of IR processing is lowering. IR passes in this phase perform some kind of transformation - /// on the IR that optimizes the generated code. The key distinction here is that information may be discarded - /// during this phase. - /// - /// - /// Finally, the transforms the IR document into generated C# code. - /// At this time any directives or IR constructs that cannot be understood by code generation will result - /// in an error. - /// - /// public static class RazorIRPass { /// - /// An that implements a document classifier should use this value as its - /// . - /// - public static readonly int DocumentClassifierOrder = 1100; - - /// - /// value used by the default document classifier. + /// The default implementation of the s that run in a + /// will use this value for its Order property. /// - public static readonly int DefaultDocumentClassifierOrder = 1900; - - /// - /// An that implements a directive classifier should use this value as its - /// . - /// - public static readonly int DirectiveClassifierOrder = 2100; - - /// - /// value used by the default directive classifier. - /// - public static readonly int DefaultDirectiveClassifierOrder = 2900; - - /// - /// An that implements a lowering phase should use this value as its - /// . - /// - public static readonly int LoweringOrder = 4100; - - /// - /// value used by the default lowering phase. - /// - public static readonly int DefaultLoweringOrder = 4900; + /// + /// This value is chosen in such a way that the default implementation runs after the other + /// custom implementations for a particular . + /// + public static readonly int DefaultFeatureOrder = 1000; } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs index 561e4af78f..21b0c76bb1 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - public abstract class RazorIRPassBase : IRazorIRPass + public abstract class RazorIRPassBase { private RazorEngine _engine; @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution } } - public abstract int Order { get; } + public virtual int Order { get; } protected void ThrowForMissingDocumentDependency(TDocumentDependency value) { @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { } - public DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public void Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { if (codeDocument == null) { @@ -72,9 +72,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution throw new InvalidOperationException(Resources.FormatPhaseMustBeInitialized(nameof(Engine))); } - return ExecuteCore(codeDocument, irDocument); + ExecuteCore(codeDocument, irDocument); } - public abstract DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + public abstract void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributePass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributeOptimizationPass.cs similarity index 96% rename from src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributePass.cs rename to src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributeOptimizationPass.cs index 90956c9576..211923a91b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributePass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorPreallocatedTagHelperAttributeOptimizationPass.cs @@ -7,16 +7,14 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - internal class RazorPreallocatedTagHelperAttributeOptimizationPass : RazorIRPassBase + internal class RazorPreallocatedTagHelperAttributeOptimizationPass : RazorIRPassBase, IRazorIROptimizationPass { - public override int Order => RazorIRPass.DefaultLoweringOrder; + public override int Order => RazorIRPass.DefaultFeatureOrder; - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new PreallocatedTagHelperWalker(); walker.VisitDocument(irDocument); - - return irDocument; } internal class PreallocatedTagHelperWalker : RazorIRNodeWalker diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs index 4348741a63..2469b7754d 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs @@ -9,31 +9,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution { public class DefaultDirectiveIRPassTest { - [Fact] - public void Execute_MutatesIRDocument() - { - // Arrange - var content = -@"@inherits Hello -@functions { - var value = true; -}"; - var sourceDocument = TestRazorSourceDocument.Create(content); - var codeDocument = RazorCodeDocument.Create(sourceDocument); - var originalIRDocument = Lower(codeDocument); - var defaultEngine = RazorEngine.Create(); - var pass = new DefaultDirectiveIRPass() - { - Engine = defaultEngine, - }; - - // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); - - // Assert - Assert.Same(originalIRDocument, irDocument); - } - [Fact] public void Execute_Inherits_SetsClassDeclarationBaseType() { @@ -41,7 +16,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var content = "@inherits Hello"; var sourceDocument = TestRazorSourceDocument.Create(content); var codeDocument = RazorCodeDocument.Create(sourceDocument); - var originalIRDocument = Lower(codeDocument); + var irDocument = Lower(codeDocument); var defaultEngine = RazorEngine.Create(); var pass = new DefaultDirectiveIRPass() { @@ -49,7 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution }; // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); + pass.Execute(codeDocument, irDocument); // Assert Children(irDocument, @@ -71,7 +46,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var content = "@functions { var value = true; }"; var sourceDocument = TestRazorSourceDocument.Create(content); var codeDocument = RazorCodeDocument.Create(sourceDocument); - var originalIRDocument = Lower(codeDocument); + var irDocument = Lower(codeDocument); var defaultEngine = RazorEngine.Create(); var pass = new DefaultDirectiveIRPass() { @@ -79,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution }; // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); + pass.Execute(codeDocument, irDocument); // Assert Children(irDocument, @@ -105,7 +80,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var content = "@section Header {

Hello World

}"; var sourceDocument = TestRazorSourceDocument.Create(content); var codeDocument = RazorCodeDocument.Create(sourceDocument); - var originalIRDocument = Lower(codeDocument); + var irDocument = Lower(codeDocument); var defaultEngine = RazorEngine.Create(); var pass = new DefaultDirectiveIRPass() { @@ -113,7 +88,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution }; // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); + pass.Execute(codeDocument, irDocument); // Assert Children(irDocument, @@ -140,7 +115,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var designTimeEngine = RazorEngine.CreateDesignTime(); var sourceDocument = TestRazorSourceDocument.Create(content); var codeDocument = RazorCodeDocument.Create(sourceDocument); - var originalIRDocument = Lower(codeDocument, designTimeEngine); + var irDocument = Lower(codeDocument, designTimeEngine); var defaultEngine = RazorEngine.Create(); var pass = new DefaultDirectiveIRPass() { @@ -148,7 +123,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution }; // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); + pass.Execute(codeDocument, irDocument); // Assert Children(irDocument, @@ -167,41 +142,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution node => CSharpStatement("});", node)); } - [Fact] - public void Execute_Custom_RemovesDirectiveIRNodeFromIRDocument() - { - // Arrange - var content = "@custom Hello"; - var sourceDocument = TestRazorSourceDocument.Create(content); - var codeDocument = RazorCodeDocument.Create(sourceDocument); - var defaultEngine = RazorEngine.Create(b => - { - var customDirective = DirectiveDescriptorBuilder.Create("custom").AddString().Build(); - b.AddDirective(customDirective); - }); - var originalIRDocument = Lower(codeDocument, defaultEngine); - var pass = new DefaultDirectiveIRPass() - { - Engine = defaultEngine, - }; - - // Act - var irDocument = pass.Execute(codeDocument, originalIRDocument); - - // Assert - Children(irDocument, - node => Assert.IsType(node), - node => Assert.IsType(node)); - var @namespace = irDocument.Children[1]; - Children(@namespace, - node => Assert.IsType(node), - node => Assert.IsType(node), - node => Assert.IsType(node)); - var @class = @namespace.Children[2]; - var method = SingleChild(@class); - Assert.Empty(method.Children); - } - private static DocumentIRNode Lower(RazorCodeDocument codeDocument) { var engine = RazorEngine.Create(); @@ -216,7 +156,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var phase = engine.Phases[i]; phase.Execute(codeDocument); - if (phase is IRazorIRLoweringPhase) + if (phase is IRazorDocumentClassifierPhase) { break; } @@ -225,8 +165,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution var irDocument = codeDocument.GetIRDocument(); Assert.NotNull(irDocument); - // These tests depend on the document->namespace->class structure. - irDocument = new DefaultDocumentClassifierPass() { Engine = engine, }.Execute(codeDocument, irDocument); return irDocument; } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultInstrumentationPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultInstrumentationPassTest.cs index ecf04b2d1c..22b70a077a 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultInstrumentationPassTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultInstrumentationPassTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var pass = new DefaultInstrumentationPass(); // Act - var result = pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); + pass.ExecuteCore(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert Children( diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDirectiveClassifierPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDirectiveClassifierPhaseTest.cs new file mode 100644 index 0000000000..f518cd527f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDirectiveClassifierPhaseTest.cs @@ -0,0 +1,102 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorDirectiveClassifierPhaseTest + { + [Fact] + public void OnInitialized_OrdersPassesInAscendingOrder() + { + // Arrange & Act + var phase = new DefaultRazorDirectiveClassifierPhase(); + + var first = Mock.Of(p => p.Order == 15); + var second = Mock.Of(p => p.Order == 17); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(second); + b.Features.Add(first); + }); + + // Assert + Assert.Collection( + phase.Passes, + p => Assert.Same(first, p), + p => Assert.Same(second, p)); + } + + [Fact] + public void Execute_ThrowsForMissingDependency() + { + // Arrange + var phase = new DefaultRazorDirectiveClassifierPhase(); + + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act & Assert + ExceptionAssert.Throws( + () => phase.Execute(codeDocument), + $"The '{nameof(DefaultRazorDirectiveClassifierPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + + $"provided by the '{nameof(RazorCodeDocument)}'."); + } + + [Fact] + public void Execute_ExecutesPhasesInOrder() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // We're going to set up mocks to simulate a sequence of passes. We don't care about + // what's in the nodes, we're just going to look at the identity via strict mocks. + var originalNode = new DocumentIRNode(); + var firstPassNode = new DocumentIRNode(); + var secondPassNode = new DocumentIRNode(); + codeDocument.SetIRDocument(originalNode); + + var firstPass = new Mock(MockBehavior.Strict); + firstPass.SetupGet(m => m.Order).Returns(0); + firstPass.SetupProperty(m => m.Engine); + firstPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + originalNode.Children.Add(firstPassNode); + }); + + var secondPass = new Mock(MockBehavior.Strict); + secondPass.SetupGet(m => m.Order).Returns(1); + secondPass.SetupProperty(m => m.Engine); + secondPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + // Works only when the first pass has run before this. + originalNode.Children[0].Children.Add(secondPassNode); + }); + + var phase = new DefaultRazorDirectiveClassifierPhase(); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(firstPass.Object); + b.Features.Add(secondPass.Object); + }); + + // Act + phase.Execute(codeDocument); + + // Assert + Assert.Same(secondPassNode, codeDocument.GetIRDocument().Children[0].Children[0]); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDocumentClassifierPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDocumentClassifierPhaseTest.cs new file mode 100644 index 0000000000..c3a5f46ae8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorDocumentClassifierPhaseTest.cs @@ -0,0 +1,102 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorDocumentClassifierPhaseTest + { + [Fact] + public void OnInitialized_OrdersPassesInAscendingOrder() + { + // Arrange & Act + var phase = new DefaultRazorDocumentClassifierPhase(); + + var first = Mock.Of(p => p.Order == 15); + var second = Mock.Of(p => p.Order == 17); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(second); + b.Features.Add(first); + }); + + // Assert + Assert.Collection( + phase.Passes, + p => Assert.Same(first, p), + p => Assert.Same(second, p)); + } + + [Fact] + public void Execute_ThrowsForMissingDependency() + { + // Arrange + var phase = new DefaultRazorDocumentClassifierPhase(); + + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act & Assert + ExceptionAssert.Throws( + () => phase.Execute(codeDocument), + $"The '{nameof(DefaultRazorDocumentClassifierPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + + $"provided by the '{nameof(RazorCodeDocument)}'."); + } + + [Fact] + public void Execute_ExecutesPhasesInOrder() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // We're going to set up mocks to simulate a sequence of passes. We don't care about + // what's in the nodes, we're just going to look at the identity via strict mocks. + var originalNode = new DocumentIRNode(); + var firstPassNode = new DocumentIRNode(); + var secondPassNode = new DocumentIRNode(); + codeDocument.SetIRDocument(originalNode); + + var firstPass = new Mock(MockBehavior.Strict); + firstPass.SetupGet(m => m.Order).Returns(0); + firstPass.SetupProperty(m => m.Engine); + firstPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + originalNode.Children.Add(firstPassNode); + }); + + var secondPass = new Mock(MockBehavior.Strict); + secondPass.SetupGet(m => m.Order).Returns(1); + secondPass.SetupProperty(m => m.Engine); + secondPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + // Works only when the first pass has run before this. + originalNode.Children[0].Children.Add(secondPassNode); + }); + + var phase = new DefaultRazorDocumentClassifierPhase(); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(firstPass.Object); + b.Features.Add(secondPass.Object); + }); + + // Act + phase.Execute(codeDocument); + + // Assert + Assert.Same(secondPassNode, codeDocument.GetIRDocument().Children[0].Children[0]); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIROptimizationPhaseTest.cs similarity index 70% rename from test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs rename to test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIROptimizationPhaseTest.cs index cacd605b62..5c21d05535 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIROptimizationPhaseTest.cs @@ -9,16 +9,16 @@ using Xunit; namespace Microsoft.AspNetCore.Razor.Evolution { - public class DefaultRazorIRPhaseTest + public class DefaultRazorIROptimizationPhaseTest { [Fact] public void OnInitialized_OrdersPassesInAscendingOrder() { // Arrange & Act - var phase = new DefaultRazorIRPhase(); + var phase = new DefaultRazorIROptimizationPhase(); - var first = Mock.Of(p => p.Order == 15); - var second = Mock.Of(p => p.Order == 17); + var first = Mock.Of(p => p.Order == 15); + var second = Mock.Of(p => p.Order == 17); var engine = RazorEngine.CreateEmpty(b => { @@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution public void Execute_ThrowsForMissingDependency() { // Arrange - var phase = new DefaultRazorIRPhase(); + var phase = new DefaultRazorIROptimizationPhase(); var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution // Act & Assert ExceptionAssert.Throws( () => phase.Execute(codeDocument), - $"The '{nameof(DefaultRazorIRPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + + $"The '{nameof(DefaultRazorIROptimizationPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + $"provided by the '{nameof(RazorCodeDocument)}'."); } @@ -65,17 +65,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution var secondPassNode = new DocumentIRNode(); codeDocument.SetIRDocument(originalNode); - var firstPass = new Mock(MockBehavior.Strict); + var firstPass = new Mock(MockBehavior.Strict); firstPass.SetupGet(m => m.Order).Returns(0); firstPass.SetupProperty(m => m.Engine); - firstPass.Setup(m => m.Execute(codeDocument, originalNode)).Returns(firstPassNode); + firstPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + originalNode.Children.Add(firstPassNode); + }); - var secondPass = new Mock(MockBehavior.Strict); + var secondPass = new Mock(MockBehavior.Strict); secondPass.SetupGet(m => m.Order).Returns(1); secondPass.SetupProperty(m => m.Engine); - secondPass.Setup(m => m.Execute(codeDocument, firstPassNode)).Returns(secondPassNode); + secondPass.Setup(m => m.Execute(codeDocument, originalNode)).Callback(() => + { + // Works only when the first pass has run before this. + originalNode.Children[0].Children.Add(secondPassNode); + }); - var phase = new DefaultRazorIRPhase(); + var phase = new DefaultRazorIROptimizationPhase(); var engine = RazorEngine.CreateEmpty(b => { @@ -89,7 +96,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution phase.Execute(codeDocument); // Assert - Assert.Same(secondPassNode, codeDocument.GetIRDocument()); + Assert.Same(secondPassNode, codeDocument.GetIRDocument().Children[0].Children[0]); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveRemovalIROptimizationPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveRemovalIROptimizationPassTest.cs new file mode 100644 index 0000000000..d8efdb5879 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveRemovalIROptimizationPassTest.cs @@ -0,0 +1,102 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Xunit; +using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DirectiveRemovalIROptimizationPassTest + { + [Fact] + public void Execute_Custom_RemovesDirectiveIRNodeFromIRDocument() + { + // Arrange + var content = "@custom Hello"; + var sourceDocument = TestRazorSourceDocument.Create(content); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var defaultEngine = RazorEngine.Create(b => + { + var customDirective = DirectiveDescriptorBuilder.Create("custom").AddString().Build(); + b.AddDirective(customDirective); + }); + var irDocument = Lower(codeDocument, defaultEngine); + var pass = new DirectiveRemovalIROptimizationPass() + { + Engine = defaultEngine, + }; + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + Children(irDocument, + node => Assert.IsType(node), + node => Assert.IsType(node)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + node => Assert.IsType(node), + node => Assert.IsType(node), + node => Assert.IsType(node)); + var @class = @namespace.Children[2]; + var method = SingleChild(@class); + Assert.Empty(method.Children); + } + + [Fact] + public void Execute_MultipleCustomDirectives_RemovesDirectiveIRNodesFromIRDocument() + { + // Arrange + var content = "@custom Hello" + Environment.NewLine + "@custom World"; + var sourceDocument = TestRazorSourceDocument.Create(content); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var defaultEngine = RazorEngine.Create(b => + { + var customDirective = DirectiveDescriptorBuilder.Create("custom").AddString().Build(); + b.AddDirective(customDirective); + }); + var irDocument = Lower(codeDocument, defaultEngine); + var pass = new DirectiveRemovalIROptimizationPass() + { + Engine = defaultEngine, + }; + + // Act + pass.Execute(codeDocument, irDocument); + + // Assert + Children(irDocument, + node => Assert.IsType(node), + node => Assert.IsType(node)); + var @namespace = irDocument.Children[1]; + Children(@namespace, + node => Assert.IsType(node), + node => Assert.IsType(node), + node => Assert.IsType(node)); + var @class = @namespace.Children[2]; + var method = SingleChild(@class); + Assert.Empty(method.Children); + } + + private static DocumentIRNode Lower(RazorCodeDocument codeDocument, RazorEngine engine) + { + for (var i = 0; i < engine.Phases.Count; i++) + { + var phase = engine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorDirectiveClassifierPhase) + { + break; + } + } + + var irDocument = codeDocument.GetIRDocument(); + Assert.NotNull(irDocument); + + return irDocument; + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs index 7db437817b..3fcd234496 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs @@ -224,6 +224,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution private class TestDocumentClassifierPass : DocumentClassifierPassBase { + public override int Order => RazorIRPass.DefaultFeatureOrder; + public bool ShouldMatch { get; set; } = true; public string Namespace { get; set; } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs index 38340b8f5e..3003af19b4 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -1530,16 +1530,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests AssertDesignTimeDocumentMatchBaseline(document); } - private class ApiSetsIRTestAdapter : RazorIRPassBase + private class ApiSetsIRTestAdapter : RazorIRPassBase, IRazorIROptimizationPass { - public override int Order => RazorIRPass.LoweringOrder; - - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new ApiSetsIRWalker(); walker.Visit(irDocument); - - return irDocument; } private class ApiSetsIRWalker : RazorIRNodeWalker diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index e02d6bdaa4..433b502755 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -141,6 +141,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature)); } @@ -151,7 +152,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution phase => Assert.IsType(phase), phase => Assert.IsType(phase), phase => Assert.IsType(phase), - phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), phase => Assert.IsType(phase)); } @@ -164,6 +167,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature)); } @@ -175,7 +179,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution phase => Assert.IsType(phase), phase => Assert.IsType(phase), phase => Assert.IsType(phase), - phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), + phase => Assert.IsType(phase), phase => Assert.IsType(phase)); } }