-
Notifications
You must be signed in to change notification settings - Fork 222
Added MarkupElementRewriter #2624
Added MarkupElementRewriter #2624
Conversation
{ | ||
internal class MarkupElementRewriter | ||
{ | ||
public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called before tag helper phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Names are wacky here because there are two rewritiers. Suggest something like AddElementNodes
RemoveElementNodes
return newSyntaxTree; | ||
} | ||
|
||
public static RazorSyntaxTree RemoveMarkupElement(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called after tag helper phase.
BaselineTest(rewritten); | ||
|
||
var unrewritten = MarkupElementRewriter.RemoveMarkupElement(rewritten); | ||
Assert.Equal(syntaxTree.Root.SerializedValue, unrewritten.Root.SerializedValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each test will also run the removal phase and make sure the output matches the original syntax tree. It didn't make sense to add separate tests for the removal phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it.
continue; | ||
} | ||
|
||
var tagName = tagBlock.GetTagName(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetTagName()
is an extension method to MarkupTagBlockSyntax
. This will change in the future when tag block have a first class TagName child.
9d014f6
to
4ac4e54
Compare
|
||
namespace Microsoft.AspNetCore.Razor.Language.Syntax | ||
{ | ||
internal class MarkupElementRewriter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static class
?
|
||
private bool IsVoidElement(MarkupTagBlockSyntax tagBlock) | ||
{ | ||
return VoidElements.Contains(tagBlock.GetTagName(), StringComparer.OrdinalIgnoreCase); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is doing Enumerable.Contains
. Did you mean to do VoidElements.Contains(tagBlock.GetTagName())
since the hashset is already using an OrdinalIgnoreCase
comparer?
return VoidElements.Contains(tagBlock.GetTagName(), StringComparer.OrdinalIgnoreCase); | ||
} | ||
|
||
private bool IsSelfClosing(MarkupTagBlockSyntax tagBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this is is a end tag like </>
but don't see a corresponding test. Should this be unit tested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a corresponding test. https://github.com/aspnet/Razor/pull/2624/files#diff-8dfd8aeca4f15b18bb45a38795fd1c02R94
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't either of those have one or more child nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They can't have any child nodes.
{ | ||
var lastChild = tagBlock.ChildNodes().LastOrDefault(); | ||
|
||
return lastChild?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why does IsEndTag
use literal tokens and this use GetContent()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IsEndTag
was a copy-paste-modify. I was wondering the same thing as why is it doing all the weird index stuff. But I left it as is for now.
return false; | ||
} | ||
|
||
private SyntaxNode RewriteMalformedTags(SyntaxNode parent, int malformedTagCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is invoked when malformedTagCount != _startTagTracker.Count
, but reading the code it looks like _startTagTracker.Count must be > malformedTagCount
(since it's popped that many times). Does the if
need to be changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. It doesn't functionally change anything but I'll change the if.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
node = RewriteNodeCore(node, startTag: tagBlock, tagChildren: Enumerable.Empty<RazorSyntaxNode>(), endTag: null); | ||
|
||
// Since we rewrote 'node', it's children are different. Update our collection. | ||
children = node.ChildNodes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work? Should this restart from the beginning since the children are different or continue from the next index because this node is already observed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't restart. It just continues from where it left off. I still have to update the collection because rewriting node changes the children.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if instead you created a new list of children and then rewrote it once.
So like:
A
B
C
D
Iterate through the children of A
- (B, C, D)
and rewrite just those nodes as needed so you end up with (B, C!, D)
(assume C was rewritten). Then rewrite A
once to replace its children. That's how I would expect something like this to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that when I rewrite (B, C, D) => (B, C!, D), it actually becomes (B, C!, D!) because everything after a rewritten node is different because red nodes are created on demand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this in more detail? I'm not sure that I get it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I missed this part "created a new list of children". What I said doesn't apply in that case. I'll give this a shot
|
||
public Rewriter() | ||
{ | ||
_startTagTracker = new Stack<TagBlockTracker>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline?
MarkupTagBlockSyntax endTag) | ||
{ | ||
var body = new SyntaxList<RazorSyntaxNode>(tagChildren); | ||
var markupElement = SyntaxFactory.MarkupElement((MarkupTagBlockSyntax)startTag, body, endTag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be initialized inline (in the lambda)?
} | ||
|
||
// Replace nodes | ||
var rewrittenElement = parent.ReplaceNodes(originalNodes, (original, rewritten) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting. Also local functions for 💯 points
|
||
var currentIndex = 0; | ||
var children = node.ChildNodes(); | ||
while (currentIndex < children.Count) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for (var i= 0; i < children.Count; i++)
works really well here.
return newSyntaxTree; | ||
} | ||
|
||
private class Rewriter : SyntaxRewriter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there precendent for any of this? Like is this based on something or did you write it fresh?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there precendent for any of this? Like is this based on something or did you write it fresh?
This is mostly fresh. Some of the malformed tag handling logic is very similar to https://github.com/aspnet/Razor/blob/master/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs#L730
if (node != null) | ||
{ | ||
node = RewriteNode(node); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can a node ever be null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Visit()
is called on every child and Visit()
calls node.Accept(this)
if the node is not null.
node = RewriteNode(node); | ||
} | ||
|
||
return base.Visit(node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense for this to be a preorder traversal instead? https://github.com/aspnet/Blazor/blob/master/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs#L55
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works as well
{ | ||
// Nothing to replace | ||
return parent; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this preserve order correctly? What about a case like:
<div>Hi</div>
@Something!
<div>bye</div>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, you did the below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would make a little more sense if you mutated originalNodes
in place and preserved ordering while doing so.
return true; | ||
} | ||
|
||
// Could not recover tag. Aka we found an end tag without a corresponding start tag. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure you have a test for recovery when the cause is a malformed void element. We had a bug for this recently in Blazor.
<input ....></input>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
5df1ece
to
f1492ef
Compare
🆙 📅 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me
TrackChild(rewritten, rewrittenChildren); | ||
} | ||
|
||
tagChildren.ForEach(c => TrackChild(null, rewrittenChildren)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not use lambdas with side effects? Just write the foreach
, it's more readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd also let you pass in Array.Empty<RazorSyntaxNode>()
instead of new List<>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall things are good. The only beef I have with the PR is that the two core methods BuildMarkupElement
and TrackChild
are supppper confusing to follow.
{ | ||
internal static class MarkupElementRewriter | ||
{ | ||
public static RazorSyntaxTree AddMarkupElement(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be plural, AddMarkupElements
Ditto with the remove
{ | ||
// Don't want to track incomplete, void or self-closing tags. | ||
// Simply wrap it in a block with no body or start/end tag. | ||
if (IsEndTag(tagBlock)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol this is super odd. This case handles:
</ >
</foo />
Should update the comment.
return node; | ||
} | ||
|
||
private void BuildMarkupElement(List<SyntaxNode> rewrittenChildren, MarkupTagBlockSyntax startTag, List<RazorSyntaxNode> tagChildren, MarkupTagBlockSyntax endTag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IReadOnlyList<RazorSyntaxNode> tagChildren
, don't want to give the perception it's edited.
// The call to SyntaxNode.ReplaceNodes() later will take care removing the nodes whose replacement is null. | ||
|
||
var body = tagChildren.Where(t => t != null).ToList(); | ||
var rewritten = SyntaxFactory.MarkupElement(startTag, new SyntaxList<RazorSyntaxNode>(body), endTag: endTag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for endTag:
return node; | ||
} | ||
|
||
_startTagTracker.Clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In practice is this ever not already clear?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be. Just being extra defensive.
} | ||
} | ||
|
||
private void TrackChild(SyntaxNode child, List<SyntaxNode> rewrittenChildren) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confusing af
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this to https://github.com/aspnet/Razor/issues/2619.
a7846a1
to
e7706ab
Compare
I've addressed most of the feedback here. I've added the one I didn't address to this list https://github.com/aspnet/Razor/issues/2619 to be addressed later. Merging this now. |
e7706ab
to
e0f3c3d
Compare
#2584
This is going to be run before and after the tag helper phase. I am currently not adding any errors at this level. All errors will be added during the tag helper phase. Sending this as a separate PR just to make sure everyone is on the same page.
Added tests