diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index 518467911d..5891743730 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -32,6 +32,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution { builder.Phases.Add(new DefaultRazorParsingPhase()); builder.Phases.Add(new DefaultRazorSyntaxTreePhase()); + + builder.Features.Add(new TagHelperBinderSyntaxTreePass()); } public abstract IReadOnlyList Features { get; } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs new file mode 100644 index 0000000000..fcfd115a79 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperBinderSyntaxTreePass.cs @@ -0,0 +1,65 @@ +// 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 System.Linq; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class TagHelperBinderSyntaxTreePass : IRazorSyntaxTreePass + { + public RazorEngine Engine { get; set; } + + public int Order => 100; + + public RazorSyntaxTree Execute(RazorCodeDocument document, RazorSyntaxTree syntaxTree) + { + var resolver = Engine.Features.OfType().FirstOrDefault()?.Resolver; + if (resolver == null) + { + // No resolver, nothing to do. + return syntaxTree; + } + + var errorSink = new ErrorSink(); + var visitor = new TagHelperDirectiveSpanVisitor(resolver, errorSink); + var descriptors = visitor.GetDescriptors(syntaxTree.Root); + + if (!descriptors.Any()) + { + if (errorSink.Errors.Count > 0) + { + var combinedErrors = CombineErrors(syntaxTree.Diagnostics, errorSink.Errors); + var erroredTree = RazorSyntaxTree.Create(syntaxTree.Root, combinedErrors); + + return erroredTree; + } + + return syntaxTree; + } + + var descriptorProvider = new TagHelperDescriptorProvider(descriptors); + var rewriter = new TagHelperParseTreeRewriter(descriptorProvider); + var rewrittenRoot = rewriter.Rewrite(syntaxTree.Root, errorSink); + var diagnostics = syntaxTree.Diagnostics; + + if (errorSink.Errors.Count > 0) + { + diagnostics = CombineErrors(diagnostics, errorSink.Errors); + } + + var newSyntaxTree = RazorSyntaxTree.Create(rewrittenRoot, diagnostics); + return newSyntaxTree; + } + + private IReadOnlyList CombineErrors(IReadOnlyList errors1, IReadOnlyList errors2) + { + var combinedErrors = new List(errors1.Count + errors2.Count); + combinedErrors.AddRange(errors1); + combinedErrors.AddRange(errors2); + + return combinedErrors; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperFeature.cs new file mode 100644 index 0000000000..363a74f731 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/TagHelperFeature.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.Legacy; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class TagHelperFeature : IRazorEngineFeature + { + public TagHelperFeature(ITagHelperDescriptorResolver resolver) + { + Resolver = resolver; + } + + public RazorEngine Engine { get; set; } + + public ITagHelperDescriptorResolver Resolver { get; } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs index 063def7d7a..ece3052388 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TagHelperRewritingTestBase.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using Microsoft.AspNetCore.Razor.Evolution.TagHelpers; +using Microsoft.AspNetCore.Razor.Evolution; namespace Microsoft.AspNetCore.Razor.Evolution.Legacy { diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 5afa17ae3f..82e33ec997 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Razor.Evolution; using Moq; using Xunit; @@ -72,16 +73,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution } private static void AssertDefaultFeatures(IEnumerable features) - { - Assert.Empty(features); - } - - private static void AssertDefaultPhases(IReadOnlyList features) { Assert.Collection( features, - f => Assert.IsType(f), - f => Assert.IsType(f)); + feature => Assert.IsType(feature)); + } + + private static void AssertDefaultPhases(IReadOnlyList phases) + { + Assert.Collection( + phases, + phase => Assert.IsType(phase), + phase => Assert.IsType(phase)); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs new file mode 100644 index 0000000000..54ff46c2ee --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TagHelperBinderSyntaxTreePassTest.cs @@ -0,0 +1,252 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class TagHelperBinderSyntaxTreePassTest + { + [Fact] + public void Execute_RewritesTagHelpers() + { + // Arrange + var engine = RazorEngine.Create(builder => + { + var descriptors = new[] + { + new TagHelperDescriptor + { + TagName = "form", + }, + new TagHelperDescriptor + { + TagName = "input", + } + }; + var resolver = new TestTagHelperDescriptorResolver(descriptors); + var tagHelperFeature = new TagHelperFeature(resolver); + builder.Features.Add(tagHelperFeature); + }); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + + // Act + var rewrittenTree = pass.Execute(codeDocument, originalTree); + + // Assert + Assert.Empty(rewrittenTree.Diagnostics); + Assert.Equal(3, rewrittenTree.Root.Children.Count); + var formTagHelper = Assert.IsType(rewrittenTree.Root.Children[2]); + Assert.Equal("form", formTagHelper.TagName); + Assert.Equal(3, formTagHelper.Children.Count); + var inputTagHelper = Assert.IsType(formTagHelper.Children[1]); + Assert.Equal("input", inputTagHelper.TagName); + } + + [Fact] + public void Execute_NoopsWhenNoTagHelperFeature() + { + // Arrange + var engine = RazorEngine.Create(); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + + // Act + var outputTree = pass.Execute(codeDocument, originalTree); + + // Assert + Assert.Empty(outputTree.Diagnostics); + Assert.Same(originalTree, outputTree); + } + + [Fact] + public void Execute_NoopsWhenNoResolver() + { + // Arrange + var engine = RazorEngine.Create(builder => + { + + var tagHelperFeature = new TagHelperFeature(resolver: null); + builder.Features.Add(tagHelperFeature); + }); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + + // Act + var outputTree = pass.Execute(codeDocument, originalTree); + + // Assert + Assert.Empty(outputTree.Diagnostics); + Assert.Same(originalTree, outputTree); + } + + [Fact] + public void Execute_NoopsWhenNoTagHelperDescriptorsAreResolved() + { + // Arrange + var engine = RazorEngine.Create(builder => + { + var resolver = new TestTagHelperDescriptorResolver(descriptors: Enumerable.Empty()); + var tagHelperFeature = new TagHelperFeature(resolver); + builder.Features.Add(tagHelperFeature); + }); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + + // Act + var outputTree = pass.Execute(codeDocument, originalTree); + + // Assert + Assert.Empty(outputTree.Diagnostics); + Assert.Same(originalTree, outputTree); + } + + [Fact] + public void Execute_RecreatesSyntaxTreeOnResolverErrors() + { + // Arrange + var resolverError = new RazorError("Test error", new SourceLocation(19, 1, 17), length: 12); + var engine = RazorEngine.Create(builder => + { + var resolver = new ErrorLoggingTagHelperDescriptorResolver(resolverError); + var tagHelperFeature = new TagHelperFeature(resolver); + builder.Features.Add(tagHelperFeature); + }); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var sourceDocument = CreateTestSourceDocument(); + 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 }); + + // Act + var outputTree = pass.Execute(codeDocument, erroredOriginalTree); + + // Assert + Assert.Empty(originalTree.Diagnostics); + Assert.NotSame(erroredOriginalTree, outputTree); + Assert.Equal(new[] { initialError, resolverError }, outputTree.Diagnostics); + } + + [Fact] + public void Execute_CombinesErrorsOnRewritingErrors() + { + // Arrange + var engine = RazorEngine.Create(builder => + { + var descriptors = new[] + { + new TagHelperDescriptor + { + TagName = "form", + }, + new TagHelperDescriptor + { + TagName = "input", + } + }; + var resolver = new TestTagHelperDescriptorResolver(descriptors); + var tagHelperFeature = new TagHelperFeature(resolver); + builder.Features.Add(tagHelperFeature); + }); + var pass = new TagHelperBinderSyntaxTreePass() + { + Engine = engine, + }; + var content = + @" +@addTagHelper *, TestAssembly +
+ "; + var sourceDocument = TestRazorSourceDocument.Create(content); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + var initialError = new RazorError("Initial test error", SourceLocation.Zero, length: 1); + var expectedRewritingError = new RazorError( + LegacyResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper("form"), + new SourceLocation(Environment.NewLine.Length + 32, 2, 0), + length: 4); + var erroredOriginalTree = RazorSyntaxTree.Create(originalTree.Root, new[] { initialError }); + + // Act + var outputTree = pass.Execute(codeDocument, erroredOriginalTree); + + // Assert + Assert.Empty(originalTree.Diagnostics); + Assert.NotSame(erroredOriginalTree, outputTree); + Assert.Equal(new[] { initialError, expectedRewritingError }, outputTree.Diagnostics); + } + + private static RazorSourceDocument CreateTestSourceDocument() + { + var content = + @" +@addTagHelper *, TestAssembly + + +
"; + var sourceDocument = TestRazorSourceDocument.Create(content); + return sourceDocument; + } + + private class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + private readonly IEnumerable _descriptors; + + public TestTagHelperDescriptorResolver(IEnumerable descriptors) + { + _descriptors = descriptors; + } + + public IEnumerable Resolve(TagHelperDescriptorResolutionContext resolutionContext) + { + return _descriptors; + } + } + + private class ErrorLoggingTagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + private readonly RazorError _error; + + public ErrorLoggingTagHelperDescriptorResolver(RazorError error) + { + _error = error; + } + + public IEnumerable Resolve(TagHelperDescriptorResolutionContext resolutionContext) + { + resolutionContext.ErrorSink.OnError(_error); + + return Enumerable.Empty(); + } + } + } +}