Add support for `@removeTagHelpers` inheritance from _ViewStart files

Fixes #1657
This commit is contained in:
Pranav K 2014-12-01 15:24:10 -08:00
parent 106b9fc30c
commit 6641997836
10 changed files with 275 additions and 69 deletions

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
@ -20,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
private readonly Dictionary<string, CodeTree> _parsedCodeTrees;
private readonly MvcRazorHost _razorHost;
private readonly IFileSystem _fileSystem;
private readonly IEnumerable<Chunk> _defaultInheritedChunks;
private readonly IReadOnlyList<Chunk> _defaultInheritedChunks;
/// <summary>
/// Initializes a new instance of <see cref="ChunkInheritanceUtility"/>.
@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
/// <param name="defaultInheritedChunks">Sequence of <see cref="Chunk"/>s inherited by default.</param>
public ChunkInheritanceUtility([NotNull] MvcRazorHost razorHost,
[NotNull] IFileSystem fileSystem,
[NotNull] IEnumerable<Chunk> defaultInheritedChunks)
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks)
{
_razorHost = razorHost;
_fileSystem = fileSystem;
@ -39,14 +40,16 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
}
/// <summary>
/// Gets a <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/> containing parsed results of _ViewStart files
/// that are used for inheriting tag helpers and chunks to the page located at <paramref name="pagePath"/>.
/// Gets an ordered <see cref="IReadOnlyList{T}"/> of parsed <see cref="CodeTree"/> for each _ViewStart that
/// is applicable to the page located at <paramref name="pagePath"/>. The list is ordered so that the
/// <see cref="CodeTree"/> for the _ViewStart closest to the <paramref name="pagePath"/> in the filesystem
/// appears first.
/// </summary>
/// <param name="pagePath">The path of the page to locate inherited chunks for.</param>
/// <returns>A <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/> from _ViewStart pages.</returns>
public IReadOnlyList<Chunk> GetInheritedChunks([NotNull] string pagePath)
/// <returns>A <see cref="IReadOnlyList{CodeTree}"/> of parsed _ViewStart <see cref="CodeTree"/>s.</returns>
public IReadOnlyList<CodeTree> GetInheritedCodeTrees([NotNull] string pagePath)
{
var inheritedChunks = new List<Chunk>();
var inheritedCodeTrees = new List<CodeTree>();
var templateEngine = new RazorTemplateEngine(_razorHost);
foreach (var viewStartPath in ViewStartUtility.GetViewStartLocations(pagePath))
@ -55,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
if (_parsedCodeTrees.TryGetValue(viewStartPath, out codeTree))
{
inheritedChunks.AddRange(codeTree.Chunks);
inheritedCodeTrees.Add(codeTree);
}
else
{
@ -68,25 +71,26 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// for the current _ViewStart to succeed.
codeTree = ParseViewFile(templateEngine, fileInfo, viewStartPath);
_parsedCodeTrees.Add(viewStartPath, codeTree);
inheritedChunks.AddRange(codeTree.Chunks);
inheritedCodeTrees.Add(codeTree);
}
}
}
inheritedChunks.AddRange(_defaultInheritedChunks);
return inheritedChunks;
return inheritedCodeTrees;
}
/// <summary>
/// Merges a list of chunks into the specified <paramref name="codeTree"/>.
/// Merges <see cref="Chunk"/> inherited by default and <see cref="CodeTree"/> instances produced by parsing
/// _ViewStart files into the specified <paramref name="codeTree"/>.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to merge.</param>
/// <param name="inherited">The <see credit="IReadOnlyList{T}"/> of <see cref="Chunk"/> to merge.</param>
/// <param name="codeTree">The <see cref="CodeTree"/> to merge in to.</param>
/// <param name="inheritedCodeTrees"><see cref="IReadOnlyList{CodeTree}"/> inherited from _ViewStart
/// files.</param>
/// <param name="defaultModel">The list of chunks to merge.</param>
public void MergeInheritedChunks([NotNull] CodeTree codeTree,
[NotNull] IReadOnlyList<Chunk> inherited,
string defaultModel)
public void MergeInheritedCodeTrees([NotNull] CodeTree codeTree,
[NotNull] IReadOnlyList<CodeTree> inheritedCodeTrees,
string defaultModel)
{
var mergerMappings = GetMergerMappings(codeTree, defaultModel);
IChunkMerger merger;
@ -104,7 +108,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// In the second phase we invoke IChunkMerger.Merge for each chunk that has a mapped merger.
// During this phase, the merger can either add to the CodeTree or ignore the chunk based on the merging
// rules.
foreach (var chunk in inherited)
// Read the chunks outside in - that is chunks from the _ViewStart closest to the page get merged in first
// and the furthest one last. This allows the merger to ignore a directive like @model that was previously
// seen.
var chunksToMerge = inheritedCodeTrees.SelectMany(tree => tree.Chunks)
.Concat(_defaultInheritedChunks);
foreach (var chunk in chunksToMerge)
{
if (mergerMappings.TryGetValue(chunk.GetType(), out merger))
{

View File

@ -183,8 +183,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
public override RazorParser DecorateRazorParser([NotNull] RazorParser razorParser, string sourceFileName)
{
var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(sourceFileName);
return new MvcRazorParser(razorParser, inheritedChunks);
var inheritedCodeTrees = ChunkInheritanceUtility.GetInheritedCodeTrees(sourceFileName);
return new MvcRazorParser(razorParser, inheritedCodeTrees, DefaultInheritedChunks);
}
/// <inheritdoc />
@ -197,9 +197,9 @@ namespace Microsoft.AspNet.Mvc.Razor
public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder,
[NotNull] CodeBuilderContext context)
{
var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(context.SourceFile);
var inheritedChunks = ChunkInheritanceUtility.GetInheritedCodeTrees(context.SourceFile);
ChunkInheritanceUtility.MergeInheritedChunks(context.CodeTreeBuilder.CodeTree,
ChunkInheritanceUtility.MergeInheritedCodeTrees(context.CodeTreeBuilder.CodeTree,
inheritedChunks,
DefaultModel);

View File

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Generator.Compiler;
@ -8,6 +9,7 @@ using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Parser.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Text;
namespace Microsoft.AspNet.Mvc.Razor
{
@ -17,18 +19,23 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary>
public class MvcRazorParser : RazorParser
{
private readonly IReadOnlyList<Chunk> _viewStartChunks;
private readonly IEnumerable<TagHelperDirectiveDescriptor> _viewStartDirectiveDescriptors;
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorParser"/>.
/// </summary>
/// <param name="parser">The <see cref="RazorParser"/> to copy properties from.</param>
/// <param name="viewStartChunks">The <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/>s that are inherited
/// by parsed pages from _ViewStart files.</param>
public MvcRazorParser(RazorParser parser, IReadOnlyList<Chunk> viewStartChunks)
/// <param name="inheritedCodeTrees">The <see cref="IReadOnlyList{CodeTree}"/>s that are inherited
/// from parsed pages from _ViewStart files.</param>
/// <param name="defaultInheritedChunks">The <see cref="IReadOnlyList{Chunk}"/> inherited by
/// default by all Razor pages in the application.</param>
public MvcRazorParser([NotNull] RazorParser parser,
[NotNull] IReadOnlyList<CodeTree> inheritedCodeTrees,
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks)
: base(parser)
{
_viewStartChunks = viewStartChunks;
// Construct tag helper descriptors from @addTagHelper and @removeTagHelper chunks
_viewStartDirectiveDescriptors = GetTagHelperDescriptors(inheritedCodeTrees, defaultInheritedChunks);
}
/// <inheritdoc />
@ -36,17 +43,47 @@ namespace Microsoft.AspNet.Mvc.Razor
[NotNull] Block documentRoot,
[NotNull] ParserErrorSink errorSink)
{
// Grab all the @addtaghelper chunks from view starts and construct TagHelperDirectiveDescriptors
var directiveDescriptors = _viewStartChunks.OfType<AddTagHelperChunk>()
.Select(chunk => new TagHelperDirectiveDescriptor(
chunk.LookupText,
chunk.Start,
TagHelperDirectiveType.AddTagHelper));
var visitor = new ViewStartAddRemoveTagHelperVisitor(TagHelperDescriptorResolver,
directiveDescriptors,
_viewStartDirectiveDescriptors,
errorSink);
var descriptors = visitor.GetDescriptors(documentRoot);
return visitor.GetDescriptors(documentRoot);
}
private static IEnumerable<TagHelperDirectiveDescriptor> GetTagHelperDescriptors(
IReadOnlyList<CodeTree> inheritedCodeTrees,
IReadOnlyList<Chunk> defaultInheritedChunks)
{
var descriptors = new List<TagHelperDirectiveDescriptor>();
// For tag helpers, the @removeTagHelper only applies tag helpers that were added prior to it.
// Consequently we must visit tag helpers outside-in - furthest _ViewStart first and nearest one last. This
// is different from the behavior of chunk merging where we visit the nearest one first and ignore chunks
// that were previously visited.
var chunksFromViewStarts = inheritedCodeTrees.Reverse()
.SelectMany(tree => tree.Chunks);
var chunksInOrder = defaultInheritedChunks.Concat(chunksFromViewStarts);
foreach (var chunk in chunksInOrder)
{
var addHelperChunk = chunk as AddTagHelperChunk;
if (addHelperChunk != null)
{
var descriptor = new TagHelperDirectiveDescriptor(addHelperChunk.LookupText,
SourceLocation.Undefined,
TagHelperDirectiveType.AddTagHelper);
descriptors.Add(descriptor);
}
else
{
var removeHelperChunk = chunk as RemoveTagHelperChunk;
if (removeHelperChunk != null)
{
var descriptor = new TagHelperDirectiveDescriptor(removeHelperChunk.LookupText,
SourceLocation.Undefined,
TagHelperDirectiveType.RemoveTagHelper);
descriptors.Add(descriptor);
}
}
}
return descriptors;
}

View File

@ -54,17 +54,40 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedContent, responseContent);
}
public static IEnumerable<object[]> TagHelpersAreInheritedFromViewStartPagesData
{
get
{
var expected1 =
@"<root>root-content</root>
<nested>nested-content</nested>";
yield return new[] { "NestedViewStartTagHelper", expected1 };
var expected2 =
@"layout:<root>root-content</root>
<nested>nested-content</nested>";
yield return new[] { "ViewWithLayoutAndNestedTagHelper", expected2 };
var expected3 =
@"layout:<root>root-content</root>
page:<root/>
<nested>nested-content</nested>";
yield return new[] { "ViewWithInheritedRemoveTagHelper", expected3 };
}
}
[Theory]
[InlineData("NestedViewStartTagHelper")]
[InlineData("ViewWithLayoutAndNestedTagHelper")]
public async Task TagHelpersAreInheritedFromViewStartPages(string action)
[MemberData(nameof(TagHelpersAreInheritedFromViewStartPagesData))]
public async Task TagHelpersAreInheritedFromViewStartPages(string action, string expected)
{
// Arrange
var expected = string.Join(Environment.NewLine,
"<root>root-content</root>",
"",
"",
"<nested>nested-content</nested>");
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();

View File

@ -25,31 +25,41 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
}
");
var defaultChunks = new Chunk[]
{
new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" },
};
var host = new MvcRazorHost(fileSystem);
var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]);
var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks);
// Act
var chunks = utility.GetInheritedChunks(@"Views\home\Index.cshtml");
var codeTrees = utility.GetInheritedCodeTrees(@"Views\home\Index.cshtml");
// Assert
Assert.Equal(8, chunks.Count);
Assert.IsType<LiteralChunk>(chunks[0]);
Assert.Equal(2, codeTrees.Count);
var viewStartChunks = codeTrees[0].Chunks;
Assert.Equal(3, viewStartChunks.Count);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]);
Assert.IsType<LiteralChunk>(viewStartChunks[0]);
var usingChunk = Assert.IsType<UsingChunk>(viewStartChunks[1]);
Assert.Equal("MyNamespace", usingChunk.Namespace);
Assert.IsType<LiteralChunk>(viewStartChunks[2]);
Assert.IsType<LiteralChunk>(chunks[2]);
Assert.IsType<LiteralChunk>(chunks[3]);
viewStartChunks = codeTrees[1].Chunks;
Assert.Equal(5, viewStartChunks.Count);
var injectChunk = Assert.IsType<InjectChunk>(chunks[4]);
Assert.IsType<LiteralChunk>(viewStartChunks[0]);
var injectChunk = Assert.IsType<InjectChunk>(viewStartChunks[1]);
Assert.Equal("MyHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Helper", injectChunk.MemberName);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunks[5]);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(viewStartChunks[2]);
Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName);
Assert.IsType<StatementChunk>(chunks[6]);
Assert.IsType<LiteralChunk>(chunks[7]);
Assert.IsType<StatementChunk>(viewStartChunks[3]);
Assert.IsType<LiteralChunk>(viewStartChunks[4]);
}
[Fact]
@ -61,17 +71,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
fileSystem.AddFile(@"Views\_Layout.cshtml", string.Empty);
fileSystem.AddFile(@"Views\home\_not-viewstart.cshtml", string.Empty);
var host = new MvcRazorHost(fileSystem);
var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]);
var defaultChunks = new Chunk[]
{
new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" },
};
var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks);
// Act
var chunks = utility.GetInheritedChunks(@"Views\home\Index.cshtml");
var codeTrees = utility.GetInheritedCodeTrees(@"Views\home\Index.cshtml");
// Assert
Assert.Empty(chunks);
Assert.Empty(codeTrees);
}
[Fact]
public void GetInheritedChunks_ReturnsDefaultInheritedChunks()
public void MergeInheritedChunks_MergesDefaultInheritedChunks()
{
// Arrange
var fileSystem = new TestFileSystem();
@ -83,19 +98,38 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" },
};
var inheritedCodeTrees = new CodeTree[]
{
new CodeTree
{
Chunks = new Chunk[]
{
new UsingChunk { Namespace = "InheritedNamespace" },
new LiteralChunk { Text = "some text" }
}
},
new CodeTree
{
Chunks = new Chunk[]
{
new UsingChunk { Namespace = "AppNamespace.Model" },
}
}
};
var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks);
var codeTree = new CodeTree();
// Act
var chunks = utility.GetInheritedChunks(@"Views\Home\Index.cshtml");
utility.MergeInheritedCodeTrees(codeTree,
inheritedCodeTrees,
"dynamic");
// Assert
Assert.Equal(4, chunks.Count);
var injectChunk = Assert.IsType<InjectChunk>(chunks[1]);
Assert.Equal("DifferentHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName);
Assert.Same(defaultChunks[0], chunks[2]);
Assert.Same(defaultChunks[1], chunks[3]);
Assert.Equal(3, codeTree.Chunks.Count);
Assert.Same(inheritedCodeTrees[0].Chunks[0], codeTree.Chunks[0]);
Assert.Same(inheritedCodeTrees[1].Chunks[0], codeTree.Chunks[1]);
Assert.Same(defaultChunks[0], codeTree.Chunks[2]);
}
}
}

