|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Language;
public class DefaultRazorTagHelperContextDiscoveryPhaseTest : RazorProjectEngineTestBase
{
protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest;
#region Legacy
[Fact]
public void Execute_CanHandleSingleLengthAddTagHelperDirective()
{
// Arrange
var expectedDiagnostics = new[]
{
RazorDiagnosticFactory.CreateParsing_UnterminatedStringLiteral(
new SourceSpan(new SourceLocation(14 + Environment.NewLine.Length, 1, 14), contentLength: 1)),
RazorDiagnosticFactory.CreateParsing_InvalidTagHelperLookupText(
new SourceSpan(new SourceLocation(14 + Environment.NewLine.Length, 1, 14), contentLength: 1), "\"")
};
var content =
@"
@addTagHelper """;
var source = TestRazorSourceDocument.Create(content, filePath: null);
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var erroredNode = codeDocument.GetSyntaxTree().Root.DescendantNodes().First(n => n.GetChunkGenerator() is AddTagHelperChunkGenerator);
var chunkGenerator = Assert.IsType<AddTagHelperChunkGenerator>(erroredNode.GetChunkGenerator());
Assert.Equal(expectedDiagnostics, chunkGenerator.Diagnostics);
}
[Fact]
public void Execute_CanHandleSingleLengthRemoveTagHelperDirective()
{
// Arrange
var expectedDiagnostics = new[]
{
RazorDiagnosticFactory.CreateParsing_UnterminatedStringLiteral(
new SourceSpan(new SourceLocation(17 + Environment.NewLine.Length, 1, 17), contentLength: 1)),
RazorDiagnosticFactory.CreateParsing_InvalidTagHelperLookupText(
new SourceSpan(new SourceLocation(17 + Environment.NewLine.Length, 1, 17), contentLength: 1), "\"")
};
var content =
@"
@removeTagHelper """;
var source = TestRazorSourceDocument.Create(content, filePath: null);
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var erroredNode = codeDocument.GetSyntaxTree().Root.DescendantNodes().First(n => n.GetChunkGenerator() is RemoveTagHelperChunkGenerator);
var chunkGenerator = Assert.IsType<RemoveTagHelperChunkGenerator>(erroredNode.GetChunkGenerator());
Assert.Equal(expectedDiagnostics, chunkGenerator.Diagnostics);
}
[Fact]
public void Execute_CanHandleSingleLengthTagHelperPrefix()
{
// Arrange
var expectedDiagnostics = new[]
{
RazorDiagnosticFactory.CreateParsing_UnterminatedStringLiteral(
new SourceSpan(new SourceLocation(17 + Environment.NewLine.Length, 1, 17), contentLength: 1)),
RazorDiagnosticFactory.CreateParsing_InvalidTagHelperPrefixValue(
new SourceSpan(new SourceLocation(17 + Environment.NewLine.Length, 1, 17), contentLength: 1), "tagHelperPrefix", '\"', "\""),
};
var content =
@"
@tagHelperPrefix """;
var source = TestRazorSourceDocument.Create(content, filePath: null);
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var erroredNode = codeDocument.GetSyntaxTree().Root.DescendantNodes().First(n => n.GetChunkGenerator() is TagHelperPrefixDirectiveChunkGenerator);
var chunkGenerator = Assert.IsType<TagHelperPrefixDirectiveChunkGenerator>(erroredNode.GetChunkGenerator());
Assert.Equal(expectedDiagnostics, chunkGenerator.Diagnostics);
}
[Fact]
public void Execute_RewritesTagHelpers()
{
// Arrange
var tagHelper1 = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
var tagHelper2 = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.SetTagHelpers(tagHelper1, tagHelper2);
});
var source = CreateTestSourceDocument();
var codeDocument = projectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
Assert.Equal("form", tagHelperNodes[0].TagName);
Assert.Equal("input", tagHelperNodes[1].TagName);
}
[Fact]
public void Execute_WithTagHelperDescriptorsFromCodeDocument_RewritesTagHelpers()
{
// Arrange
var tagHelper1 = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
var tagHelper2 = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var sourceDocument = CreateTestSourceDocument();
var codeDocument = ProjectEngine.CreateCodeDocument(sourceDocument);
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
codeDocument = codeDocument.WithTagHelpers([tagHelper1, tagHelper2]);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
Assert.Equal("form", tagHelperNodes[0].TagName);
Assert.Equal("input", tagHelperNodes[1].TagName);
}
[Fact]
public void Execute_NullTagHelperDescriptorsFromCodeDocument_FallsBackToTagHelperFeature()
{
// Arrange
var tagHelper1 = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
var tagHelper2 = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.SetTagHelpers(tagHelper1, tagHelper2);
});
var source = CreateTestSourceDocument();
var codeDocument = projectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
codeDocument = codeDocument.WithTagHelpers(value: null);
// Act
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
Assert.Equal("form", tagHelperNodes[0].TagName);
Assert.Equal("input", tagHelperNodes[1].TagName);
}
[Fact]
public void Execute_EmptyTagHelperDescriptorsFromCodeDocument_DoesNotFallbackToTagHelperFeature()
{
// Arrange
var tagHelper1 = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
var tagHelper2 = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.SetTagHelpers(tagHelper1, tagHelper2);
});
var source = CreateTestSourceDocument();
var codeDocument = projectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
codeDocument = codeDocument.WithTagHelpers(value: []);
// Act
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
Assert.Empty(tagHelperNodes);
}
[Fact]
public void Execute_DirectiveWithoutQuotes_RewritesTagHelpers_TagHelperMatchesElementTwice()
{
// Arrange
var tagHelper = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly",
ruleBuilders:
[
ruleBuilder => ruleBuilder
.RequireAttributeDescriptor(attribute => attribute
.Name("a", RequiredAttributeNameComparison.FullMatch)),
ruleBuilder => ruleBuilder
.RequireAttributeDescriptor(attribute => attribute
.Name("b", RequiredAttributeNameComparison.FullMatch)),
]);
var content = @"
@addTagHelper *, TestAssembly
<form a=""hi"" b=""there"">
</form>";
var source = TestRazorSourceDocument.Create(content);
var codeDocument = ProjectEngine.CreateCodeDocument(source, [tagHelper]);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
var formTagHelper = Assert.Single(tagHelperNodes);
Assert.Equal("form", formTagHelper.TagName);
Assert.Contains(formTagHelper.TagHelpers, th => th.Name == tagHelper.Name && th.AssemblyName == tagHelper.AssemblyName);
}
[Fact]
public void Execute_DirectiveWithQuotes_RewritesTagHelpers_TagHelperMatchesElementTwice()
{
// Arrange
var tagHelper = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly",
ruleBuilders:
[
ruleBuilder => ruleBuilder
.RequireAttributeDescriptor(attribute => attribute
.Name("a", RequiredAttributeNameComparison.FullMatch)),
ruleBuilder => ruleBuilder
.RequireAttributeDescriptor(attribute => attribute
.Name("b", RequiredAttributeNameComparison.FullMatch)),
]);
var content = @"
@addTagHelper ""*, TestAssembly""
<form a=""hi"" b=""there"">
</form>";
var source = TestRazorSourceDocument.Create(content);
var codeDocument = ProjectEngine.CreateCodeDocument(source, [tagHelper]);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
var formTagHelper = Assert.Single(tagHelperNodes);
Assert.Equal("form", formTagHelper.TagName);
Assert.Contains(formTagHelper.TagHelpers, th => th.Name == tagHelper.Name && th.AssemblyName == tagHelper.AssemblyName);
}
[Fact]
public void Execute_TagHelpersFromCodeDocumentAndFeature_PrefersCodeDocument()
{
// Arrange
var featureTagHelper = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.SetTagHelpers(featureTagHelper);
});
var source = CreateTestSourceDocument();
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
var codeDocumentTagHelper = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
codeDocument = codeDocument.WithTagHelpers([codeDocumentTagHelper]);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = ProjectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
// Assert
var documentNode = codeDocument.GetDocumentNode();
Assert.Empty(codeDocument.GetSyntaxTree().Diagnostics);
var tagHelperNodes = FindTagHelperNodes(documentNode);
var formTagHelper = Assert.Single(tagHelperNodes);
Assert.Equal("form", formTagHelper.TagName);
}
[Fact]
public void Execute_NoopsWhenNoTagHelpersFromCodeDocumentOrFeature()
{
// Arrange
var source = CreateTestSourceDocument();
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var outputTree = codeDocument.GetSyntaxTree();
Assert.Empty(outputTree.Diagnostics);
Assert.Same(originalTree, outputTree);
}
[Fact]
public void Execute_NoopsWhenNoTagHelperDescriptorsAreResolved()
{
// Arrange
// No taghelper directives here so nothing is resolved.
var source = TestRazorSourceDocument.Create("Hello, world");
var codeDocument = ProjectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = ProjectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var outputTree = codeDocument.GetSyntaxTree();
Assert.Empty(outputTree.Diagnostics);
Assert.Same(originalTree, outputTree);
}
[Fact]
public void Execute_SetsTagHelperDocumentContext()
{
// Arrange
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.Features.Add(new TestTagHelperFeature());
});
// No taghelper directives here so nothing is resolved.
var source = TestRazorSourceDocument.Create("Hello, world");
var codeDocument = projectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
codeDocument = codeDocument.WithSyntaxTree(originalTree);
// Act
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
// Assert
var context = codeDocument.GetTagHelperContext();
Assert.NotNull(context);
Assert.Null(context.Prefix);
Assert.Empty(context.TagHelpers);
}
[Fact]
public void Execute_CombinesErrorsOnRewritingErrors()
{
// Arrange
var tagHelper1 = CreateTagHelperDescriptor(
tagName: "form",
typeName: "TestFormTagHelper",
assemblyName: "TestAssembly");
var tagHelper2 = CreateTagHelperDescriptor(
tagName: "input",
typeName: "TestInputTagHelper",
assemblyName: "TestAssembly");
var projectEngine = RazorProjectEngine.Create(builder =>
{
builder.SetTagHelpers(tagHelper1, tagHelper2);
});
var content =
@"
@addTagHelper *, TestAssembly
<form>
<input value='Hello' type='text' />";
var source = TestRazorSourceDocument.Create(content, filePath: null);
var codeDocument = projectEngine.CreateCodeDocument(source);
var originalTree = RazorSyntaxTree.Parse(source);
var initialError = RazorDiagnostic.Create(
new RazorDiagnosticDescriptor("RZ9999", "Initial test error", RazorDiagnosticSeverity.Error),
new SourceSpan(SourceLocation.Zero, contentLength: 1));
var expectedRewritingError = RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper(
new SourceSpan(new SourceLocation((Environment.NewLine.Length * 2) + 30, 2, 1), contentLength: 4), "form");
var erroredOriginalTree = new RazorSyntaxTree(originalTree.Root, originalTree.Source, [initialError], originalTree.Options);
codeDocument = codeDocument.WithSyntaxTree(erroredOriginalTree);
// Act
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperContextDiscoveryPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultRazorIntermediateNodeLoweringPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultTagHelperResolutionPhase>(codeDocument);
codeDocument = projectEngine.ExecutePhase<DefaultRazorTagHelperRewritePhase>(codeDocument);
// Assert
var outputTree = codeDocument.GetTagHelperRewrittenSyntaxTree();
Assert.Empty(originalTree.Diagnostics);
Assert.NotSame(erroredOriginalTree, outputTree);
Assert.Equal<RazorDiagnostic>([initialError, expectedRewritingError], outputTree.Diagnostics);
}
private static string AssemblyA => "TestAssembly";
private static string AssemblyB => "AnotherAssembly";
private static TagHelperDescriptor Valid_PlainTagHelperDescriptor
{
get
{
return CreateTagHelperDescriptor(
tagName: "valid_plain",
typeName: "Microsoft.AspNetCore.Razor.TagHelpers.ValidPlainTagHelper",
assemblyName: AssemblyA);
}
}
private static TagHelperDescriptor Valid_InheritedTagHelperDescriptor
{
get
{
return CreateTagHelperDescriptor(
tagName: "valid_inherited",
typeName: "Microsoft.AspNetCore.Razor.TagHelpers.ValidInheritedTagHelper",
assemblyName: AssemblyA);
}
}
private static TagHelperDescriptor String_TagHelperDescriptor
{
get
{
// We're treating 'string' as a TagHelper so we can test TagHelpers in multiple assemblies without
// building a separate assembly with a single TagHelper.
return CreateTagHelperDescriptor(
tagName: "string",
typeName: "System.String",
assemblyName: AssemblyB);
}
}
public static TheoryData<string, string> ProcessTagHelperPrefixData
{
get
{
// source, expected prefix
return new TheoryData<string, string>
{
{
$@"
@tagHelperPrefix """"
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidPlain*, TestAssembly",
null
},
{
$@"
@tagHelperPrefix th:
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidPlain*, {AssemblyA}",
"th:"
},
{
$@"
@addTagHelper *, {AssemblyA}
@tagHelperPrefix th:",
"th:"
},
{
$@"
@tagHelperPrefix th-
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidPlain*, {AssemblyA}
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidInherited*, {AssemblyA}",
"th-"
},
{
$@"
@tagHelperPrefix
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidPlain*, {AssemblyA}
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidInherited*, {AssemblyA}",
null
},
{
$@"
@tagHelperPrefix ""th""
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}",
"th"
},
{
$@"
@addTagHelper *, {AssemblyA}
@tagHelperPrefix th:-
@addTagHelper *, {AssemblyB}",
"th:-"
},
};
}
}
[Theory]
[MemberData(nameof(ProcessTagHelperPrefixData))]
public void DirectiveVisitor_ExtractsPrefixFromSyntaxTree(
string source,
string expectedPrefix)
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(source, filePath: "TestFile");
var parser = new RazorParser();
var syntaxTree = parser.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.TagHelperDirectiveVisitor();
visitor.Initialize(tagHelpers: [], sourceDocument.FilePath);
// Act
visitor.Visit(syntaxTree.Root);
// Assert
Assert.Equal(expectedPrefix, visitor.TagHelperPrefix);
}
public static TheoryData<string, TagHelperCollection, TagHelperCollection> ProcessTagHelperMatchesData
// source, taghelpers, expected descriptors
=> new()
{
{
$@"
@addTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper *, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor],
[String_TagHelperDescriptor]
},
{
$@"
@addTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}
@addTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor, Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor, Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidPlain*, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper Microsoft.AspNetCore.Razor.TagHelpers.*, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper Microsoft.AspNetCore.Razor.TagHelpers.ValidP*, {AssemblyA}
@addTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor, Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper Str*, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper *, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper System.{String_TagHelperDescriptor.Name}, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper Microsoft.*, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor],
[String_TagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper ?Microsoft*, {AssemblyA}
@removeTagHelper System.{String_TagHelperDescriptor.Name}, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor, Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper TagHelper*, {AssemblyA}
@removeTagHelper System.{String_TagHelperDescriptor.Name}, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor],
[Valid_InheritedTagHelperDescriptor, Valid_PlainTagHelperDescriptor, String_TagHelperDescriptor]
}
};
[Theory]
[MemberData(nameof(ProcessTagHelperMatchesData))]
public void DirectiveVisitor_FiltersTagHelpersByDirectives(
string source,
TagHelperCollection tagHelpers,
TagHelperCollection expectedTagHelpers)
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(source, filePath: "TestFile");
var parser = new RazorParser();
var syntaxTree = parser.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.TagHelperDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath);
// Act
visitor.Visit(syntaxTree.Root);
var results = visitor.GetResults();
// Assert
Assert.Equal(expectedTagHelpers.Count, results.Count);
foreach (var expectedTagHelper in expectedTagHelpers)
{
Assert.Contains(expectedTagHelper, results);
}
}
public static TheoryData<string, TagHelperCollection> ProcessTagHelperMatches_EmptyResultData
// source, taghelpers
=> new()
{
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper *, {AssemblyA}",
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}
@removeTagHelper {Valid_InheritedTagHelperDescriptor.Name}, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper *, {AssemblyA}
@removeTagHelper *, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@addTagHelper *, {AssemblyA}
@addTagHelper *, {AssemblyB}
@removeTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}
@removeTagHelper {Valid_InheritedTagHelperDescriptor.Name}, {AssemblyA}
@removeTagHelper {String_TagHelperDescriptor.Name}, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@removeTagHelper *, {AssemblyA}
@removeTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}",
[]
},
{
$@"
@addTagHelper *, {AssemblyA}
@removeTagHelper Mic*, {AssemblyA}",
[Valid_PlainTagHelperDescriptor]
},
{
$@"
@addTagHelper Mic*, {AssemblyA}
@removeTagHelper {Valid_PlainTagHelperDescriptor.Name}, {AssemblyA}
@removeTagHelper {Valid_InheritedTagHelperDescriptor.Name}, {AssemblyA}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor]
},
{
$@"
@addTagHelper Microsoft.*, {AssemblyA}
@addTagHelper System.*, {AssemblyB}
@removeTagHelper Microsoft.AspNetCore.Razor.TagHelpers*, {AssemblyA}
@removeTagHelper System.*, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@addTagHelper ?icrosoft.*, {AssemblyA}
@addTagHelper ?ystem.*, {AssemblyB}
@removeTagHelper *?????r, {AssemblyA}
@removeTagHelper Sy??em.*, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor]
},
{
$@"
@addTagHelper ?i?crosoft.*, {AssemblyA}
@addTagHelper ??ystem.*, {AssemblyB}",
[Valid_PlainTagHelperDescriptor, Valid_InheritedTagHelperDescriptor, String_TagHelperDescriptor]
}
};
[Theory]
[MemberData(nameof(ProcessTagHelperMatches_EmptyResultData))]
public void ProcessDirectives_CanReturnEmptyDescriptorsBasedOnDirectiveDescriptors(
string source,
TagHelperCollection tagHelpers)
{
// Arrange
var sourceDocument = TestRazorSourceDocument.Create(source, filePath: "TestFile");
var parser = new RazorParser();
var syntaxTree = parser.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.TagHelperDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath);
// Act
visitor.Visit(syntaxTree.Root);
var results = visitor.GetResults();
// Assert
Assert.Empty(results);
}
[Fact]
public void TagHelperDirectiveVisitor_DoesNotMatch_Components()
{
// Arrange
var componentDescriptor = CreateComponentDescriptor("counter", "SomeProject.Counter", AssemblyA);
var legacyDescriptor = Valid_PlainTagHelperDescriptor;
TagHelperCollection tagHelpers =
[
legacyDescriptor,
componentDescriptor
];
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.TagHelperDirectiveVisitor();
visitor.Initialize(tagHelpers, filePath: null);
var sourceDocument = CreateTestSourceDocument();
var tree = RazorSyntaxTree.Parse(sourceDocument);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
var result = Assert.Single(results);
Assert.Same(legacyDescriptor, result);
}
private static RazorSourceDocument CreateTestSourceDocument()
{
var content =
@"
@addTagHelper *, TestAssembly
<form>
<input value='Hello' type='text' />
</form>";
return TestRazorSourceDocument.Create(content, filePath: null);
}
private static TagHelperDescriptor CreateTagHelperDescriptor(
string tagName,
string typeName,
string assemblyName,
string typeNamespace = null,
string typeNameIdentifier = null,
IEnumerable<Action<BoundAttributeDescriptorBuilder>> attributes = null,
IEnumerable<Action<TagMatchingRuleDescriptorBuilder>> ruleBuilders = null)
{
return CreateDescriptor(TagHelperKind.ITagHelper, tagName, typeName, assemblyName, typeNamespace, typeNameIdentifier, attributes, ruleBuilders);
}
#endregion
#region Components
[Fact]
public void ComponentDirectiveVisitor_DoesNotMatch_LegacyTagHelpers()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor("counter", "SomeProject.Counter", AssemblyA);
var legacyDescriptor = Valid_PlainTagHelperDescriptor;
TagHelperCollection tagHelpers =
[
legacyDescriptor,
componentDescriptor
];
var sourceDocument = CreateComponentTestSourceDocument(@"<Counter />", "C:\\SomeFolder\\SomeProject\\Counter.cshtml");
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
// Assert
Assert.Null(visitor.TagHelperPrefix);
var result = Assert.Single(visitor.GetResults());
Assert.Same(componentDescriptor, result);
}
[Fact]
public void ComponentDirectiveVisitor_AddsErrorOnLegacyTagHelperDirectives()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor("counter", "SomeProject.Counter", AssemblyA);
var legacyDescriptor = Valid_PlainTagHelperDescriptor;
TagHelperCollection tagHelpers =
[
legacyDescriptor,
componentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
@tagHelperPrefix th:
<Counter />
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
Assert.Null(visitor.TagHelperPrefix);
var result = Assert.Single(results);
Assert.Same(componentDescriptor, result);
var directiveChunkGenerator = (TagHelperPrefixDirectiveChunkGenerator)tree.Root.DescendantNodes().First(n => n is CSharpStatementLiteralSyntax).GetChunkGenerator();
var diagnostic = Assert.Single(directiveChunkGenerator.Diagnostics);
Assert.Equal("RZ9978", diagnostic.Id);
}
[Fact]
public void ComponentDirectiveVisitor_MatchesFullyQualifiedComponents()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor(
"SomeProject.SomeOtherFolder.Counter",
"SomeProject.SomeOtherFolder.Counter",
AssemblyA,
fullyQualified: true);
TagHelperCollection tagHelpers =
[
componentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
var result = Assert.Single(results);
Assert.Same(componentDescriptor, result);
}
[Fact]
public void ComponentDirectiveVisitor_ComponentInScope_MatchesChildContent()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor(
"Counter",
"SomeProject.Counter",
AssemblyA);
var childContentDescriptor = CreateComponentDescriptor(
"ChildContent",
"SomeProject.Counter.ChildContent",
AssemblyA,
"SomeProject",
"Counter",
childContent: true);
TagHelperCollection tagHelpers =
[
componentDescriptor,
childContentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
Assert.Equal(2, results.Count);
}
[Fact]
public void ComponentDirectiveVisitor_NullCurrentNamespace_MatchesOnlyFullyQualifiedComponents()
{
// Arrange
string currentNamespace = null;
var componentDescriptor = CreateComponentDescriptor(
"Counter",
"SomeProject.Counter",
AssemblyA);
var fullyQualifiedComponent = CreateComponentDescriptor(
"SomeProject.SomeOtherFolder.Counter",
"SomeProject.SomeOtherFolder.Counter",
AssemblyA,
fullyQualified: true);
TagHelperCollection tagHelpers =
[
componentDescriptor,
fullyQualifiedComponent
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
var result = Assert.Single(results);
Assert.Same(fullyQualifiedComponent, result);
}
[Fact]
public void ComponentDirectiveVisitor_MatchesIfNamespaceInUsing()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor(
"Counter",
"SomeProject.Counter",
AssemblyA);
var anotherComponentDescriptor = CreateComponentDescriptor(
"Foo",
"SomeProject.SomeOtherFolder.Foo",
AssemblyA);
TagHelperCollection tagHelpers =
[
componentDescriptor,
anotherComponentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
@using SomeProject.SomeOtherFolder
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
Assert.Equal(2, results.Count);
}
[Fact]
public void ComponentDirectiveVisitor_MatchesIfNamespaceInUsing_GlobalPrefix()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor(
"Counter",
"SomeProject.SomeOtherFolder.Counter",
AssemblyA);
TagHelperCollection tagHelpers =
[
componentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = """
@using global::SomeProject.SomeOtherFolder
""";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
var result = Assert.Single(results);
Assert.Same(componentDescriptor, result);
}
[Fact]
public void ComponentDirectiveVisitor_DoesNotMatchForUsingAliasAndStaticUsings()
{
// Arrange
var currentNamespace = "SomeProject";
var componentDescriptor = CreateComponentDescriptor(
"Counter",
"SomeProject.Counter",
AssemblyA);
var anotherComponentDescriptor = CreateComponentDescriptor(
"Foo",
"SomeProject.SomeOtherFolder.Foo",
AssemblyA);
TagHelperCollection tagHelpers =
[
componentDescriptor,
anotherComponentDescriptor
];
var filePath = "C:\\SomeFolder\\SomeProject\\Counter.cshtml";
var content = @"
@using Bar = SomeProject.SomeOtherFolder
@using static SomeProject.SomeOtherFolder.Foo
";
var sourceDocument = CreateComponentTestSourceDocument(content, filePath);
var tree = RazorSyntaxTree.Parse(sourceDocument);
var visitor = new DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor();
visitor.Initialize(tagHelpers, sourceDocument.FilePath, currentNamespace);
// Act
visitor.Visit(tree);
var results = visitor.GetResults();
// Assert
var result = Assert.Single(results);
Assert.Same(componentDescriptor, result);
}
[Theory]
[InlineData("", "", true)]
[InlineData("Foo", "Project", true)]
[InlineData("Project.Foo", "Project", true)]
[InlineData("Project.Bar.Foo", "Project.Bar", true)]
[InlineData("Project.Foo", "Project.Bar", true)]
[InlineData("Project.Bar.Foo", "Project", false)]
[InlineData("Bar.Foo", "Project", false)]
public void IsTypeNamespaceInScope_WorksAsExpected(string typeName, string currentNamespace, bool expected)
{
// Arrange & Act
var descriptor = CreateComponentDescriptor(typeName, typeName, "Test.dll");
var tagHelperTypeNamespace = descriptor.TypeNamespace;
var result = DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor.IsTypeNamespaceInScope(tagHelperTypeNamespace, currentNamespace);
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void IsTagHelperFromMangledClass_WorksAsExpected()
{
// Arrange
var className = "Counter";
var typeName = $"SomeProject.SomeNamespace.{ComponentHelpers.MangleClassName(className)}";
var descriptor = CreateComponentDescriptor(
tagName: "Counter",
typeName: typeName,
assemblyName: AssemblyA);
// Act
var result = DefaultRazorTagHelperContextDiscoveryPhase.ComponentDirectiveVisitor.IsTagHelperFromMangledClass(descriptor);
// Assert
Assert.True(result);
}
private static RazorSourceDocument CreateComponentTestSourceDocument(string content, string filePath = null)
{
var sourceDocument = TestRazorSourceDocument.Create(content, filePath: filePath);
return sourceDocument;
}
private static TagHelperDescriptor CreateComponentDescriptor(
string tagName,
string typeName,
string assemblyName,
string typeNamespace = null,
string typeNameIdentifier = null,
IEnumerable<Action<BoundAttributeDescriptorBuilder>> attributes = null,
IEnumerable<Action<TagMatchingRuleDescriptorBuilder>> ruleBuilders = null,
bool fullyQualified = false,
bool childContent = false)
{
var kind = childContent ? TagHelperKind.ChildContent : TagHelperKind.Component;
return CreateDescriptor(kind, tagName, typeName, assemblyName, typeNamespace, typeNameIdentifier, attributes, ruleBuilders, fullyQualified);
}
#endregion
private static TagHelperDescriptor CreateDescriptor(
TagHelperKind kind,
string tagName,
string typeName,
string assemblyName,
string typeNamespace,
string typeNameIdentifier,
IEnumerable<Action<BoundAttributeDescriptorBuilder>> attributes = null,
IEnumerable<Action<TagMatchingRuleDescriptorBuilder>> ruleBuilders = null,
bool componentFullyQualified = false)
{
var builder = TagHelperDescriptorBuilder.CreateTagHelper(kind, typeName, assemblyName);
if (typeNamespace == null || typeNameIdentifier == null)
{
var lastDotIndex = typeName.LastIndexOf('.');
typeNamespace ??= lastDotIndex >= 0 ? typeName[..lastDotIndex] : "";
typeNameIdentifier ??= lastDotIndex >= 0 ? typeName[(lastDotIndex + 1)..] : typeName;
}
builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier);
if (attributes != null)
{
foreach (var attributeBuilder in attributes)
{
builder.BoundAttributeDescriptor(attributeBuilder);
}
}
if (ruleBuilders != null)
{
foreach (var ruleBuilder in ruleBuilders)
{
builder.TagMatchingRuleDescriptor(innerRuleBuilder =>
{
innerRuleBuilder.RequireTagName(tagName);
ruleBuilder(innerRuleBuilder);
});
}
}
else
{
builder.TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName(tagName));
}
if (componentFullyQualified)
{
builder.IsFullyQualifiedNameMatch = true;
}
var descriptor = builder.Build();
return descriptor;
}
private static TagHelperIntermediateNode[] FindTagHelperNodes(DocumentIntermediateNode documentNode)
{
var results = new System.Collections.Generic.List<TagHelperIntermediateNode>();
CollectTagHelperNodes(documentNode, results);
return results.ToArray();
}
private static void CollectTagHelperNodes(IntermediateNode node, System.Collections.Generic.List<TagHelperIntermediateNode> results)
{
if (node is TagHelperIntermediateNode tagHelperNode)
{
results.Add(tagHelperNode);
}
foreach (var child in node.Children)
{
CollectTagHelperNodes(child, results);
}
}
}
|