diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs index d2dc97749d..9d47a0b97f 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs @@ -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.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(); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxNode.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxNode.cs index 2f58f13fe3..aacc0eb91e 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxNode.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxNode.cs @@ -224,6 +224,35 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax return ((SyntaxToken)GetFirstTerminal()); } + internal SyntaxList GetTokens() + { + var tokens = SyntaxListBuilder.Create(); + + AddTokens(this, tokens); + + return tokens; + + static void AddTokens(SyntaxNode current, SyntaxListBuilder 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()); diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs index cbd8737c16..b331afa4ca 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs @@ -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(@" + Increment()""/> + +@code { + private int counter; + private void Increment() { + counter++; + } +}"); + + // Assert + AssertDocumentNodeMatchesBaseline(generated.CodeDocument); + AssertCSharpDocumentMatchesBaseline(generated.CodeDocument); + CompileToAssembly(generated); + } + [Fact] public void ChildComponent_WithLambdaEventHandler() { diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/TagHelperBlockRewriterTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/TagHelperBlockRewriterTest.cs index 7460142c23..c581a53c3c 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/TagHelperBlockRewriterTest.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/Legacy/TagHelperBlockRewriterTest.cs @@ -423,6 +423,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy .Build() }; + [Fact] + public void UnderstandsMultipartNonStringTagHelperAttributes() + { + EvaluateData(CodeTagHelperAttributes_Descriptors, " 123)()\" />"); + } + [Fact] public void CreatesMarkupCodeSpansForNonStringTagHelperAttributes1() { diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs new file mode 100644 index 0000000000..5e7e2fe184 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs @@ -0,0 +1,57 @@ +// +#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(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 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt new file mode 100644 index 0000000000..651d67138d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt @@ -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(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 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt new file mode 100644 index 0000000000..6487d39922 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentDesignTimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt @@ -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++; + } +| + diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs new file mode 100644 index 0000000000..ddcbeca415 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.codegen.cs @@ -0,0 +1,42 @@ +// +#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(0); + builder.AddAttribute(1, "onclick", Microsoft.AspNetCore.Components.EventCallback.Factory.Create(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 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt new file mode 100644 index 0000000000..b92548c14d --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.ir.txt @@ -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(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 diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt new file mode 100644 index 0000000000..e4a8581302 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentRuntimeCodeGenerationTest/Component_WithImplicitLambdaEventHandler/TestComponent.mappings.txt @@ -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++; + } +| + diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.cspans.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.cspans.txt new file mode 100644 index 0000000000..017c88aff0 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.cspans.txt @@ -0,0 +1 @@ +Code span at (13:0,13 [13] ) (Accepts:AnyExceptNewline) - Parent: Tag block at (0:0,0 [30] ) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.stree.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.stree.txt new file mode 100644 index 0000000000..51348bf9d0 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.stree.txt @@ -0,0 +1,29 @@ +RazorDocument - [0..30)::30 - [] + MarkupBlock - [0..30)::30 + MarkupTagHelperElement - [0..30)::30 - person[SelfClosing] - PersonTagHelper + MarkupTagHelperStartTag - [0..30)::30 - [] - Gen - SpanEditHandler;Accepts:Any + OpenAngle;[<]; + Text;[person]; + MarkupTagHelperAttribute - [7..27)::20 - age - DoubleQuotes - Bound - [ age="(() => 123)()"] + MarkupTextLiteral - [7..8)::1 - [ ] - Gen - SpanEditHandler;Accepts:Any + Whitespace;[ ]; + MarkupTextLiteral - [8..11)::3 - [age] - Gen - SpanEditHandler;Accepts:Any + Text;[age]; + Equals;[=]; + MarkupTextLiteral - [12..13)::1 - ["] - Gen - SpanEditHandler;Accepts:Any + DoubleQuote;["]; + MarkupTagHelperAttributeValue - [13..26)::13 + CSharpExpressionLiteral - [13..26)::13 - [(() => 123)()] - Gen - ImplicitExpressionEditHandler;Accepts:AnyExceptNewline;ImplicitExpression[ATD];K14 + Text;[(()]; + Whitespace;[ ]; + Equals;[=]; + CloseAngle;[>]; + Whitespace;[ ]; + Text;[123)()]; + MarkupTextLiteral - [26..27)::1 - ["] - Gen - SpanEditHandler;Accepts:Any + DoubleQuote;["]; + MarkupMiscAttributeContent - [27..28)::1 + MarkupTextLiteral - [27..28)::1 - [ ] - Gen - SpanEditHandler;Accepts:Any + Whitespace;[ ]; + ForwardSlash;[/]; + CloseAngle;[>]; diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.tspans.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.tspans.txt new file mode 100644 index 0000000000..6c93624813 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/test/TestFiles/ParserTests/TagHelperBlockRewriterTest/UnderstandsMultipartNonStringTagHelperAttributes.tspans.txt @@ -0,0 +1 @@ +TagHelper span at (0:0,0 [30] ) - PersonTagHelper