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 DefaultRazorParsingPhase());
|
||||||
builder.Phases.Add(new DefaultRazorSyntaxTreePhase());
|
builder.Phases.Add(new DefaultRazorSyntaxTreePhase());
|
||||||
|
|
||||||
|
builder.Features.Add(new TagHelperBinderSyntaxTreePass());
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract IReadOnlyList<IRazorEngineFeature> Features { get; }
|
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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Razor.Evolution.TagHelpers;
|
using Microsoft.AspNetCore.Razor.Evolution;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Razor.Evolution;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -72,16 +73,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertDefaultFeatures(IEnumerable<IRazorEngineFeature> features)
|
private static void AssertDefaultFeatures(IEnumerable<IRazorEngineFeature> features)
|
||||||
{
|
|
||||||
Assert.Empty(features);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertDefaultPhases(IReadOnlyList<IRazorEnginePhase> features)
|
|
||||||
{
|
{
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
features,
|
features,
|
||||||
f => Assert.IsType<DefaultRazorParsingPhase>(f),
|
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature));
|
||||||
f => Assert.IsType<DefaultRazorSyntaxTreePhase>(f));
|
}
|
||||||
|
|
||||||
|
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