Add TagHelper binding abstractions.

- Added a TagHelperFeature to hold TagHelper specific pieces that can be replaced.
- Built a syntax tree pass that applies the ported TagHelper bits.
- Updated tests that expected different RazorEngine defaults.
- Added new tests to verify binder pass.
This commit is contained in:
N. Taylor Mullen 2016-11-14 15:34:27 -08:00
parent 26a1cf3cff
commit 6c8ef157b4
6 changed files with 349 additions and 8 deletions

View File

@ -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<IRazorEngineFeature> Features { get; }

View File

@ -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<TagHelperFeature>().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<RazorError> CombineErrors(IReadOnlyList<RazorError> errors1, IReadOnlyList<RazorError> errors2)
{
var combinedErrors = new List<RazorError>(errors1.Count + errors2.Count);
combinedErrors.AddRange(errors1);
combinedErrors.AddRange(errors2);
return combinedErrors;
}
}
}

View File

@ -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; }
}
}

View File

@ -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
{

View File

@ -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<IRazorEngineFeature> features)
{
Assert.Empty(features);
}
private static void AssertDefaultPhases(IReadOnlyList<IRazorEnginePhase> features)
{
Assert.Collection(
features,
f => Assert.IsType<DefaultRazorParsingPhase>(f),
f => Assert.IsType<DefaultRazorSyntaxTreePhase>(f));
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature));
}
private static void AssertDefaultPhases(IReadOnlyList<IRazorEnginePhase> phases)
{
Assert.Collection(
phases,
phase => Assert.IsType<DefaultRazorParsingPhase>(phase),
phase => Assert.IsType<DefaultRazorSyntaxTreePhase>(phase));
}
}
}

View File

@ -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<TagHelperBlock>(rewrittenTree.Root.Children[2]);
Assert.Equal("form", formTagHelper.TagName);
Assert.Equal(3, formTagHelper.Children.Count);
var inputTagHelper = Assert.IsType<TagHelperBlock>(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<TagHelperDescriptor>());
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
<form>
<input value='Hello' type='text' />";
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
<form>
<input value='Hello' type='text' />
</form>";
var sourceDocument = TestRazorSourceDocument.Create(content);
return sourceDocument;
}
private class TestTagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
private readonly IEnumerable<TagHelperDescriptor> _descriptors;
public TestTagHelperDescriptorResolver(IEnumerable<TagHelperDescriptor> descriptors)
{
_descriptors = descriptors;
}
public IEnumerable<TagHelperDescriptor> Resolve(TagHelperDescriptorResolutionContext resolutionContext)
{
return _descriptors;
}
}
private class ErrorLoggingTagHelperDescriptorResolver : ITagHelperDescriptorResolver
{
private readonly RazorError _error;
public ErrorLoggingTagHelperDescriptorResolver(RazorError error)
{
_error = error;
}
public IEnumerable<TagHelperDescriptor> Resolve(TagHelperDescriptorResolutionContext resolutionContext)
{
resolutionContext.ErrorSink.OnError(_error);
return Enumerable.Empty<TagHelperDescriptor>();
}
}
}
}