Enable Component parameter delegates to not require @() at design time.

- The core issue is that the Razor parser splits attribute values based on whitespace. Therefore, when it encounters `@onclick="() => Foo()"` it breaks it into three different tokens based on the spaces involved. This separation results in multiple adjacent classified spans for C# which is currently unsupported by WTE due to multiple seams overlapping. All that being said we have the opportunity to be smarter when generating attribute values that we feel can be simplified or collapsed; because of this in this PR I changed the `TagHelperBlockRewriter` phase to understand "simple" collapsible blocks and to then collapse them. In the future a goal would be to take a collapsing approach to all potential attributes and then to re-inspect each token individually at higher layers in order to decouple our TagHelper phases from what the parser initially parses.
- Added an integration and parser test to validate the new functionality. Most of the testing is from the fact that no other tests had to change because of this (it doesn't break anything).
- Added a new SyntaxNode method `GetTokens` that flattens a node into only its token representation.

aspnet/AspNetCoredotnet/aspnetcore-tooling#11826
\n\nCommit migrated from 80f1bc76a4
This commit is contained in:
N. Taylor Mullen 2019-08-06 14:39:00 -07:00
parent 56a440ec9d
commit 88a002a918
13 changed files with 318 additions and 0 deletions

View File

@ -521,6 +521,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
_tryParseResult = result;
}
public override SyntaxNode VisitGenericBlock(GenericBlockSyntax node)
{
if (_tryParseResult.IsBoundNonStringAttribute && CanBeCollapsed(node))
{
var tokens = node.GetTokens();
var expression = SyntaxFactory.CSharpExpressionLiteral(tokens);
var rewrittenExpression = (CSharpExpressionLiteralSyntax)VisitCSharpExpressionLiteral(expression);
var newChildren = SyntaxListBuilder<RazorSyntaxNode>.Create();
newChildren.Add(rewrittenExpression);
return node.Update(newChildren);
}
return base.VisitGenericBlock(node);
}
public override SyntaxNode VisitCSharpTransition(CSharpTransitionSyntax node)
{
if (!_tryParseResult.IsBoundNonStringAttribute)
@ -769,6 +785,32 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return value.WithSpanContext(node.GetSpanContext());
}
// Being collapsed represents that a block contains several identical looking markup literal attribute values. This can be the case
// when a user has written something like: @onclick="() => SomeMethod()"
// In that case there would be 3 children:
// - ()
// - =>
// - SomeMethod()
// There are 3 children because the Razor parser separates attribute values based on whitespace.
private static bool CanBeCollapsed(GenericBlockSyntax node)
{
if (node.Children.Count <= 1)
{
// The node is either already collapsed or has no children.
return false;
}
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i].Kind != SyntaxKind.MarkupLiteralAttributeValue)
{
return false;
}
}
return true;
}
private SyntaxNode ConfigureNonStringAttribute(SyntaxNode node)
{
var context = node.GetSpanContext();

View File

@ -224,6 +224,35 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
return ((SyntaxToken)GetFirstTerminal());
}
internal SyntaxList<SyntaxToken> GetTokens()
{
var tokens = SyntaxListBuilder<SyntaxToken>.Create();
AddTokens(this, tokens);
return tokens;
static void AddTokens(SyntaxNode current, SyntaxListBuilder<SyntaxToken> tokens)
{
if (current.SlotCount == 0 && current is SyntaxToken token)
{
// Token
tokens.Add(token);
return;
}
for (var i = 0; i < current.SlotCount; i++)
{
var child = current.GetNodeSlot(i);
if (child != null)
{
AddTokens(child, tokens);
}
}
}
}
internal SyntaxToken GetLastToken()
{
return ((SyntaxToken)GetLastTerminal());

View File

@ -2352,6 +2352,39 @@ namespace Test
#region Event Handlers
[Fact]
public void Component_WithImplicitLambdaEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using System;
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : ComponentBase
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
<MyComponent @onclick=""() => Increment()""/>
@code {
private int counter;
private void Increment() {
counter++;
}
}");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void ChildComponent_WithLambdaEventHandler()
{

View File

@ -423,6 +423,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
.Build()
};
[Fact]
public void UnderstandsMultipartNonStringTagHelperAttributes()
{
EvaluateData(CodeTagHelperAttributes_Descriptors, "<person age=\"(() => 123)()\" />");
}
[Fact]
public void CreatesMarkupCodeSpansForNonStringTagHelperAttributes1()
{

View File

@ -0,0 +1,57 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
__o = Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.UIMouseEventArgs>(this,
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
() => Increment()
#line default
#line hidden
#nullable disable
);
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((builder2) => {
}
));
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
__o = typeof(MyComponent);
#line default
#line hidden
#nullable disable
}
#pragma warning restore 1998
#nullable restore
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private int counter;
private void Increment() {
counter++;
}
#line default
#line hidden
#nullable disable
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,26 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [37] ) - Microsoft.AspNetCore.Components
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
DesignTimeDirective -
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (0:0,0 [43] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent
ComponentAttribute - (23:0,23 [17] x:\dir\subdir\Test\TestComponent.cshtml) - onclick - AttributeStructure.DoubleQuotes
CSharpExpression -
IntermediateToken - - CSharp - Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.UIMouseEventArgs>(this,
IntermediateToken - (23:0,23 [17] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - () => Increment()
IntermediateToken - - CSharp - )
HtmlContent - (43:0,43 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (43:0,43 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
CSharpCode - (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private int counter;\n private void Increment() {\n counter++;\n }\n

View File

@ -0,0 +1,20 @@
Source Location: (23:0,23 [17] x:\dir\subdir\Test\TestComponent.cshtml)
|() => Increment()|
Generated Location: (1002:25,23 [17] )
|() => Increment()|
Source Location: (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml)
|
private int counter;
private void Increment() {
counter++;
}
|
Generated Location: (1512:45,7 [87] )
|
private int counter;
private void Increment() {
counter++;
}
|

View File

@ -0,0 +1,42 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
public class TestComponent : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
builder.OpenComponent<Test.MyComponent>(0);
builder.AddAttribute(1, "onclick", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.UIMouseEventArgs>(this,
#nullable restore
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
() => Increment()
#line default
#line hidden
#nullable disable
));
builder.CloseComponent();
}
#pragma warning restore 1998
#nullable restore
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private int counter;
private void Increment() {
counter++;
}
#line default
#line hidden
#nullable disable
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,17 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
UsingDirective - (104:5,1 [39] ) - Microsoft.AspNetCore.Components
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Components.ComponentBase -
MethodDeclaration - - protected override - void - BuildRenderTree
Component - (0:0,0 [43] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent
ComponentAttribute - (23:0,23 [17] x:\dir\subdir\Test\TestComponent.cshtml) - onclick - AttributeStructure.DoubleQuotes
CSharpExpression -
IntermediateToken - - CSharp - Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.UIMouseEventArgs>(this,
IntermediateToken - (23:0,23 [17] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - () => Increment()
IntermediateToken - - CSharp - )
CSharpCode - (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private int counter;\n private void Increment() {\n counter++;\n }\n

View File

@ -0,0 +1,15 @@
Source Location: (54:2,7 [87] x:\dir\subdir\Test\TestComponent.cshtml)
|
private int counter;
private void Increment() {
counter++;
}
|
Generated Location: (1071:30,7 [87] )
|
private int counter;
private void Increment() {
counter++;
}
|

View File

@ -0,0 +1 @@
Code span at (13:0,13 [13] ) (Accepts:AnyExceptNewline) - Parent: Tag block at (0:0,0 [30] )

View File

@ -0,0 +1,29 @@
RazorDocument - [0..30)::30 - [<person age="(() => 123)()" />]
MarkupBlock - [0..30)::30
MarkupTagHelperElement - [0..30)::30 - person[SelfClosing] - PersonTagHelper
MarkupTagHelperStartTag - [0..30)::30 - [<person age="(() => 123)()" />] - Gen<Markup> - SpanEditHandler;Accepts:Any
OpenAngle;[<];
Text;[person];
MarkupTagHelperAttribute - [7..27)::20 - age - DoubleQuotes - Bound - [ age="(() => 123)()"]
MarkupTextLiteral - [7..8)::1 - [ ] - Gen<Markup> - SpanEditHandler;Accepts:Any
Whitespace;[ ];
MarkupTextLiteral - [8..11)::3 - [age] - Gen<Markup> - SpanEditHandler;Accepts:Any
Text;[age];
Equals;[=];
MarkupTextLiteral - [12..13)::1 - ["] - Gen<None> - SpanEditHandler;Accepts:Any
DoubleQuote;["];
MarkupTagHelperAttributeValue - [13..26)::13
CSharpExpressionLiteral - [13..26)::13 - [(() => 123)()] - Gen<None> - ImplicitExpressionEditHandler;Accepts:AnyExceptNewline;ImplicitExpression[ATD];K14
Text;[(()];
Whitespace;[ ];
Equals;[=];
CloseAngle;[>];
Whitespace;[ ];
Text;[123)()];
MarkupTextLiteral - [26..27)::1 - ["] - Gen<None> - SpanEditHandler;Accepts:Any
DoubleQuote;["];
MarkupMiscAttributeContent - [27..28)::1
MarkupTextLiteral - [27..28)::1 - [ ] - Gen<Markup> - SpanEditHandler;Accepts:Any
Whitespace;[ ];
ForwardSlash;[/];
CloseAngle;[>];