From 3e5154c56e37aaefff7c86e45f2434887bdcb1ee Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Sat, 28 Feb 2026 17:16:38 -0800 Subject: [PATCH 1/5] Add @utf8HtmlLiterals directive for opt-in UTF-8 HTML string literals Implements the @utf8HtmlLiterals directive (with boolean token) that when enabled causes the Razor compiler to emit HTML literal blocks as C# UTF-8 string literals ("..."u8) instead of regular string literals. This allows the page's base class to provide a WriteLiteral(ReadOnlySpan) overload that writes pre-encoded UTF-8 bytes directly to the output, avoiding runtime UTF-16 to UTF-8 encoding and associated memory allocations. Key changes: - Add WriteHtmlUtf8StringLiterals flag to RazorCodeGenerationOptions - Add Utf8HtmlLiteralsDirective and Utf8HtmlLiteralsDirectivePass - Register directive for Legacy (.cshtml) files, gated on Version_11_0 - Modify CodeWriterExtensions to append u8 suffix when flag is set - Modify RuntimeNodeWriter to pass flag from options to code writer - Use documentNode.Options in lowering phase (respects directive passes) - Relax directive keyword validation to allow digits (not just letters) Fixes #8429 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CodeGenerationIntegrationTest.cs | 37 ++++++++ .../Utf8HtmlLiterals.cshtml | 9 ++ .../Utf8HtmlLiterals_Runtime.codegen.cs | 95 +++++++++++++++++++ .../Utf8HtmlLiterals_Runtime.ir.txt | 47 +++++++++ .../CodeGeneration/RuntimeNodeWriterTest.cs | 46 +++++++++ .../test/DirectiveDescriptorTest.cs | 4 +- .../CodeGeneration/CodeWriterExtensions.cs | 24 +++-- .../CodeGeneration/RuntimeNodeWriter.cs | 2 +- .../DefaultRazorCSharpLoweringPhase.cs | 2 +- .../src/Language/DirectiveDescriptor.cs | 6 +- .../Extensions/Utf8HtmlLiteralsDirective.cs | 31 ++++++ .../Utf8HtmlLiteralsDirectivePass.cs | 28 ++++++ .../src/Language/Legacy/SyntaxConstants.cs | 1 + .../RazorCodeGenerationOptions.Builder.cs | 9 ++ .../RazorCodeGenerationOptions.Flags.cs | 1 + .../Language/RazorCodeGenerationOptions.cs | 14 ++- .../src/Language/RazorProjectEngine.cs | 5 + .../src/Language/Resources.resx | 11 ++- .../TestCodeRenderingContext.cs | 8 +- 19 files changed, 363 insertions(+), 17 deletions(-) create mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml create mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs create mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs index 01c1f17d866..979bf9f357b 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -926,6 +926,43 @@ public void InvalidCode_EmptyImplicitExpression_Runtime() CompileToAssembly(generated, throwOnFailure: false, ignoreRazorDiagnostics: true); } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] + public void Utf8HtmlLiterals_Runtime() + { + // Arrange + _configuration = new(RazorLanguageVersion.Preview, "MVC-3.0", Extensions: []); + + AddCSharpSyntaxTree(""" + + using System; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc.Razor; + + public abstract class MyUtf8PageBase : RazorPage + { + public void WriteLiteral(ReadOnlySpan value) + { + WriteLiteral(System.Text.Encoding.UTF8.GetString(value)); + } + } + + """); + + var projectItem = CreateProjectItemFromFile(); + + // Act + var compiled = CompileToAssembly(projectItem, designTime: false); + + // Assert + AssertDocumentNodeMatchesBaseline(compiled.CodeDocument.GetDocumentNode()); + AssertCSharpDocumentMatchesBaseline(compiled.CodeDocument.GetCSharpDocument()); + AssertLinePragmas(compiled.CodeDocument); + + // Verify that the generated code contains UTF-8 string literals + var generatedCode = compiled.CodeDocument.GetCSharpDocument().Text.ToString(); + Assert.Contains("u8)", generatedCode); + } + #endregion #region DesignTime diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml new file mode 100644 index 00000000000..5efa6f29031 --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml @@ -0,0 +1,9 @@ +@inherits MyUtf8PageBase +@utf8HtmlLiterals true + + + +

Hello World

+

This is UTF-8 encoded HTML content.

+ + diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs new file mode 100644 index 00000000000..473add1badc --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs @@ -0,0 +1,95 @@ +#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "22fb469a637f8d91884dc0bc96afe67098c3248cafe342540e23642267add1fc" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals), @"mvc.1.0.view", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] +namespace AspNetCoreGeneratedDocument +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Mvc; + using global::Microsoft.AspNetCore.Mvc.Rendering; + using global::Microsoft.AspNetCore.Mvc.ViewFeatures; + #line default + #line hidden + [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"Sha256", @"22fb469a637f8d91884dc0bc96afe67098c3248cafe342540e23642267add1fc", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] + [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] + [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] + #nullable restore + internal sealed class TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals : +#nullable restore +#line (1,11)-(1,25) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml" +MyUtf8PageBase + +#line default +#line hidden +#nullable disable + + #nullable disable + { + #line hidden + #pragma warning disable 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext; + #pragma warning restore 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner(); + #pragma warning disable 0169 + private string __tagHelperStringValueBuffer; + #pragma warning restore 0169 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null; + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager + { + get + { + if (__backed__tagHelperScopeManager == null) + { + __backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope); + } + return __backed__tagHelperScopeManager; + } + } + private global::Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper; + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + WriteLiteral("\r\n\r\n"u8); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("body", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "__UniqueIdSuppressedForTesting__", async() => { + WriteLiteral("\r\n

Hello World

\r\n

This is UTF-8 encoded HTML content.

