From ea778b9b6d72d8223c8bc6815476e213299b0588 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 9 Feb 2017 15:22:33 -0800 Subject: [PATCH] Implement a simple base for document classifiers This should allow us to de-dupe a lot of code in MVC. --- .../DefaultDocumentClassifierPass.cs | 19 ++++ ...ifier.cs => DocumentClassifierPassBase.cs} | 42 +++++-- .../RazorEngine.cs | 2 +- .../DefaultDirectiveIRPassTest.cs | 2 +- .../DefaultDocumentClassifierPassTest.cs | 56 ++++++++++ ...t.cs => DocumentClassifierPassBaseTest.cs} | 104 ++++++++++++++++-- .../RazorEngineTest.cs | 4 +- 7 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs rename src/Microsoft.AspNetCore.Razor.Evolution/{DefaultDocumentClassifier.cs => DocumentClassifierPassBase.cs} (67%) create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierPassTest.cs rename test/Microsoft.AspNetCore.Razor.Evolution.Test/{DefaultDocumentClassifierTest.cs => DocumentClassifierPassBaseTest.cs} (57%) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs new file mode 100644 index 0000000000..ccaac151c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifierPass.cs @@ -0,0 +1,19 @@ +// 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 +{ + internal class DefaultDocumentClassifierPass : DocumentClassifierPassBase + { + public override int Order => RazorIRPass.DefaultDocumentClassifierOrder; + + protected override string DocumentKind => "default"; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs similarity index 67% rename from src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs rename to src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs index c8655caf65..f95cd4a03e 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs @@ -6,22 +6,35 @@ using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { - internal class DefaultDocumentClassifier : RazorIRPassBase + public abstract class DocumentClassifierPassBase : RazorIRPassBase { - public override int Order => RazorIRPass.DefaultDocumentClassifierOrder; + protected abstract string DocumentKind { get; } - public static string DocumentKind = "default"; + public override int Order => RazorIRPass.DocumentClassifierOrder; - public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + public sealed override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { if (irDocument.DocumentKind != null) { return irDocument; } + if (!IsMatch(codeDocument, irDocument)) + { + return irDocument; + } + irDocument.DocumentKind = DocumentKind; - // Rewrite a use default namespace and class declaration. + Rewrite(codeDocument, irDocument); + + return irDocument; + } + + private void Rewrite(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + // Rewrite the document from a flat structure to use a sensible default structure, + // a namespace and class declaration with a single 'razor' method. var children = new List(irDocument.Children); irDocument.Children.Clear(); @@ -39,7 +52,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var method = new RazorMethodDeclarationIRNode() { //AccessModifier = "public", - // Modifiers = new List() { "async" }, + // Modifiers = new List() { "async" }, //Name = "Execute", //ReturnType = "Task", }; @@ -62,7 +75,20 @@ namespace Microsoft.AspNetCore.Razor.Evolution visitor.Visit(children[i]); } - return irDocument; + // Note that this is called at the *end* of rewriting so that user code can see the tree + // and look at its content to make a decision. + OnDocumentStructureCreated(codeDocument, @namespace, @class, method); + } + + protected abstract bool IsMatch(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + + protected virtual void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIRNode @namespace, + ClassDeclarationIRNode @class, + RazorMethodDeclarationIRNode @method) + { + // Intentionally empty. } private class Visitor : RazorIRNodeVisitor @@ -84,7 +110,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { _document.Insert(0, node); } - + public override void VisitUsingStatement(UsingStatementIRNode node) { _namespace.AddAfter(node); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index de2804d5a9..31f3673757 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Features.Add(new TagHelperBinderSyntaxTreePass()); // IR Passes - builder.Features.Add(new DefaultDocumentClassifier()); + builder.Features.Add(new DefaultDocumentClassifierPass()); builder.Features.Add(new DefaultDirectiveIRPass()); } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs index 936ed291ab..4348741a63 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs @@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Assert.NotNull(irDocument); // These tests depend on the document->namespace->class structure. - irDocument = new DefaultDocumentClassifier() { Engine = engine, }.Execute(codeDocument, irDocument); + irDocument = new DefaultDocumentClassifierPass() { Engine = engine, }.Execute(codeDocument, irDocument); return irDocument; } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierPassTest.cs new file mode 100644 index 0000000000..c3eaf3f2b6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierPassTest.cs @@ -0,0 +1,56 @@ +// 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 Xunit; +using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + // We're purposely lean on tests here because the functionality is well covered by + // integration tests, and is mostly implemented by the base class. + public class DefaultDocumentClassifierPassTest + { + [Fact] + public void Execute_IgnoresDocumentsWithDocumentKind() + { + // Arrange + var irDocument = new DocumentIRNode() + { + DocumentKind = "ignore", + }; + + var pass = new DefaultDocumentClassifierPass(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal("ignore", irDocument.DocumentKind); + NoChildren(irDocument); + } + + [Fact] + public void Execute_CreatesClassStructure() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var pass = new DefaultDocumentClassifierPass(); + pass.Engine = RazorEngine.CreateEmpty(b =>{ }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal("default", irDocument.DocumentKind); + + var @namespace = SingleChild(irDocument); + var @class = SingleChild(@namespace); + var method = SingleChild(@class); + NoChildren(method); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs similarity index 57% rename from test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs rename to test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs index ceb146320b..459b9f7eb7 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs @@ -2,16 +2,17 @@ // 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 DefaultDocumentClassifierTest + public class DocumentClassifierPassBaseTest { [Fact] - public void Execute_IgnoresDocumentsWithDocumentKind() + public void Execute_HasDocumentKind_IgnoresDocument() { // Arrange var irDocument = new DocumentIRNode() @@ -19,7 +20,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution DocumentKind = "ignore", }; - var pass = new DefaultDocumentClassifier(); + var pass = new TestDocumentClassifierPass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act @@ -31,19 +32,39 @@ namespace Microsoft.AspNetCore.Razor.Evolution } [Fact] - public void Execute_CreatesClassStructure() + public void Execute_NoMatch_IgnoresDocument() { // Arrange var irDocument = new DocumentIRNode(); - var pass = new DefaultDocumentClassifier(); - pass.Engine = RazorEngine.CreateEmpty(b =>{ }); + var pass = new TestDocumentClassifierPass() + { + Engine = RazorEngine.CreateEmpty(b => { }), + ShouldMatch = false, + }; // Act pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); // Assert - Assert.Equal(DefaultDocumentClassifier.DocumentKind, irDocument.DocumentKind); + Assert.Null(irDocument.DocumentKind); + NoChildren(irDocument); + } + + [Fact] + public void Execute_Match_SetsDocumentType_AndCreatesStructure() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var pass = new TestDocumentClassifierPass(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal("test", irDocument.DocumentKind); var @namespace = SingleChild(irDocument); var @class = SingleChild(@namespace); @@ -60,7 +81,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var builder = RazorIRBuilder.Create(irDocument); builder.Add(new ChecksumIRNode()); - var pass = new DefaultDocumentClassifier(); + var pass = new TestDocumentClassifierPass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act @@ -82,7 +103,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var builder = RazorIRBuilder.Create(irDocument); builder.Add(new UsingStatementIRNode()); - var pass = new DefaultDocumentClassifier(); + var pass = new TestDocumentClassifierPass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act @@ -105,7 +126,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution var builder = RazorIRBuilder.Create(irDocument); builder.Add(new DeclareTagHelperFieldsIRNode()); - var pass = new DefaultDocumentClassifier(); + var pass = new TestDocumentClassifierPass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act @@ -130,7 +151,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Add(new HtmlContentIRNode()); builder.Add(new CSharpStatementIRNode()); - var pass = new DefaultDocumentClassifier(); + var pass = new TestDocumentClassifierPass(); pass.Engine = RazorEngine.CreateEmpty(b => { }); // Act @@ -145,5 +166,66 @@ namespace Microsoft.AspNetCore.Razor.Evolution n => Assert.IsType(n), n => Assert.IsType(n)); } + + [Fact] + public void Execute_CanInitializeDefaults() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var builder = RazorIRBuilder.Create(irDocument); + builder.Add(new HtmlContentIRNode()); + builder.Add(new CSharpStatementIRNode()); + + var pass = new TestDocumentClassifierPass() + { + Engine = RazorEngine.CreateEmpty(b => { }), + Namespace = "TestNamespace", + Class = "TestClass", + Method = "TestMethod", + }; + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + var @namespace = SingleChild(irDocument); + Assert.Equal("TestNamespace", @namespace.Content); + + var @class = SingleChild(@namespace); + Assert.Equal("TestClass", @class.Name); + + var method = SingleChild(@class); + Assert.Equal("TestMethod", method.Name); + } + + private class TestDocumentClassifierPass : DocumentClassifierPassBase + { + public bool ShouldMatch { get; set; } = true; + + public string Namespace { get; set; } + + public string Class { get; set; } + + public string Method { get; set; } + + protected override string DocumentKind => "test"; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + return ShouldMatch; + } + + protected override void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIRNode @namespace, + ClassDeclarationIRNode @class, + RazorMethodDeclarationIRNode method) + { + @namespace.Content = Namespace; + @class.Name = Class; + @method.Name = Method; + } + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 15cf61467f..6dd8a800d7 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -139,7 +139,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), feature => Assert.IsType(feature)); } @@ -162,7 +162,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), feature => Assert.IsType(feature), feature => Assert.IsType(feature));