View File

@ -0,0 +1,91 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Razor.Text;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class MvcRazorCodeParserTest
{
[Fact]
public void GetTagHelperDescriptors_ReturnsDescriptorsFromViewStart()
{
// Arrange
var builder = new BlockBuilder { Type = BlockType.Comment };
var block = new Block(builder);
var codeTrees = new[]
{
new CodeTree
{
Chunks = new Chunk[]
{
new LiteralChunk { Text = "Hello world" },
new AddTagHelperChunk { LookupText = "Add Tag Helper" },
}
},
new CodeTree
{
Chunks = new[]
{
new RemoveTagHelperChunk { LookupText = "Remove Tag Helper" },
}
}
};
IList<TagHelperDirectiveDescriptor> descriptors = null;
var resolver = new Mock<ITagHelperDescriptorResolver>();
resolver.Setup(r => r.Resolve(It.IsAny<TagHelperDescriptorResolutionContext>()))
.Callback((TagHelperDescriptorResolutionContext context) =>
{
descriptors = context.DirectiveDescriptors;
})
.Returns(Enumerable.Empty<TagHelperDescriptor>())
.Verifiable();
var baseParser = new RazorParser(new CSharpCodeParser(),
new HtmlMarkupParser(),
resolver.Object);
var parser = new TestableMvcRazorParser(baseParser, codeTrees, new Chunk[0]);
var sink = new ParserErrorSink();
// Act
var result = parser.GetTagHelperDescriptorsPublic(block, sink).ToArray();
// Assert
Assert.NotNull(descriptors);
Assert.Equal(2, descriptors.Count);
Assert.Equal("Remove Tag Helper", descriptors[0].LookupText);
Assert.Equal(SourceLocation.Undefined, descriptors[0].Location);
Assert.Equal("Add Tag Helper", descriptors[1].LookupText);
Assert.Equal(TagHelperDirectiveType.AddTagHelper, descriptors[1].DirectiveType);
Assert.Equal(SourceLocation.Undefined, descriptors[1].Location);
}
private class TestableMvcRazorParser : MvcRazorParser
{
public TestableMvcRazorParser(RazorParser parser,
IReadOnlyList<CodeTree> codeTrees,
IReadOnlyList<Chunk> defaultInheritedChunks)
: base(parser, codeTrees, defaultInheritedChunks)
{
}
public IEnumerable<TagHelperDescriptor> GetTagHelperDescriptorsPublic(
Block documentRoot,
ParserErrorSink errorSink)
{
return GetTagHelperDescriptors(documentRoot, errorSink);
}
}
}
}

View File

@ -39,5 +39,10 @@ namespace TagHelpersWebSite.Controllers
{
return View();
}
public ViewResult ViewWithInheritedRemoveTagHelper()
{
return View("/Views/RemoveTagHelperViewStart/ViewWithInheritedRemoveTagHelper.cshtml");
}
}
}

View File

@ -0,0 +1,2 @@
page:<root/>
<nested>some-content</nested>

View File

@ -0,0 +1,5 @@
@{
Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml";
}
@removetaghelper "TagHelpersWebSite.TagHelpers.RootViewStartTagHelper, TagHelpersWebSite"
@addtaghelper "TagHelpersWebSite.TagHelpers.NestedViewStartTagHelper, TagHelpersWebSite"

View File

@ -1,2 +1,2 @@
<root />
layout:<root/>
@RenderBody()