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:
parent
26a1cf3cff
commit
6c8ef157b4
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue