Skip to content
This repository was archived by the owner on Dec 19, 2018. It is now read-only.

Added MarkupElementRewriter #2624

Merged
merged 1 commit into from
Oct 4, 2018

Conversation

ajaybhargavb
Copy link
Contributor

#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

{
internal class MarkupElementRewriter
{
public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree)
Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Contributor Author

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);
Copy link
Contributor Author

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.

Copy link
Member

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();
Copy link
Contributor Author

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.

@ajaybhargavb ajaybhargavb force-pushed the ajbaaska/markup-rewrite branch from 9d014f6 to 4ac4e54 Compare October 1, 2018 20:17

namespace Microsoft.AspNetCore.Razor.Language.Syntax
{
internal class MarkupElementRewriter
Copy link
Contributor

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);
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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()?

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

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>();
Copy link
Contributor

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);
Copy link
Contributor

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) => {
Copy link
Contributor

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)
Copy link
Contributor

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.

@ajaybhargavb
Copy link
Contributor Author

Ping @NTaylorMullen @rynowak @pranavkm

return newSyntaxTree;
}

private class Rewriter : SyntaxRewriter
Copy link
Member

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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);
}
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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

Copy link
Contributor Author

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;
}
Copy link
Member

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>

Copy link
Member

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.

Copy link
Member

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.
Copy link
Member

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>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@ajaybhargavb ajaybhargavb force-pushed the ajbaaska/markup-rewrite branch from 5df1ece to f1492ef Compare October 2, 2018 22:47
@ajaybhargavb
Copy link
Contributor Author

🆙 📅

Copy link
Contributor

@pranavkm pranavkm left a 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));
Copy link
Contributor

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

Copy link
Contributor

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<>

Copy link

@NTaylorMullen NTaylorMullen left a 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)

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))
Copy link

@NTaylorMullen NTaylorMullen Oct 3, 2018

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)

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);

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();

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?

Copy link
Contributor Author

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing af

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajaybhargavb ajaybhargavb force-pushed the ajbaaska/markup-rewrite branch from a7846a1 to e7706ab Compare October 4, 2018 22:08
@ajaybhargavb
Copy link
Contributor Author

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.

@ajaybhargavb ajaybhargavb force-pushed the ajbaaska/markup-rewrite branch from e7706ab to e0f3c3d Compare October 4, 2018 22:15
@ajaybhargavb ajaybhargavb merged commit e0f3c3d into ajbaaska/taghelper-parser Oct 4, 2018
@ajaybhargavb ajaybhargavb deleted the ajbaaska/markup-rewrite branch October 4, 2018 22:16
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants