From aa024eb33f3c92f3466167118451f300a4669832 Mon Sep 17 00:00:00 2001 From: Chris Sienkiewicz Date: Mon, 26 Aug 2024 10:14:26 -0700 Subject: [PATCH] [FUSE] Provide intellisense for @inject directives (#10771) * When @Inject is missing the member name, generate a syntactically valid c# identifier so we get intellisense * Emit an empty section when there is no typename * Add tests and update baselines --- .../CodeGenerationIntegrationTest.cs | 24 +++++++++ .../IncompleteDirectives_DesignTime.ir.txt | 3 ++ .../IncompleteDirectives_Runtime.codegen.cs | 42 ++++++++------- ...completeDirectives_Runtime.diagnostics.txt | 12 ++--- .../IncompleteDirectives_Runtime.ir.txt | 51 +++++++------------ .../IncompleteDirectives_Runtime.mappings.txt | 15 ++++++ .../IncompleteDirectives_DesignTime.ir.txt | 3 ++ .../IncompleteDirectives_Runtime.codegen.cs | 16 ++++++ .../IncompleteDirectives_Runtime.ir.txt | 3 ++ .../IncompleteDirectives_DesignTime.ir.txt | 3 ++ .../IncompleteDirectives_Runtime.codegen.cs | 23 +++++++++ .../IncompleteDirectives_Runtime.ir.txt | 3 ++ .../ComponentDirectiveIntegrationTest.cs | 17 +++++++ .../ComponentInjectDirectivePass.cs | 40 +++++++++------ .../ComponentInjectIntermediateNode.cs | 38 ++++++++++---- .../DefaultTagHelperTargetExtension.cs | 19 ++++--- .../src/Language/SourceSpan.cs | 5 ++ .../src/Mvc.Version1_X/InjectDirective.cs | 36 ++++++++----- .../Mvc.Version1_X/InjectIntermediateNode.cs | 3 ++ .../Mvc.Version1_X/InjectTargetExtension.cs | 16 ++++-- .../src/Mvc.Version2_X/InjectDirective.cs | 36 ++++++++----- .../Mvc.Version2_X/InjectIntermediateNode.cs | 3 ++ .../Mvc.Version2_X/InjectTargetExtension.cs | 16 ++++-- .../src/Mvc/InjectDirective.cs | 36 ++++++++----- .../src/Mvc/InjectIntermediateNode.cs | 3 ++ .../src/Mvc/InjectTargetExtension.cs | 16 ++++-- .../RazorIntegrationTestBase.cs | 12 +++++ 27 files changed, 358 insertions(+), 136 deletions(-) create mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.mappings.txt diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/IntegrationTests/CodeGenerationIntegrationTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/IntegrationTests/CodeGenerationIntegrationTest.cs index 0e1e4627bb..88bd6ed87e 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -69,6 +69,30 @@ public class MyService Assert.NotEmpty(compiled.CodeDocument.GetCSharpDocument().Diagnostics); } + [Fact] + public void IncompleteDirectives_Runtime() + { + // Arrange + AddCSharpSyntaxTree(@" +public class MyService +{ + public string Html { get; set; } +}"); + + var projectItem = CreateProjectItemFromFile(); + + // Act + var compiled = CompileToCSharp(projectItem, designTime: false); + + // Assert + AssertDocumentNodeMatchesBaseline(compiled.CodeDocument.GetDocumentIntermediateNode()); + AssertCSharpDocumentMatchesBaseline(compiled.CodeDocument.GetCSharpDocument()); + AssertSourceMappingsMatchBaseline(compiled.CodeDocument); + + // We expect this test to generate a bunch of errors. + Assert.True(compiled.CodeDocument.GetCSharpDocument().Diagnostics.Length > 0); + } + [Fact] public void InheritsViewModel_DesignTime() { diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt index 253d706c04..df78386296 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt @@ -60,3 +60,6 @@ Inject - Inject - Inject - + Inject - + Inject - + Inject - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs index c87e4aaf40..470f2a28c4 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs @@ -1,9 +1,10 @@ -#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "844eb91b909a14b78feddd5e6866563b5a75e021" +#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "c83d1c26cf039a87fc6aedc860fd9d28a34d96dfb2e405e6af3918602ca27755" // #pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCore.TestFiles_IntegrationTests_CodeGenerationIntegrationTest_IncompleteDirectives), @"mvc.1.0.view", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml")] namespace AspNetCore { - #line hidden + #line default using System; using System.Collections.Generic; using System.Linq; @@ -11,31 +12,38 @@ namespace AspNetCore using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; - public class TestFiles_IntegrationTests_CodeGenerationIntegrationTest_IncompleteDirectives_cshtml : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + #line default + #line hidden + [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"Sha256", @"c83d1c26cf039a87fc6aedc860fd9d28a34d96dfb2e405e6af3918602ca27755", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml")] + public class TestFiles_IntegrationTests_CodeGenerationIntegrationTest_IncompleteDirectives : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage { #pragma warning disable 1998 public async override global::System.Threading.Tasks.Task ExecuteAsync() { - BeginContext(83, 4, true); + WriteLiteral("\r\n"); + WriteLiteral("\r\n"); WriteLiteral("\r\n\r\n"); - EndContext(); - BeginContext(93, 2, true); WriteLiteral("\r\n"); - EndContext(); - BeginContext(102, 4, true); - WriteLiteral("\r\n\r\n"); - EndContext(); - BeginContext(113, 2, true); WriteLiteral("\r\n"); - EndContext(); - BeginContext(123, 2, true); WriteLiteral("\r\n"); - EndContext(); - BeginContext(150, 2, true); - WriteLiteral("\r\n"); - EndContext(); } #pragma warning restore 1998 + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public +#line (8,9)-(8,18) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" +MyService + +#line default +#line hidden + Member_test { get; private set; } +#line (7,9)-(7,9) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden +#line (6,8)-(6,8) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.diagnostics.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.diagnostics.txt index 34210018b6..d70ab96ce6 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.diagnostics.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.diagnostics.txt @@ -1,6 +1,6 @@ -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(3,7): Error RZ9999: The 'model' directive expects a type name. -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,1): Error RZ9999: The 'model' directive may only occur once per document. -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,8): Error RZ9999: The 'model' directive expects a type name. -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(6,8): Error RZ9999: The 'inject' directive expects a type name. -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,9): Error RZ9999: The 'inject' directive expects a type name. -TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,26): Error RZ9999: The 'inject' directive expects an identifier. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(3,7): Error RZ1013: The 'model' directive expects a type name. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,1): Error RZ2001: The 'model' directive may only occur once per document. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,8): Error RZ1013: The 'model' directive expects a type name. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(6,8): Error RZ1013: The 'inject' directive expects a type name. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,9): Error RZ1013: The 'inject' directive expects a type name. +TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,26): Error RZ1015: The 'inject' directive expects an identifier. diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt index e1132db9c4..88b3a491c5 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt @@ -1,4 +1,5 @@ Document - + RazorCompiledItemAttribute - NamespaceDeclaration - - AspNetCore UsingDirective - (1:0,1 [14] ) - System UsingDirective - (16:1,1 [34] ) - System.Collections.Generic @@ -7,50 +8,36 @@ UsingDirective - (102:4,1 [32] ) - Microsoft.AspNetCore.Mvc UsingDirective - (135:5,1 [42] ) - Microsoft.AspNetCore.Mvc.Rendering UsingDirective - (178:6,1 [45] ) - Microsoft.AspNetCore.Mvc.ViewFeatures - ClassDeclaration - - public - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_IncompleteDirectives_cshtml - global::Microsoft.AspNetCore.Mvc.Razor.RazorPage - + RazorSourceChecksumAttribute - + ClassDeclaration - - public - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_IncompleteDirectives - global::Microsoft.AspNetCore.Mvc.Razor.RazorPage - MethodDeclaration - - public async override - global::System.Threading.Tasks.Task - ExecuteAsync - CSharpCode - - IntermediateToken - - CSharp - BeginContext(83, 4, true); - HtmlContent - (83:0,83 [4] IncompleteDirectives.cshtml) - IntermediateToken - (83:0,83 [4] IncompleteDirectives.cshtml) - Html - \n\n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + HtmlContent - (85:1,0 [2] IncompleteDirectives.cshtml) + LazyIntermediateToken - (85:1,0 [2] IncompleteDirectives.cshtml) - Html - \n MalformedDirective - (87:2,0 [6] IncompleteDirectives.cshtml) - model - CSharpCode - - IntermediateToken - - CSharp - BeginContext(93, 2, true); + CSharpCode - (93:2,6 [0] IncompleteDirectives.cshtml) + LazyIntermediateToken - (93:2,6 [0] IncompleteDirectives.cshtml) - CSharp - HtmlContent - (93:2,6 [2] IncompleteDirectives.cshtml) - IntermediateToken - (93:2,6 [2] IncompleteDirectives.cshtml) - Html - \n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + LazyIntermediateToken - (93:2,6 [2] IncompleteDirectives.cshtml) - Html - \n MalformedDirective - (95:3,0 [7] IncompleteDirectives.cshtml) - model - CSharpCode - - IntermediateToken - - CSharp - BeginContext(102, 4, true); + DirectiveToken - (102:3,7 [0] IncompleteDirectives.cshtml) - HtmlContent - (102:3,7 [4] IncompleteDirectives.cshtml) - IntermediateToken - (102:3,7 [4] IncompleteDirectives.cshtml) - Html - \n\n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + LazyIntermediateToken - (102:3,7 [4] IncompleteDirectives.cshtml) - Html - \n\n MalformedDirective - (106:5,0 [7] IncompleteDirectives.cshtml) - inject - CSharpCode - - IntermediateToken - - CSharp - BeginContext(113, 2, true); + CSharpCode - (113:5,7 [0] IncompleteDirectives.cshtml) + LazyIntermediateToken - (113:5,7 [0] IncompleteDirectives.cshtml) - CSharp - HtmlContent - (113:5,7 [2] IncompleteDirectives.cshtml) - IntermediateToken - (113:5,7 [2] IncompleteDirectives.cshtml) - Html - \n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + LazyIntermediateToken - (113:5,7 [2] IncompleteDirectives.cshtml) - Html - \n MalformedDirective - (115:6,0 [8] IncompleteDirectives.cshtml) - inject - CSharpCode - - IntermediateToken - - CSharp - BeginContext(123, 2, true); + DirectiveToken - (123:6,8 [0] IncompleteDirectives.cshtml) - HtmlContent - (123:6,8 [2] IncompleteDirectives.cshtml) - IntermediateToken - (123:6,8 [2] IncompleteDirectives.cshtml) - Html - \n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + LazyIntermediateToken - (123:6,8 [2] IncompleteDirectives.cshtml) - Html - \n MalformedDirective - (125:7,0 [25] IncompleteDirectives.cshtml) - inject DirectiveToken - (133:7,8 [17] IncompleteDirectives.cshtml) - MyService - CSharpCode - - IntermediateToken - - CSharp - BeginContext(150, 2, true); HtmlContent - (150:7,25 [2] IncompleteDirectives.cshtml) - IntermediateToken - (150:7,25 [2] IncompleteDirectives.cshtml) - Html - \n - CSharpCode - - IntermediateToken - - CSharp - EndContext(); + LazyIntermediateToken - (150:7,25 [2] IncompleteDirectives.cshtml) - Html - \n + Inject - + Inject - + Inject - Inject - Inject - Inject - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.mappings.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.mappings.txt new file mode 100644 index 0000000000..61ba7c7637 --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.mappings.txt @@ -0,0 +1,15 @@ +Source Location: (133:7,8 [9] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml) +|MyService| +Generated Location: (1895:33,0 [9] ) +|MyService| + +Source Location: (123:6,8 [0] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml) +|| +Generated Location: (2096:39,0 [0] ) +|| + +Source Location: (113:5,7 [0] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml) +|| +Generated Location: (2233:43,0 [0] ) +|| + diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt index dd224f999b..8831c20bbb 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt @@ -76,6 +76,9 @@ Inject - Inject - Inject - + Inject - + Inject - + Inject - CSharpCode - IntermediateToken - - CSharp - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => (global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary)PageContext?.ViewData; CSharpCode - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs index 7c3e56d4eb..4044c13d2f 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs @@ -50,6 +50,22 @@ namespace AspNetCore EndContext(); } #pragma warning restore 1998 + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public +#line (12,9)-(12,18) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" +MyService + +#line default +#line hidden + Member_test { get; private set; } +#line (11,9)-(11,9) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden +#line (10,8)-(10,8) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt index e19bd0c0c4..6216dbe0d3 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt @@ -91,6 +91,9 @@ Inject - Inject - Inject - + Inject - + Inject - + Inject - CSharpCode - IntermediateToken - - CSharp - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => (global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary)PageContext?.ViewData; CSharpCode - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt index 1c2c877056..61e677a4eb 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt @@ -78,6 +78,9 @@ Inject - Inject - Inject - + Inject - + Inject - + Inject - CSharpCode - IntermediateToken - - CSharp - #nullable restore\npublic global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => (global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary)PageContext?.ViewData!;\n#nullable disable CSharpCode - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs index 7ec2b156f5..641f07ac3e 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.codegen.cs @@ -35,6 +35,29 @@ namespace AspNetCore WriteLiteral("\r\n"); } #pragma warning restore 1998 + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public +#nullable restore +#line (12,9)-(12,18) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" +MyService + +#line default +#line hidden +#nullable disable + Member_test { get; private set; } + = default!; +#nullable restore +#line (11,9)-(11,9) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden +#nullable disable +#nullable restore +#line (10,8)-(10,8) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + +#line default +#line hidden +#nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt index 3ebf901844..11e2588b30 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt @@ -55,6 +55,9 @@ Inject - Inject - Inject - + Inject - + Inject - + Inject - CSharpCode - IntermediateToken - - CSharp - #nullable restore\npublic global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => (global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary)PageContext?.ViewData!;\n#nullable disable CSharpCode - diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDirectiveIntegrationTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDirectiveIntegrationTest.cs index 703bf8dffb..3c9f0249c0 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDirectiveIntegrationTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentDirectiveIntegrationTest.cs @@ -100,6 +100,23 @@ public class ComponentDirectiveIntegrationTest : RazorIntegrationTestBase s => AssertEx.Equal("private TestNamespace.IMyService2 Test.TestComponent.MyService2 { get; set; }", s.ToTestDisplayString())); } + [Fact] + public void SupportsIncompleteInjectDirectives() + { + var component = CompileToComponent(""" + @inject + @inject DateTime + @inject DateTime Value + """); + + // Assert 1: Compiled type has correct properties + var injectableProperties = component.GetMembers().OfType() + .Where(p => p.GetAttributes().Any(a => a.AttributeClass.Name == "InjectAttribute")); + Assert.Collection(injectableProperties.OrderBy(p => p.Name), + s => AssertEx.Equal("private System.DateTime Test.TestComponent.Member___UniqueIdSuppressedForTesting__ { get; set; }", s.ToTestDisplayString()), + s => AssertEx.Equal("private System.DateTime Test.TestComponent.Value { get; set; }", s.ToTestDisplayString())); + } + private const string AdditionalCode = """ using Microsoft.AspNetCore.Components; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectDirectivePass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectDirectivePass.cs index b5e7a0afb1..0cbf25785f 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectDirectivePass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectDirectivePass.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -25,30 +26,31 @@ internal class ComponentInjectDirectivePass : IntermediateNodePassBase, IRazorDi for (var i = visitor.Directives.Count - 1; i >= 0; i--) { var directive = visitor.Directives[i]; - var tokens = directive.Tokens.ToArray(); - if (tokens.Length < 2) + var tokens = directive.Children.OfType().ToArray(); + var isMalformed = directive is MalformedDirectiveIntermediateNode; + + var hasType = tokens.Length > 0 && !string.IsNullOrWhiteSpace(tokens[0].Content); + Debug.Assert(hasType || isMalformed); + var typeName = hasType ? tokens[0].Content : string.Empty; + var typeSpan = hasType ? tokens[0].Source : directive.Source?.GetZeroWidthEndSpan(); + + var hasMemberName = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1].Content); + Debug.Assert(hasMemberName || isMalformed); + var memberName = hasMemberName ? tokens[1].Content : null; + var memberSpan = hasMemberName ? tokens[1].Source : null; + + if (hasMemberName && !properties.Add(memberName)) { continue; } - var typeName = tokens[0].Content; - var typeSpan = tokens[0].Source; - var memberName = tokens[1].Content; - var memberSpan = tokens[1].Source; - - if (!properties.Add(memberName)) - { - continue; - } - - classNode.Children.Add(new ComponentInjectIntermediateNode(typeName, memberName, typeSpan, memberSpan)); + classNode.Children.Add(new ComponentInjectIntermediateNode(typeName, memberName, typeSpan, memberSpan, isMalformed)); } } private class Visitor : IntermediateNodeWalker { - public IList Directives { get; } - = new List(); + public IList Directives { get; } = []; public override void VisitDirective(DirectiveIntermediateNode node) { @@ -57,5 +59,13 @@ internal class ComponentInjectDirectivePass : IntermediateNodePassBase, IRazorDi Directives.Add(node); } } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + if (node.Directive == ComponentInjectDirective.Directive) + { + Directives.Add(node); + } + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs index 13812ccf59..3642e635ce 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Razor.Language.Components; @@ -18,13 +19,14 @@ internal class ComponentInjectIntermediateNode : ExtensionIntermediateNode "private" // Encapsulation is the default }; - public ComponentInjectIntermediateNode(string typeName, string memberName, SourceSpan? typeSpan, SourceSpan? memberSpan) + public ComponentInjectIntermediateNode(string typeName, string memberName, SourceSpan? typeSpan, SourceSpan? memberSpan, bool isMalformed) { TypeName = typeName; MemberName = memberName; TypeSpan = typeSpan; MemberSpan = memberSpan; - } + IsMalformed = isMalformed; + } public string TypeName { get; } @@ -34,8 +36,9 @@ internal class ComponentInjectIntermediateNode : ExtensionIntermediateNode public SourceSpan? MemberSpan { get; } - public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + public bool IsMalformed { get; } + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; public override void Accept(IntermediateNodeVisitor visitor) { @@ -59,13 +62,26 @@ internal class ComponentInjectIntermediateNode : ExtensionIntermediateNode throw new ArgumentNullException(nameof(context)); } - context.CodeWriter.WriteAutoPropertyDeclaration( - _injectedPropertyModifiers, - TypeName, - MemberName, - TypeSpan, - MemberSpan, - context, - defaultValue: true); + if (TypeName == string.Empty && TypeSpan.HasValue && !context.Options.DesignTime) + { + // if we don't even have a type name, just emit an empty mapped region so that intellisense still works + context.CodeWriter.BuildEnhancedLinePragma(TypeSpan.Value, context).Dispose(); + } + else + { + var memberName = MemberName ?? "Member_" + DefaultTagHelperTargetExtension.GetDeterministicId(context); + + if (!context.Options.DesignTime || !IsMalformed) + { + context.CodeWriter.WriteAutoPropertyDeclaration( + _injectedPropertyModifiers, + TypeName, + memberName, + TypeSpan, + MemberSpan, + context, + defaultValue: true); + } + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperTargetExtension.cs index 381812e40f..2df263902a 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/DefaultTagHelperTargetExtension.cs @@ -101,12 +101,8 @@ internal sealed class DefaultTagHelperTargetExtension : IDefaultTagHelperTargetE // Assign a unique ID for this instance of the source HTML tag. This must be unique // per call site, e.g. if the tag is on the view twice, there should be two IDs. - var uniqueId = context.Options.SuppressUniqueIds; - if (uniqueId == null) - { - uniqueId = GetDeterministicId(context); - } - + var uniqueId = GetDeterministicId(context); + context.CodeWriter.WriteStringLiteral(node.TagName) .WriteParameterSeparator() .Write(TagModeTypeName) @@ -650,10 +646,13 @@ internal sealed class DefaultTagHelperTargetExtension : IDefaultTagHelperTargetE // Internal for testing internal static string GetDeterministicId(CodeRenderingContext context) { - // Use the file checksum along with the absolute position in the generated code to create a unique id for each tag helper call site. - var checksum = ChecksumUtilities.BytesToString(context.SourceDocument.Text.GetChecksum()); - var uniqueId = checksum + context.CodeWriter.Location.AbsoluteIndex; - + var uniqueId = context.Options.SuppressUniqueIds; + if (uniqueId is null) + { + // Use the file checksum along with the absolute position in the generated code to create a unique id for each tag helper call site. + var checksum = ChecksumUtilities.BytesToString(context.SourceDocument.Text.GetChecksum()); + uniqueId = checksum + context.CodeWriter.Location.AbsoluteIndex; + } return uniqueId; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpan.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpan.cs index 919e5fbc4b..4686220faf 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpan.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpan.cs @@ -108,6 +108,11 @@ public struct SourceSpan : IEquatable endCharacterIndex); } + internal readonly SourceSpan GetZeroWidthEndSpan() + { + return new SourceSpan(FilePath, AbsoluteIndex + EndCharacterIndex, LineIndex, characterIndex: EndCharacterIndex, length: 0, lineCount: 0, EndCharacterIndex); + } + public static bool operator ==(SourceSpan left, SourceSpan right) { return left.Equals(right); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectDirective.cs index f7527b9016..9d803064c1 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectDirective.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectDirective.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -55,18 +56,20 @@ public static class InjectDirective for (var i = visitor.Directives.Count - 1; i >= 0; i--) { var directive = visitor.Directives[i]; - var tokens = directive.Tokens.ToArray(); - if (tokens.Length < 2) - { - continue; - } + var tokens = directive.Children.OfType().ToArray(); + var isMalformed = directive is MalformedDirectiveIntermediateNode; - var typeName = tokens[0].Content; - var typeSpan = tokens[0].Source; - var memberName = tokens[1].Content; - var memberSpan = tokens[1].Source; + var hasType = tokens.Length > 0 && !string.IsNullOrWhiteSpace(tokens[0].Content); + Debug.Assert(hasType || isMalformed); + var typeName = hasType ? tokens[0].Content : string.Empty; + var typeSpan = hasType ? tokens[0].Source : directive.Source?.GetZeroWidthEndSpan(); - if (!properties.Add(memberName)) + var hasMemberName = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1].Content); + Debug.Assert(hasMemberName || isMalformed); + var memberName = hasMemberName ? tokens[1].Content : null; + var memberSpan = hasMemberName ? tokens[1].Source : null; + + if (hasMemberName && !properties.Add(memberName)) { continue; } @@ -86,7 +89,8 @@ public static class InjectDirective TypeName = typeName, MemberName = memberName, TypeSource = typeSpan, - MemberSource = memberSpan + MemberSource = memberSpan, + IsMalformed = isMalformed }; visitor.Class.Children.Add(injectNode); @@ -98,7 +102,7 @@ public static class InjectDirective { public ClassDeclarationIntermediateNode Class { get; private set; } - public IList Directives { get; } = new List(); + public IList Directives { get; } = []; public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) { @@ -117,5 +121,13 @@ public static class InjectDirective Directives.Add(node); } } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + Directives.Add(node); + } + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectIntermediateNode.cs index 21eda2d793..72bda5a3dd 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectIntermediateNode.cs @@ -20,6 +20,8 @@ public class InjectIntermediateNode : ExtensionIntermediateNode public SourceSpan? MemberSource { get; set; } + public bool IsMalformed { get; set; } + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; public override void Accept(IntermediateNodeVisitor visitor) @@ -60,5 +62,6 @@ public class InjectIntermediateNode : ExtensionIntermediateNode formatter.WriteProperty(nameof(MemberName), MemberName); formatter.WriteProperty(nameof(TypeName), TypeName); + formatter.WriteProperty(nameof(IsMalformed), IsMalformed.ToString()); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectTargetExtension.cs index fb26d69904..dd2af30c9c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version1_X/InjectTargetExtension.cs @@ -5,6 +5,7 @@ using System; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; @@ -26,10 +27,19 @@ public class InjectTargetExtension : IInjectTargetExtension if (!context.Options.DesignTime && !string.IsNullOrWhiteSpace(node.TypeSource?.FilePath)) { - context.CodeWriter.WriteLine(RazorInjectAttribute); - context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, node.MemberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + if (node.TypeName == "") + { + // if we don't even have a type name, just emit an empty mapped region so that intellisense still works + context.CodeWriter.BuildEnhancedLinePragma(node.TypeSource.Value, context).Dispose(); + } + else + { + context.CodeWriter.WriteLine(RazorInjectAttribute); + var memberName = node.MemberName ?? "Member_" + DefaultTagHelperTargetExtension.GetDeterministicId(context); + context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, memberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + } } - else + else if (!node.IsMalformed) { var property = $"public {node.TypeName} {node.MemberName} {{ get; private set; }}"; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectDirective.cs index 1ad821bc8a..951e955bcf 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectDirective.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectDirective.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -55,18 +56,20 @@ public static class InjectDirective for (var i = visitor.Directives.Count - 1; i >= 0; i--) { var directive = visitor.Directives[i]; - var tokens = directive.Tokens.ToArray(); - if (tokens.Length < 2) - { - continue; - } + var tokens = directive.Children.OfType().ToArray(); + var isMalformed = directive is MalformedDirectiveIntermediateNode; - var typeName = tokens[0].Content; - var typeSpan = tokens[0].Source; - var memberName = tokens[1].Content; - var memberSpan = tokens[1].Source; + var hasType = tokens.Length > 0 && !string.IsNullOrWhiteSpace(tokens[0].Content); + Debug.Assert(hasType || isMalformed); + var typeName = hasType ? tokens[0].Content : string.Empty; + var typeSpan = hasType ? tokens[0].Source : directive.Source?.GetZeroWidthEndSpan(); - if (!properties.Add(memberName)) + var hasMemberName = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1].Content); + Debug.Assert(hasMemberName || isMalformed); + var memberName = hasMemberName ? tokens[1].Content : null; + var memberSpan = hasMemberName ? tokens[1].Source : null; + + if (hasMemberName && !properties.Add(memberName)) { continue; } @@ -86,7 +89,8 @@ public static class InjectDirective TypeName = typeName, MemberName = memberName, TypeSource = typeSpan, - MemberSource = memberSpan + MemberSource = memberSpan, + IsMalformed = isMalformed }; visitor.Class.Children.Add(injectNode); @@ -98,7 +102,7 @@ public static class InjectDirective { public ClassDeclarationIntermediateNode Class { get; private set; } - public IList Directives { get; } = new List(); + public IList Directives { get; } = []; public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) { @@ -117,5 +121,13 @@ public static class InjectDirective Directives.Add(node); } } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + Directives.Add(node); + } + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectIntermediateNode.cs index 9cab9ce14e..d9b3d960ed 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectIntermediateNode.cs @@ -20,6 +20,8 @@ public class InjectIntermediateNode : ExtensionIntermediateNode public SourceSpan? MemberSource { get; set; } + public bool IsMalformed { get; set; } + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; public override void Accept(IntermediateNodeVisitor visitor) @@ -60,5 +62,6 @@ public class InjectIntermediateNode : ExtensionIntermediateNode formatter.WriteProperty(nameof(MemberName), MemberName); formatter.WriteProperty(nameof(TypeName), TypeName); + formatter.WriteProperty(nameof(IsMalformed), IsMalformed.ToString()); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectTargetExtension.cs index 6447555042..865ba972e1 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/InjectTargetExtension.cs @@ -5,6 +5,7 @@ using System; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; @@ -26,10 +27,19 @@ public class InjectTargetExtension : IInjectTargetExtension if (!context.Options.DesignTime && !string.IsNullOrWhiteSpace(node.TypeSource?.FilePath)) { - context.CodeWriter.WriteLine(RazorInjectAttribute); - context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, node.MemberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + if (node.TypeName == "") + { + // if we don't even have a type name, just emit an empty mapped region so that intellisense still works + context.CodeWriter.BuildEnhancedLinePragma(node.TypeSource.Value, context).Dispose(); + } + else + { + context.CodeWriter.WriteLine(RazorInjectAttribute); + var memberName = node.MemberName ?? "Member_" + DefaultTagHelperTargetExtension.GetDeterministicId(context); + context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, memberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + } } - else + else if(!node.IsMalformed) { var property = $"public {node.TypeName} {node.MemberName} {{ get; private set; }}"; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs index 569a83748b..15ed4fc032 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -62,18 +63,20 @@ public static class InjectDirective for (var i = visitor.Directives.Count - 1; i >= 0; i--) { var directive = visitor.Directives[i]; - var tokens = directive.Tokens.ToArray(); - if (tokens.Length < 2) - { - continue; - } + var tokens = directive.Children.OfType().ToArray(); + var isMalformed = directive is MalformedDirectiveIntermediateNode; - var typeName = tokens[0].Content; - var typeSpan = tokens[0].Source; - var memberName = tokens[1].Content; - var memberSpan = tokens[1].Source; + var hasType = tokens.Length > 0 && !string.IsNullOrWhiteSpace(tokens[0].Content); + Debug.Assert(hasType || isMalformed); + var typeName = hasType ? tokens[0].Content : string.Empty; + var typeSpan = hasType ? tokens[0].Source : directive.Source?.GetZeroWidthEndSpan(); - if (!properties.Add(memberName)) + var hasMemberName = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1].Content); + Debug.Assert(hasMemberName || isMalformed); + var memberName = hasMemberName ? tokens[1].Content : null; + var memberSpan = hasMemberName ? tokens[1].Source : null; + + if (hasMemberName && !properties.Add(memberName)) { continue; } @@ -93,7 +96,8 @@ public static class InjectDirective TypeName = typeName, MemberName = memberName, TypeSource = typeSpan, - MemberSource = memberSpan + MemberSource = memberSpan, + IsMalformed = isMalformed }; visitor.Class.Children.Add(injectNode); @@ -105,7 +109,7 @@ public static class InjectDirective { public ClassDeclarationIntermediateNode Class { get; private set; } - public IList Directives { get; } = new List(); + public IList Directives { get; } = []; public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) { @@ -124,5 +128,13 @@ public static class InjectDirective Directives.Add(node); } } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + Directives.Add(node); + } + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectIntermediateNode.cs index de9c29ebea..8ef9bbbe3c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectIntermediateNode.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectIntermediateNode.cs @@ -20,6 +20,8 @@ public class InjectIntermediateNode : ExtensionIntermediateNode public SourceSpan? MemberSource { get; set; } + public bool IsMalformed { get; set; } + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; public override void Accept(IntermediateNodeVisitor visitor) @@ -60,5 +62,6 @@ public class InjectIntermediateNode : ExtensionIntermediateNode formatter.WriteProperty(nameof(MemberName), MemberName); formatter.WriteProperty(nameof(TypeName), TypeName); + formatter.WriteProperty(nameof(IsMalformed), IsMalformed.ToString()); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs index 506483f6a8..46e83e4cae 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs @@ -5,6 +5,7 @@ using System; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Extensions; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -26,10 +27,19 @@ public class InjectTargetExtension : IInjectTargetExtension if (!context.Options.DesignTime && !string.IsNullOrWhiteSpace(node.TypeSource?.FilePath)) { - context.CodeWriter.WriteLine(RazorInjectAttribute); - context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, node.MemberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + if (node.TypeName == "") + { + // if we don't even have a type name, just emit an empty mapped region so that intellisense still works + context.CodeWriter.BuildEnhancedLinePragma(node.TypeSource.Value, context).Dispose(); + } + else + { + context.CodeWriter.WriteLine(RazorInjectAttribute); + var memberName = node.MemberName ?? "Member_" + DefaultTagHelperTargetExtension.GetDeterministicId(context); + context.CodeWriter.WriteAutoPropertyDeclaration(["public"], node.TypeName, memberName, node.TypeSource, node.MemberSource, context, privateSetter: true, defaultValue: true); + } } - else + else if (!node.IsMalformed) { var property = $"public {node.TypeName} {node.MemberName} {{ get; private set; }}"; if (!context.Options.SuppressNullabilityEnforcement) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs index 384a9c9bb9..2e0fa3b877 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs @@ -122,6 +122,8 @@ public class RazorIntegrationTestBase b.Features.Add(new SetNewLineOptionFeature(LineEnding)); } + b.Features.Add(new SuppressUniqueIdsPhase()); + b.Features.Add(new CompilationTagHelperFeature()); b.Features.Add(new DefaultMetadataReferenceFeature() { @@ -497,6 +499,16 @@ public class RazorIntegrationTestBase } } + private sealed class SuppressUniqueIdsPhase : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature + { + public int Order { get; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) + { + options.SuppressUniqueIds = "__UniqueIdSuppressedForTesting__"; + } + } + private class TestImportProjectFeature : IImportProjectFeature { private readonly List _imports;