\r\n"u8); + } + ); + __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper); + await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + if (!__tagHelperExecutionContext.Output.IsContentModified) + { + await __tagHelperExecutionContext.SetOutputContentAsync(); + } + Write(__tagHelperExecutionContext.Output); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n\r\n"u8); + } + #pragma warning restore 1998 + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } = default!; + #nullable disable + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt new file mode 100644 index 00000000000..b9ea7b0697f --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt @@ -0,0 +1,47 @@ +Document - + RazorCompiledItemAttribute - + NamespaceDeclaration - - AspNetCoreGeneratedDocument + UsingDirective - (1:0,1 [20] ) - global::System + UsingDirective - (24:1,1 [40] ) - global::System.Collections.Generic + UsingDirective - (67:2,1 [25] ) - global::System.Linq + UsingDirective - (95:3,1 [36] ) - global::System.Threading.Tasks + UsingDirective - (134:4,1 [38] ) - global::Microsoft.AspNetCore.Mvc + UsingDirective - (175:5,1 [48] ) - global::Microsoft.AspNetCore.Mvc.Rendering + UsingDirective - (226:6,1 [51] ) - global::Microsoft.AspNetCore.Mvc.ViewFeatures + RazorSourceChecksumAttribute - + RazorCompiledItemMetadataAttribute - + CreateNewOnMetadataUpdateAttribute - + ClassDeclaration - - internal sealed - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals - MyUtf8PageBase - + DefaultTagHelperRuntime - + FieldDeclaration - - private - global::Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper - __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper + MethodDeclaration - - public async override - global::System.Threading.Tasks.Task - ExecuteAsync + HtmlContent - (50:2,0 [10] Utf8HtmlLiterals.cshtml) + LazyIntermediateToken - (50:2,0 [2] Utf8HtmlLiterals.cshtml) - Html - \n + LazyIntermediateToken - (52:3,0 [5] Utf8HtmlLiterals.cshtml) - Html - + LazyIntermediateToken - (58:3,6 [2] Utf8HtmlLiterals.cshtml) - Html - \n + TagHelper - (60:4,0 [89] Utf8HtmlLiterals.cshtml) - body - TagMode.StartTagAndEndTag + DefaultTagHelperBody - + HtmlContent - (66:4,6 [76] Utf8HtmlLiterals.cshtml) + LazyIntermediateToken - (66:4,6 [6] Utf8HtmlLiterals.cshtml) - Html - \n + LazyIntermediateToken - (72:5,4 [3] Utf8HtmlLiterals.cshtml) - Html -

+ LazyIntermediateToken - (76:5,8 [11] Utf8HtmlLiterals.cshtml) - Html - Hello World + LazyIntermediateToken - (87:5,19 [5] Utf8HtmlLiterals.cshtml) - Html -

+ LazyIntermediateToken - (92:5,24 [6] Utf8HtmlLiterals.cshtml) - Html - \n + LazyIntermediateToken - (98:6,4 [2] Utf8HtmlLiterals.cshtml) - Html -

+ LazyIntermediateToken - (101:6,7 [35] Utf8HtmlLiterals.cshtml) - Html - This is UTF-8 encoded HTML content. + LazyIntermediateToken - (136:6,42 [4] Utf8HtmlLiterals.cshtml) - Html -

