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..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 @@ -926,6 +926,82 @@ public void InvalidCode_EmptyImplicitExpression_Runtime() CompileToAssembly(generated, throwOnFailure: false, ignoreRazorDiagnostics: true); } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8429")] + public void Utf8HtmlLiterals_AutoDetectedFromInherits_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)); + } + } + + """); + + // Act + var generated = CompileToCSharp(""" + @inherits MyUtf8PageBase + + + +

Hello World

+

This is UTF-8 encoded HTML content.

+ + + """); + + // Assert + CompileToAssembly(generated); + + 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.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/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)); } 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/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/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 b11b2f319d1..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,7 +26,6 @@ public static class CSharp public const string ElseIfKeyword = "else if"; public const string NamespaceKeyword = "namespace"; public const string ClassKeyword = "class"; - // 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 260b7a2d1a2..3aef5e4a0bb 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -357,6 +357,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 2f19609b1af..bcd84831e1b 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx @@ -619,4 +619,4 @@ Parent has not been set. - \ 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 d9759cecc3c..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) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), Compilation) pair, CancellationToken ct) { - var ((options, parseOptions), references) = pair; + var (((options, parseOptions), references), compilation) = pair; var globalOptions = options.GlobalOptions; Log.ComputeRazorSourceGeneratorOptions(); @@ -63,6 +63,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 4358c46193c..b3206e9d8a3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -44,6 +44,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var razorSourceGeneratorOptions = analyzerConfigOptions .Combine(parseOptions) .Combine(metadataRefs.Collect()) + .Combine(compilation) .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 f800c9961c9..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 @@ -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); + } } 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 } 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));