+ LazyIntermediateToken - (140:6,46 [2] Utf8HtmlLiterals.cshtml) - Html - \n + DefaultTagHelperCreate - - Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper + DefaultTagHelperExecute - + HtmlContent - (149:7,7 [11] Utf8HtmlLiterals.cshtml) + LazyIntermediateToken - (149:7,7 [2] Utf8HtmlLiterals.cshtml) - Html - \n + LazyIntermediateToken - (151:8,0 [7] Utf8HtmlLiterals.cshtml) - Html - + LazyIntermediateToken - (158:8,7 [2] Utf8HtmlLiterals.cshtml) - Html - \n + Inject - + Inject - + Inject - + Inject - + Inject - diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/RuntimeNodeWriterTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/RuntimeNodeWriterTest.cs index 2384f4452b6..9564d215e5d 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/RuntimeNodeWriterTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/RuntimeNodeWriterTest.cs @@ -445,6 +445,52 @@ public void WriteHtmlContent_RendersContentCorrectly() ignoreLineEndingDifferences: true); } + [Fact] + public void WriteHtmlContent_Utf8_RendersContentCorrectly() + { + // Arrange + var writer = RuntimeNodeWriter.Instance; + using var context = TestCodeRenderingContext.CreateRuntime(writeHtmlUtf8StringLiterals: true); + + var node = new HtmlContentIntermediateNode(); + node.Children.Add(IntermediateNodeFactory.HtmlToken("SomeContent")); + + // Act + writer.WriteHtmlContent(context, node); + + // Assert + var csharp = context.CodeWriter.GetText().ToString(); + Assert.Equal( +@"WriteLiteral(""SomeContent""u8); +", + csharp, + ignoreLineEndingDifferences: true); + } + + [Fact] + public void WriteHtmlContent_Utf8_LargeStringLiteral_UsesMultipleWrites() + { + // Arrange + var writer = RuntimeNodeWriter.Instance; + using var context = TestCodeRenderingContext.CreateRuntime(writeHtmlUtf8StringLiterals: true); + + var node = new HtmlContentIntermediateNode(); + node.Children.Add(IntermediateNodeFactory.HtmlToken(new string('*', 2000))); + + // Act + writer.WriteHtmlContent(context, node); + + // Assert + var csharp = context.CodeWriter.GetText().ToString(); + Assert.Equal(string.Format( + CultureInfo.InvariantCulture, +@"WriteLiteral(@""{0}""u8); +WriteLiteral(@""{1}""u8); +", new string('*', 1024), new string('*', 976)), + csharp, + ignoreLineEndingDifferences: true); + } + [Fact] public void WriteHtmlContent_LargeStringLiteral_UsesMultipleWrites() { diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs index 0afd595e63f..f1a75539ed5 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs @@ -125,7 +125,7 @@ public void Build_ValidatesDirectiveKeyword_EmptyIsInvalid() var ex = Assert.Throws(() => DirectiveDescriptor.CreateSingleLineDirective("")); // Assert - Assert.Equal("Invalid directive keyword ''. Directives must have a non-empty keyword that consists only of letters.", ex.Message); + Assert.Equal("Invalid directive keyword ''. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits.", ex.Message); } [Fact] @@ -135,7 +135,7 @@ public void Build_ValidatesDirectiveKeyword_InvalidCharacter() var ex = Assert.Throws(() => DirectiveDescriptor.CreateSingleLineDirective("test_directive")); // Assert - Assert.Equal("Invalid directive keyword 'test_directive'. Directives must have a non-empty keyword that consists only of letters.", ex.Message); + Assert.Equal("Invalid directive keyword 'test_directive'. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits.", ex.Message); } [Fact] diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeWriterExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeWriterExtensions.cs index 6741b8799e5..945763d0345 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeWriterExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeWriterExtensions.cs @@ -265,18 +265,18 @@ public static CodeWriter WriteStartNewObject(this CodeWriter writer, string type return writer.Write("new ").Write(typeName).Write("("); } - public static CodeWriter WriteStringLiteral(this CodeWriter writer, string literal) - => writer.WriteStringLiteral(literal.AsMemory()); + public static CodeWriter WriteStringLiteral(this CodeWriter writer, string literal, bool utf8 = false) + => writer.WriteStringLiteral(literal.AsMemory(), utf8); - public static CodeWriter WriteStringLiteral(this CodeWriter writer, ReadOnlyMemory literal) + public static CodeWriter WriteStringLiteral(this CodeWriter writer, ReadOnlyMemory literal, bool utf8 = false) { if (literal.Length >= 256 && literal.Length <= 1500 && literal.Span.IndexOf('\0') == -1) { - WriteVerbatimStringLiteral(writer, literal); + WriteVerbatimStringLiteral(writer, literal, utf8); } else { - WriteCStyleStringLiteral(writer, literal); + WriteCStyleStringLiteral(writer, literal, utf8); } return writer; @@ -900,7 +900,7 @@ public static CodeWriter WriteSeparatedList(this CodeWriter writer, string se return writer; } - private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory literal) + private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory literal, bool utf8 = false) { writer.Write("@\""); @@ -926,10 +926,15 @@ private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory writer.Write("\""); + if (utf8) + { + writer.Write("u8"); + } + writer.CurrentIndent = oldIndent; } - private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory literal) + private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory literal, bool utf8 = false) { // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM writer.Write("\""); @@ -983,6 +988,11 @@ private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory content) { context.CodeWriter .WriteStartMethodInvocation(WriteHtmlContentMethod) - .WriteStringLiteral(content) + .WriteStringLiteral(content, context.Options.WriteHtmlUtf8StringLiterals) .WriteEndMethodInvocation(); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs index 00abcedbdb2..2b058c97f1b 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs @@ -41,7 +41,7 @@ private static RazorCSharpDocument WriteDocument(RazorCodeDocument codeDocument, codeTarget.CreateNodeWriter(), codeDocument.Source, documentNode, - codeDocument.CodeGenerationOptions); + documentNode.Options ?? codeDocument.CodeGenerationOptions); context.SetVisitor(new Visitor(context, codeTarget, cancellationToken)); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs index 050b5ba155f..09d4b23863f 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs @@ -202,14 +202,14 @@ public DefaultDirectiveDescriptorBuilder(string name, DirectiveKind kind) public DirectiveDescriptor Build() { - if (Directive.Length == 0) + if (Directive.Length == 0 || !char.IsLetter(Directive[0])) { throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive)); } - for (var i = 0; i < Directive.Length; i++) + for (var i = 1; i < Directive.Length; i++) { - if (!char.IsLetter(Directive[i])) + if (!char.IsLetterOrDigit(Directive[i])) { throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive)); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs new file mode 100644 index 00000000000..40e681cdead --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language.Extensions; + +public static class Utf8HtmlLiteralsDirective +{ + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + SyntaxConstants.CSharp.Utf8HtmlLiteralsKeyword, + DirectiveKind.SingleLine, + builder => + { + builder.AddBooleanToken(Resources.Utf8HtmlLiteralsDirective_BooleanToken_Name, Resources.Utf8HtmlLiteralsDirective_BooleanToken_Description); + builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.Utf8HtmlLiteralsDirective_Description; + }); + + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive, RazorFileKind.Legacy); + builder.Features.Add(new Utf8HtmlLiteralsDirectivePass()); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs new file mode 100644 index 00000000000..6de2028bc46 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Extensions; + +public sealed class Utf8HtmlLiteralsDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass +{ + protected override void ExecuteCore( + RazorCodeDocument codeDocument, + DocumentIntermediateNode documentNode, + CancellationToken cancellationToken) + { + foreach (var directive in documentNode.FindDirectiveReferences(Utf8HtmlLiteralsDirective.Directive)) + { + var token = directive.Node.Tokens.FirstOrDefault(); + if (token != null && + string.Equals(token.Content, "true", System.StringComparison.Ordinal)) + { + documentNode.Options = documentNode.Options.WithFlags(writeHtmlUtf8StringLiterals: true); + break; + } + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs index b11b2f319d1..591239701dd 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs @@ -26,6 +26,7 @@ public static class CSharp public const string ElseIfKeyword = "else if"; public const string NamespaceKeyword = "namespace"; public const string ClassKeyword = "class"; + public const string Utf8HtmlLiteralsKeyword = "utf8HtmlLiterals"; // Not supported. Only used for error cases. public const string HelperKeyword = "helper"; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs index 812a115c35d..82754b8a46b 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs @@ -158,6 +158,15 @@ public bool RemapLinePragmaPathsOnWindows set => _flags.UpdateFlag(Flags.RemapLinePragmaPathsOnWindows, value); } + /// + /// Gets or sets a value that determines if HTML literals should be written as C# UTF-8 string literals. + /// + public bool WriteHtmlUtf8StringLiterals + { + get => _flags.IsFlagSet(Flags.WriteHtmlUtf8StringLiterals); + set => _flags.UpdateFlag(Flags.WriteHtmlUtf8StringLiterals, value); + } + public RazorCodeGenerationOptions ToOptions() => new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, _flags); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Flags.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Flags.cs index 71a3641b612..4ff206c8e8d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Flags.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Flags.cs @@ -22,6 +22,7 @@ private enum Flags UseEnhancedLinePragma = 1 << 9, SuppressAddComponentParameter = 1 << 10, RemapLinePragmaPathsOnWindows = 1 << 11, + WriteHtmlUtf8StringLiterals = 1 << 12, DefaultFlags = UseEnhancedLinePragma, DefaultDesignTimeFlags = DesignTime | SuppressMetadataAttributes | UseEnhancedLinePragma | RemapLinePragmaPathsOnWindows diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs index 3774b8783c1..f315964661a 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs @@ -159,6 +159,12 @@ public bool SuppressAddComponentParameter public bool RemapLinePragmaPathsOnWindows => (_flags & Flags.RemapLinePragmaPathsOnWindows) == Flags.RemapLinePragmaPathsOnWindows; + /// + /// Gets a value that determines if HTML literals should be written as C# UTF-8 string literals. + /// + public bool WriteHtmlUtf8StringLiterals + => (_flags & Flags.WriteHtmlUtf8StringLiterals) == Flags.WriteHtmlUtf8StringLiterals; + public RazorCodeGenerationOptions WithIndentSize(int value) => IndentSize == value ? this @@ -196,7 +202,8 @@ public RazorCodeGenerationOptions WithFlags( Optional supportLocalizedComponentNames = default, Optional useEnhancedLinePragma = default, Optional suppressAddComponentParameter = default, - Optional remapLinePragmaPathsOnWindows = default) + Optional remapLinePragmaPathsOnWindows = default, + Optional writeHtmlUtf8StringLiterals = default) { var flags = _flags; @@ -260,6 +267,11 @@ public RazorCodeGenerationOptions WithFlags( flags.UpdateFlag(Flags.RemapLinePragmaPathsOnWindows, remapLinePragmaPathsOnWindows.Value); } + if (writeHtmlUtf8StringLiterals.HasValue) + { + flags.UpdateFlag(Flags.WriteHtmlUtf8StringLiterals, writeHtmlUtf8StringLiterals.Value); + } + return flags == _flags ? this : new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, flags); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs index 3427898e1a1..e1d29e4f36c 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -314,6 +314,11 @@ public static RazorProjectEngine Create( builder.Features.Add(new ViewCssScopePass()); } + if (configuration.LanguageVersion >= RazorLanguageVersion.Version_11_0) + { + Utf8HtmlLiteralsDirective.Register(builder); + } + if (configuration.LanguageVersion >= RazorLanguageVersion.Version_3_0) { FunctionsDirective.Register(builder); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx index 2f19609b1af..51d34dae602 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx @@ -177,7 +177,7 @@ The document type '{0}' does not support the extension '{1}'. - Invalid directive keyword '{0}'. Directives must have a non-empty keyword that consists only of letters. + Invalid directive keyword '{0}'. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits. A non-optional directive token cannot follow an optional directive token. @@ -619,4 +619,13 @@ Parent has not been set. + + Opt-in to HTML literals being emitted as C# UTF-8 string literals for this page. + + + Utf8HtmlLiterals + + + Whether the current page should emit HTML literals as C# UTF-8 string literals. + \ No newline at end of file diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs index 870132e673c..32c16f09e71 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs @@ -31,7 +31,8 @@ public static CodeRenderingContext CreateRuntime( string newLineString = null, string suppressUniqueIds = "test", RazorSourceDocument source = null, - IntermediateNodeWriter nodeWriter = null) + IntermediateNodeWriter nodeWriter = null, + bool writeHtmlUtf8StringLiterals = false) { nodeWriter ??= RuntimeNodeWriter.Instance; source ??= TestRazorSourceDocument.Create(); @@ -39,6 +40,11 @@ public static CodeRenderingContext CreateRuntime( var options = ConfigureOptions(RazorCodeGenerationOptions.Default, newLineString, suppressUniqueIds); + if (writeHtmlUtf8StringLiterals) + { + options = options.WithFlags(writeHtmlUtf8StringLiterals: true); + } + var context = new CodeRenderingContext(nodeWriter, source, documentNode, options); context.SetVisitor(new RenderChildrenVisitor(context.CodeWriter)); From 1c1b859fe9b60422a674b63e968702f1c28a4c94 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 3 Mar 2026 12:51:22 -0800 Subject: [PATCH 2/5] Auto-detect UTF-8 literal WriteLiteral usage Replace @utf8HtmlLiterals directive wiring with automatic detection based on whether the inherited base type exposes a callable WriteLiteral(ReadOnlySpan) overload. Update compiler/source-generator plumbing and tests accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CodeGenerationIntegrationTest.cs | 57 +++++++-- .../Utf8HtmlLiterals.cshtml | 9 -- .../Utf8HtmlLiterals_Runtime.codegen.cs | 95 -------------- .../Utf8HtmlLiterals_Runtime.ir.txt | 47 ------- .../test/DirectiveDescriptorTest.cs | 4 +- .../src/CSharp/CompilationExtensions.cs | 40 +++++- .../src/Language/DirectiveDescriptor.cs | 6 +- .../Extensions/Utf8HtmlLiteralsDirective.cs | 31 ----- .../Utf8HtmlLiteralsDirectivePass.cs | 28 ----- .../Utf8WriteLiteralDetectionPass.cs | 119 ++++++++++++++++++ .../src/Language/Legacy/SyntaxConstants.cs | 2 - .../src/Language/RazorProjectEngine.cs | 6 +- .../src/Language/Resources.resx | 13 +- .../RazorSourceGenerationOptions.cs | 4 + .../RazorSourceGenerator.Helpers.cs | 5 + .../RazorSourceGenerator.RazorProviders.cs | 5 +- .../SourceGenerators/RazorSourceGenerator.cs | 1 + .../RazorSourceGeneratorCshtmlTests.cs | 85 ++++++++++++- 18 files changed, 308 insertions(+), 249 deletions(-) delete mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml delete mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs delete mode 100644 src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt delete mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs delete mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8WriteLiteralDetectionPass.cs diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs index 979bf9f357b..3c821fac262 100644 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -927,7 +927,7 @@ public void InvalidCode_EmptyImplicitExpression_Runtime() } [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] - public void Utf8HtmlLiterals_Runtime() + public void Utf8HtmlLiterals_AutoDetectedFromInherits_Runtime() { // Arrange _configuration = new(RazorLanguageVersion.Preview, "MVC-3.0", Extensions: []); @@ -948,21 +948,60 @@ public void WriteLiteral(ReadOnlySpan value) """); - var projectItem = CreateProjectItemFromFile(); - // Act - var compiled = CompileToAssembly(projectItem, designTime: false); + var generated = CompileToCSharp(""" + @inherits MyUtf8PageBase + + + +

Hello World

+

This is UTF-8 encoded HTML content.

+ + + """); // Assert - AssertDocumentNodeMatchesBaseline(compiled.CodeDocument.GetDocumentNode()); - AssertCSharpDocumentMatchesBaseline(compiled.CodeDocument.GetCSharpDocument()); - AssertLinePragmas(compiled.CodeDocument); + CompileToAssembly(generated); - // Verify that the generated code contains UTF-8 string literals - var generatedCode = compiled.CodeDocument.GetCSharpDocument().Text.ToString(); + var generatedCode = generated.CodeDocument.GetCSharpDocument().Text.ToString(); Assert.Contains("u8)", generatedCode); } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] + public void Utf8HtmlLiterals_WithoutOverload_UsesStringLiterals_Runtime() + { + // Arrange + _configuration = new(RazorLanguageVersion.Preview, "MVC-3.0", Extensions: []); + + AddCSharpSyntaxTree(""" + + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc.Razor; + + public abstract class MyPageBase : RazorPage + { + } + + """); + + // Act + var generated = CompileToCSharp(""" + @inherits MyPageBase + + + +

Hello World

+ + + """); + + // Assert + CompileToAssembly(generated); + + var generatedCode = generated.CodeDocument.GetCSharpDocument().Text.ToString(); + Assert.DoesNotContain("u8)", generatedCode); + } + #endregion #region DesignTime diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml deleted file mode 100644 index 5efa6f29031..00000000000 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@inherits MyUtf8PageBase -@utf8HtmlLiterals true - - - -

Hello World

-

This is UTF-8 encoded HTML content.

- - diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs deleted file mode 100644 index 473add1badc..00000000000 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.codegen.cs +++ /dev/null @@ -1,95 +0,0 @@ -#pragma checksum "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "22fb469a637f8d91884dc0bc96afe67098c3248cafe342540e23642267add1fc" -// -#pragma warning disable 1591 -[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals), @"mvc.1.0.view", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] -namespace AspNetCoreGeneratedDocument -{ - #line default - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Mvc; - using global::Microsoft.AspNetCore.Mvc.Rendering; - using global::Microsoft.AspNetCore.Mvc.ViewFeatures; - #line default - #line hidden - [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"Sha256", @"22fb469a637f8d91884dc0bc96afe67098c3248cafe342540e23642267add1fc", @"/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] - [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml")] - [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] - #nullable restore - internal sealed class TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals : -#nullable restore -#line (1,11)-(1,25) "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals.cshtml" -MyUtf8PageBase - -#line default -#line hidden -#nullable disable - - #nullable disable - { - #line hidden - #pragma warning disable 0649 - private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext; - #pragma warning restore 0649 - private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner(); - #pragma warning disable 0169 - private string __tagHelperStringValueBuffer; - #pragma warning restore 0169 - private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null; - private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager - { - get - { - if (__backed__tagHelperScopeManager == null) - { - __backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope); - } - return __backed__tagHelperScopeManager; - } - } - private global::Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper; - #pragma warning disable 1998 - public async override global::System.Threading.Tasks.Task ExecuteAsync() - { - WriteLiteral("\r\n\r\n"u8); - __tagHelperExecutionContext = __tagHelperScopeManager.Begin("body", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "__UniqueIdSuppressedForTesting__", async() => { - WriteLiteral("\r\n

Hello World

\r\n

This is UTF-8 encoded HTML content.

\r\n"u8); - } - ); - __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper = CreateTagHelper(); - __tagHelperExecutionContext.Add(__Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper); - await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); - if (!__tagHelperExecutionContext.Output.IsContentModified) - { - await __tagHelperExecutionContext.SetOutputContentAsync(); - } - Write(__tagHelperExecutionContext.Output); - __tagHelperExecutionContext = __tagHelperScopeManager.End(); - WriteLiteral("\r\n\r\n"u8); - } - #pragma warning restore 1998 - #nullable restore - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; - #nullable disable - #nullable restore - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; - #nullable disable - #nullable restore - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; - #nullable disable - #nullable restore - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; - #nullable disable - #nullable restore - [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] - public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } = default!; - #nullable disable - } -} -#pragma warning restore 1591 diff --git a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt b/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt deleted file mode 100644 index b9ea7b0697f..00000000000 --- a/src/Compiler/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Utf8HtmlLiterals_Runtime.ir.txt +++ /dev/null @@ -1,47 +0,0 @@ -Document - - RazorCompiledItemAttribute - - NamespaceDeclaration - - AspNetCoreGeneratedDocument - UsingDirective - (1:0,1 [20] ) - global::System - UsingDirective - (24:1,1 [40] ) - global::System.Collections.Generic - UsingDirective - (67:2,1 [25] ) - global::System.Linq - UsingDirective - (95:3,1 [36] ) - global::System.Threading.Tasks - UsingDirective - (134:4,1 [38] ) - global::Microsoft.AspNetCore.Mvc - UsingDirective - (175:5,1 [48] ) - global::Microsoft.AspNetCore.Mvc.Rendering - UsingDirective - (226:6,1 [51] ) - global::Microsoft.AspNetCore.Mvc.ViewFeatures - RazorSourceChecksumAttribute - - RazorCompiledItemMetadataAttribute - - CreateNewOnMetadataUpdateAttribute - - ClassDeclaration - - internal sealed - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_Utf8HtmlLiterals - MyUtf8PageBase - - DefaultTagHelperRuntime - - FieldDeclaration - - private - global::Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper - __Microsoft_AspNetCore_Mvc_Razor_TagHelpers_BodyTagHelper - MethodDeclaration - - public async override - global::System.Threading.Tasks.Task - ExecuteAsync - HtmlContent - (50:2,0 [10] Utf8HtmlLiterals.cshtml) - LazyIntermediateToken - (50:2,0 [2] Utf8HtmlLiterals.cshtml) - Html - \n - LazyIntermediateToken - (52:3,0 [5] Utf8HtmlLiterals.cshtml) - Html - - LazyIntermediateToken - (58:3,6 [2] Utf8HtmlLiterals.cshtml) - Html - \n - TagHelper - (60:4,0 [89] Utf8HtmlLiterals.cshtml) - body - TagMode.StartTagAndEndTag - DefaultTagHelperBody - - HtmlContent - (66:4,6 [76] Utf8HtmlLiterals.cshtml) - LazyIntermediateToken - (66:4,6 [6] Utf8HtmlLiterals.cshtml) - Html - \n - LazyIntermediateToken - (72:5,4 [3] Utf8HtmlLiterals.cshtml) - Html -

- LazyIntermediateToken - (76:5,8 [11] Utf8HtmlLiterals.cshtml) - Html - Hello World - LazyIntermediateToken - (87:5,19 [5] Utf8HtmlLiterals.cshtml) - Html -

- LazyIntermediateToken - (92:5,24 [6] Utf8HtmlLiterals.cshtml) - Html - \n - LazyIntermediateToken - (98:6,4 [2] Utf8HtmlLiterals.cshtml) - Html -

- LazyIntermediateToken - (101:6,7 [35] Utf8HtmlLiterals.cshtml) - Html - This is UTF-8 encoded HTML content. - LazyIntermediateToken - (136:6,42 [4] Utf8HtmlLiterals.cshtml) - Html -

- LazyIntermediateToken - (140:6,46 [2] Utf8HtmlLiterals.cshtml) - Html - \n - DefaultTagHelperCreate - - Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper - DefaultTagHelperExecute - - HtmlContent - (149:7,7 [11] Utf8HtmlLiterals.cshtml) - LazyIntermediateToken - (149:7,7 [2] Utf8HtmlLiterals.cshtml) - Html - \n - LazyIntermediateToken - (151:8,0 [7] Utf8HtmlLiterals.cshtml) - Html - - LazyIntermediateToken - (158:8,7 [2] Utf8HtmlLiterals.cshtml) - Html - \n - Inject - - Inject - - Inject - - Inject - - Inject - diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs index f1a75539ed5..0afd595e63f 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/DirectiveDescriptorTest.cs @@ -125,7 +125,7 @@ public void Build_ValidatesDirectiveKeyword_EmptyIsInvalid() var ex = Assert.Throws(() => DirectiveDescriptor.CreateSingleLineDirective("")); // Assert - Assert.Equal("Invalid directive keyword ''. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits.", ex.Message); + Assert.Equal("Invalid directive keyword ''. Directives must have a non-empty keyword that consists only of letters.", ex.Message); } [Fact] @@ -135,7 +135,7 @@ public void Build_ValidatesDirectiveKeyword_InvalidCharacter() var ex = Assert.Throws(() => DirectiveDescriptor.CreateSingleLineDirective("test_directive")); // Assert - Assert.Equal("Invalid directive keyword 'test_directive'. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits.", ex.Message); + Assert.Equal("Invalid directive keyword 'test_directive'. Directives must have a non-empty keyword that consists only of letters.", ex.Message); } [Fact] diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationExtensions.cs index dc5dcd44c86..2eecae6ca9b 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/CompilationExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Linq; namespace Microsoft.CodeAnalysis.Razor.Compiler.CSharp; @@ -16,4 +15,43 @@ public static bool HasAddComponentParameter(this Compilation compilation) t.GetMembers("AddComponentParameter") .Any(static m => m.DeclaredAccessibility == Accessibility.Public)); } + + public static bool HasCallableUtf8WriteLiteralOverload(this Compilation compilation, string probeTypeMetadataName) + { + var readOnlySpanType = compilation.GetTypeByMetadataName("System.ReadOnlySpan`1"); + var byteType = compilation.GetSpecialType(SpecialType.System_Byte); + if (readOnlySpanType is not INamedTypeSymbol readOnlySpanNamedType || + byteType.TypeKind == TypeKind.Error) + { + return false; + } + + var readOnlySpanOfByte = readOnlySpanNamedType.Construct(byteType); + var probeType = compilation.GetTypeByMetadataName(probeTypeMetadataName); + if (probeType is null || probeType.TypeKind == TypeKind.Error) + { + return false; + } + + for (var currentType = probeType; currentType is not null; currentType = currentType.BaseType) + { + foreach (var method in currentType.GetMembers("WriteLiteral").OfType()) + { + if (method.IsStatic || + !method.ReturnsVoid || + method.Parameters.Length != 1 || + !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, readOnlySpanOfByte)) + { + continue; + } + + if (compilation.IsSymbolAccessibleWithin(method, probeType)) + { + return true; + } + } + } + + return false; + } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs index 09d4b23863f..050b5ba155f 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DirectiveDescriptor.cs @@ -202,14 +202,14 @@ public DefaultDirectiveDescriptorBuilder(string name, DirectiveKind kind) public DirectiveDescriptor Build() { - if (Directive.Length == 0 || !char.IsLetter(Directive[0])) + if (Directive.Length == 0) { throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive)); } - for (var i = 1; i < Directive.Length; i++) + for (var i = 0; i < Directive.Length; i++) { - if (!char.IsLetterOrDigit(Directive[i])) + if (!char.IsLetter(Directive[i])) { throw new InvalidOperationException(Resources.FormatDirectiveDescriptor_InvalidDirectiveKeyword(Directive)); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs deleted file mode 100644 index 40e681cdead..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirective.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.AspNetCore.Razor.Language.Legacy; - -namespace Microsoft.AspNetCore.Razor.Language.Extensions; - -public static class Utf8HtmlLiteralsDirective -{ - public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( - SyntaxConstants.CSharp.Utf8HtmlLiteralsKeyword, - DirectiveKind.SingleLine, - builder => - { - builder.AddBooleanToken(Resources.Utf8HtmlLiteralsDirective_BooleanToken_Name, Resources.Utf8HtmlLiteralsDirective_BooleanToken_Description); - builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; - builder.Description = Resources.Utf8HtmlLiteralsDirective_Description; - }); - - public static void Register(RazorProjectEngineBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.AddDirective(Directive, RazorFileKind.Legacy); - builder.Features.Add(new Utf8HtmlLiteralsDirectivePass()); - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs deleted file mode 100644 index 6de2028bc46..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8HtmlLiteralsDirectivePass.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Razor.Language.Intermediate; - -namespace Microsoft.AspNetCore.Razor.Language.Extensions; - -public sealed class Utf8HtmlLiteralsDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass -{ - protected override void ExecuteCore( - RazorCodeDocument codeDocument, - DocumentIntermediateNode documentNode, - CancellationToken cancellationToken) - { - foreach (var directive in documentNode.FindDirectiveReferences(Utf8HtmlLiteralsDirective.Directive)) - { - var token = directive.Node.Tokens.FirstOrDefault(); - if (token != null && - string.Equals(token.Content, "true", System.StringComparison.Ordinal)) - { - documentNode.Options = documentNode.Options.WithFlags(writeHtmlUtf8StringLiterals: true); - break; - } - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8WriteLiteralDetectionPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8WriteLiteralDetectionPass.cs new file mode 100644 index 00000000000..cfd04df8750 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/Utf8WriteLiteralDetectionPass.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Compiler.CSharp; + +namespace Microsoft.AspNetCore.Razor.Language.Extensions; + +internal sealed class Utf8WriteLiteralDetectionPass : IntermediateNodePassBase, IRazorOptimizationPass +{ + private const string ProbeTypeMetadataName = "__RazorUtf8WriteLiteralProbeNamespace.__RazorUtf8WriteLiteralProbeType"; + + private IMetadataReferenceFeature? _referenceFeature; + + protected override void OnInitialized() + { + Engine.TryGetFeature(out _referenceFeature); + } + + protected override void ExecuteCore( + RazorCodeDocument codeDocument, + DocumentIntermediateNode documentNode, + CancellationToken cancellationToken) + { + if (!codeDocument.FileKind.IsLegacy() || + documentNode.Options is null || + documentNode.Options.DesignTime || + documentNode.Options.WriteHtmlUtf8StringLiterals) + { + return; + } + + var references = _referenceFeature?.References; + if (references is null || references.Count == 0) + { + return; + } + + var @class = documentNode.FindPrimaryClass(); + var baseType = @class?.BaseType; + if (baseType is null || string.IsNullOrWhiteSpace(baseType.BaseType.Content)) + { + return; + } + + var sourceText = BuildProbeSource(baseType, GetUsingDirectives(documentNode)); + var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, codeDocument.ParserOptions.CSharpParseOptions, cancellationToken: cancellationToken); + var compilation = CSharpCompilation.Create( + "__RazorUtf8WriteLiteralProbe", + [syntaxTree], + references); + + if (compilation.HasCallableUtf8WriteLiteralOverload(ProbeTypeMetadataName)) + { + documentNode.Options = documentNode.Options.WithFlags(writeHtmlUtf8StringLiterals: true); + } + } + + private static string BuildProbeSource(BaseTypeWithModel baseType, IEnumerable usingDirectives) + { + var builder = new StringBuilder(); + foreach (var usingDirective in usingDirectives) + { + builder.Append("using ").Append(usingDirective).AppendLine(";"); + } + + builder.AppendLine("namespace __RazorUtf8WriteLiteralProbeNamespace"); + builder.AppendLine("{"); + builder.Append(" internal class __RazorUtf8WriteLiteralProbeType : ").Append(BuildBaseType(baseType)).AppendLine(); + builder.AppendLine(" {"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + + return builder.ToString(); + } + + private static string BuildBaseType(BaseTypeWithModel baseType) + { + var builder = new StringBuilder(baseType.BaseType.Content); + if (baseType.GreaterThan is not null) + { + builder.Append(baseType.GreaterThan.Content); + } + + if (baseType.ModelType is not null) + { + builder.Append(baseType.ModelType.Content); + } + + if (baseType.LessThan is not null) + { + builder.Append(baseType.LessThan.Content); + } + + return builder.ToString(); + } + + private static IEnumerable GetUsingDirectives(DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + if (@namespace is null) + { + yield break; + } + + foreach (var child in @namespace.Children) + { + if (child is UsingDirectiveIntermediateNode usingDirective) + { + yield return usingDirective.Content; + } + } + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs index 591239701dd..d49937749e5 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/SyntaxConstants.cs @@ -26,8 +26,6 @@ public static class CSharp public const string ElseIfKeyword = "else if"; public const string NamespaceKeyword = "namespace"; public const string ClassKeyword = "class"; - public const string Utf8HtmlLiteralsKeyword = "utf8HtmlLiterals"; - // Not supported. Only used for error cases. public const string HelperKeyword = "helper"; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs index e1d29e4f36c..05b11aff6f8 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -314,11 +314,6 @@ public static RazorProjectEngine Create( builder.Features.Add(new ViewCssScopePass()); } - if (configuration.LanguageVersion >= RazorLanguageVersion.Version_11_0) - { - Utf8HtmlLiteralsDirective.Register(builder); - } - if (configuration.LanguageVersion >= RazorLanguageVersion.Version_3_0) { FunctionsDirective.Register(builder); @@ -364,6 +359,7 @@ private static void AddDefaultFeatures(ImmutableArray.Builder fea // Intermediate Node Passes features.Add(new DefaultDocumentClassifierPass()); features.Add(new MetadataAttributePass()); + features.Add(new Utf8WriteLiteralDetectionPass()); features.Add(new DesignTimeDirectivePass()); features.Add(new DirectiveRemovalOptimizationPass()); features.Add(new DefaultTagHelperOptimizationPass()); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx index 51d34dae602..bcd84831e1b 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx @@ -177,7 +177,7 @@ The document type '{0}' does not support the extension '{1}'.
- Invalid directive keyword '{0}'. Directives must have a non-empty keyword that starts with a letter and consists only of letters and digits. + Invalid directive keyword '{0}'. Directives must have a non-empty keyword that consists only of letters. A non-optional directive token cannot follow an optional directive token. @@ -619,13 +619,4 @@ Parent has not been set. - - Opt-in to HTML literals being emitted as C# UTF-8 string literals for this page. - - - Utf8HtmlLiterals - - - Whether the current page should emit HTML literals as C# UTF-8 string literals. - - \ No newline at end of file + diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerationOptions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerationOptions.cs index e8f09a0695c..3d3e4a06f17 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerationOptions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerationOptions.cs @@ -2,7 +2,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.NET.Sdk.Razor.SourceGenerators @@ -32,6 +34,8 @@ internal sealed record RazorSourceGenerationOptions internal bool UseRoslynTokenizer { get; set; } = true; + internal ImmutableArray MetadataReferences { get; set; } = []; + public override int GetHashCode() => Configuration.GetHashCode(); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs index 6a8b216fa54..ab726e75171 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -133,6 +133,11 @@ private static SourceGeneratorProjectEngine GetGenerationProjectEngine( builder.CSharpParseOptions = razorSourceGeneratorOptions.CSharpParseOptions; }); + b.Features.Add(new DefaultMetadataReferenceFeature() + { + References = razorSourceGeneratorOptions.MetadataReferences, + }); + CompilerFeatures.Register(b); RazorExtensions.Register(b); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs index 20320759d8d..56bcfb07da2 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs @@ -17,9 +17,9 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { public partial class RazorSourceGenerator { - private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), bool) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions(((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), Compilation), bool) pair, CancellationToken ct) { - var (((options, parseOptions), references), isSuppressed) = pair; + var ((((options, parseOptions), references), compilation), isSuppressed) = pair; var globalOptions = options.GlobalOptions; if (isSuppressed) @@ -68,6 +68,7 @@ public partial class RazorSourceGenerator CSharpParseOptions = (CSharpParseOptions)parseOptions, TestSuppressUniqueIds = _testSuppressUniqueIds, UseRoslynTokenizer = useRoslynTokenizer, + MetadataReferences = references.Add(compilation.ToMetadataReference()), }; return (razorSourceGenerationOptions, diagnostic); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 1ee52225c21..2382ef31b7e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -50,6 +50,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var razorSourceGeneratorOptions = analyzerConfigOptions .Combine(parseOptions) .Combine(metadataRefs.Collect()) + .Combine(compilation) .SuppressIfNeeded(isGeneratorSuppressed) .Select(ComputeRazorSourceGeneratorOptions) .WithTrackingName("RazorSourceGeneratorOptions") diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs index f800c9961c9..6963d5f21aa 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs @@ -63,7 +63,7 @@ public async Task CssScoping() }); // Act - var result = RunGenerator(compilation!, ref driver, out compilation); + var result = RunGenerator(compilation!, ref driver, out _); // Assert Assert.Empty(result.Diagnostics); @@ -106,7 +106,7 @@ public async Task ConditionalAttributes([CombinatorialValues("@null", "@n")] str var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out compilation); + var result = RunGenerator(compilation!, ref driver, out _); // Assert Assert.Empty(result.Diagnostics); @@ -131,7 +131,7 @@ public async Task OpenAngle() var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out compilation); + var result = RunGenerator(compilation!, ref driver, out _); // Assert result.Diagnostics.Verify(); @@ -155,7 +155,7 @@ public async Task QuoteInAttributeName() var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out compilation); + var result = RunGenerator(compilation!, ref driver, out _); // Assert result.Diagnostics.Verify(); @@ -167,4 +167,81 @@ public async Task QuoteInAttributeName() """, html); } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] + public async Task Utf8HtmlLiterals_AutoDetectedFromInherits() + { + // Arrange + var project = CreateTestProject( + additionalSources: new() + { + ["Pages/Index.cshtml"] = """ + @inherits MyUtf8PageBase +

Hello World

+ """, + }, + sources: new() + { + ["MyUtf8PageBase.cs"] = """ + using System; + using Microsoft.AspNetCore.Mvc.Razor; + + public abstract class MyUtf8PageBase : RazorPage + { + public void WriteLiteral(ReadOnlySpan utf8HtmlLiteral) + { + WriteLiteral(System.Text.Encoding.UTF8.GetString(utf8HtmlLiteral)); + } + } + """ + }); + + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out _); + + // Assert + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedSources); + var generatedSource = result.GeneratedSources[0].SourceText.ToString(); + Assert.Contains("u8)", generatedSource); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] + public async Task Utf8HtmlLiterals_WithoutOverload_UsesStringLiterals() + { + // Arrange + var project = CreateTestProject( + additionalSources: new() + { + ["Pages/Index.cshtml"] = """ + @inherits MyPageBase +

Hello World

+ """, + }, + sources: new() + { + ["MyPageBase.cs"] = """ + using Microsoft.AspNetCore.Mvc.Razor; + + public abstract class MyPageBase : RazorPage + { + } + """ + }); + + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out _); + + // Assert + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedSources); + var generatedSource = result.GeneratedSources[0].SourceText.ToString(); + Assert.DoesNotContain("u8)", generatedSource); + } } From 65f06f4173a66e580b724247d1448ac543edf212 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 5 Mar 2026 09:15:27 -0800 Subject: [PATCH 3/5] Fix source generator merge regression Resolve post-conflict source generator breakage by removing the unresolved suppression call, aligning option provider tuple shape, restoring cshtml test execution against output compilation, and updating incremental step expectations for compilation-dependent options. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RazorSourceGenerator.RazorProviders.cs | 4 ++-- .../src/SourceGenerators/RazorSourceGenerator.cs | 1 - .../RazorSourceGeneratorCshtmlTests.cs | 8 ++++---- .../RazorSourceGeneratorTests.cs | 8 ++++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs index 4e191100775..0a8886ce2c7 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs @@ -17,9 +17,9 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { public partial class RazorSourceGenerator { - private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions(((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), Compilation), bool) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), Compilation) pair, CancellationToken ct) { - var ((((options, parseOptions), references), compilation), isSuppressed) = pair; + var (((options, parseOptions), references), compilation) = pair; var globalOptions = options.GlobalOptions; Log.ComputeRazorSourceGeneratorOptions(); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 69ea17507e5..b3206e9d8a3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -45,7 +45,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(parseOptions) .Combine(metadataRefs.Collect()) .Combine(compilation) - .SuppressIfNeeded(isGeneratorSuppressed) .Select(ComputeRazorSourceGeneratorOptions) .WithTrackingName("RazorSourceGeneratorOptions") .ReportDiagnostics(context); diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs index 6963d5f21aa..55a55c8ab9b 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorCshtmlTests.cs @@ -63,7 +63,7 @@ public async Task CssScoping() }); // Act - var result = RunGenerator(compilation!, ref driver, out _); + var result = RunGenerator(compilation!, ref driver, out compilation); // Assert Assert.Empty(result.Diagnostics); @@ -106,7 +106,7 @@ public async Task ConditionalAttributes([CombinatorialValues("@null", "@n")] str var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out _); + var result = RunGenerator(compilation!, ref driver, out compilation); // Assert Assert.Empty(result.Diagnostics); @@ -131,7 +131,7 @@ public async Task OpenAngle() var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out _); + var result = RunGenerator(compilation!, ref driver, out compilation); // Assert result.Diagnostics.Verify(); @@ -155,7 +155,7 @@ public async Task QuoteInAttributeName() var driver = await GetDriverAsync(project); // Act - var result = RunGenerator(compilation!, ref driver, out _); + var result = RunGenerator(compilation!, ref driver, out compilation); // Assert result.Diagnostics.Verify(); diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index c4247771986..395d3435579 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -1139,7 +1139,7 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); // Verify that adding a metadata reference triggers tag helper discovery - result.VerifyIncrementalSteps("RazorSourceGeneratorOptions", IncrementalStepRunReason.Unchanged); // Re-ran but unchanged + result.VerifyIncrementalSteps("RazorSourceGeneratorOptions", IncrementalStepRunReason.Modified); // Re-ran due compilation input result.VerifyIncrementalSteps("TagHelpersFromCompilation", IncrementalStepRunReason.Unchanged); // Re-ran but compilation tag helpers unchanged result.VerifyIncrementalSteps("TagHelpersFromReferences", IncrementalStepRunReason.Modified); // New reference added result.VerifyIncrementalStepsMultiple("CheckedAndRewrittenTagHelpers", @@ -2008,10 +2008,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output) result.VerifyIncrementalSteps("TagHelpersFromCompilation", IncrementalStepRunReason.Modified); // New tag helper discovered result.VerifyIncrementalStepsMultiple("CheckedAndRewrittenTagHelpers", IncrementalStepRunReason.Modified, // Index - new tag helper affects h2 - IncrementalStepRunReason.Unchanged); // Layout - doesn't use h2 + IncrementalStepRunReason.Modified); // Layout - reprocessed because options input changed result.VerifyIncrementalStepsMultiple("GeneratedCode", IncrementalStepRunReason.Modified, // Index - re-generated with tag helper - IncrementalStepRunReason.Cached); // Layout - unchanged + IncrementalStepRunReason.Modified); // Layout - re-generated because options input changed } @@ -2814,7 +2814,7 @@ public async Task IncrementalCompilation_OnlyCompilationRuns_When_MetadataRefere // reference causes the compilation to change so we re-run tag helper discovery there // but we didn't re-check the actual reference itself - result.VerifyIncrementalSteps("RazorSourceGeneratorOptions", IncrementalStepRunReason.Unchanged); // Re-ran but unchanged + result.VerifyIncrementalSteps("RazorSourceGeneratorOptions", IncrementalStepRunReason.Modified); // Re-ran due compilation input result.VerifyIncrementalSteps("TagHelpersFromCompilation", IncrementalStepRunReason.Unchanged); // Re-ran but unchanged } From ceaac0c362f74bc34ff974bb166fc3c2108b5a13 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 5 Mar 2026 09:25:42 -0800 Subject: [PATCH 4/5] Retry CI after transient macOS lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From b55a7d93558ef96e1a4c3e94a6b23e0dffd4ef2f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 5 Mar 2026 11:50:22 -0800 Subject: [PATCH 5/5] Fix RazorProjectEngine feature expectation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/RazorProjectEngineTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs index 6e493e2172f..e55238ef8ba 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs @@ -90,6 +90,7 @@ private static void AssertDefaultFeatures(RazorProjectEngine engine) feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature)); }