Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> value)
{
WriteLiteral(System.Text.Encoding.UTF8.GetString(value));
}
}

""");

// Act
var generated = CompileToCSharp("""
@inherits MyUtf8PageBase

<html>
<body>
<h1>Hello World</h1>
<p>This is UTF-8 encoded HTML content.</p>
</body>
</html>
""");

// 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

<html>
<body>
<h1>Hello World</h1>
</body>
</html>
""");

// Assert
CompileToAssembly(generated);

var generatedCode = generated.CodeDocument.GetCSharpDocument().Text.ToString();
Assert.DoesNotContain("u8)", generatedCode);
}

#endregion

#region DesignTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private static void AssertDefaultFeatures(RazorProjectEngine engine)
feature => Assert.IsType<MetadataAttributePass>(feature),
feature => Assert.IsType<PreallocatedTagHelperAttributeOptimizationPass>(feature),
feature => Assert.IsType<TagHelperDiscoveryService>(feature),
feature => Assert.IsType<Utf8WriteLiteralDetectionPass>(feature),
feature => Assert.IsType<ViewCssScopePass>(feature));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<IMethodSymbol>())
{
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> literal)
public static CodeWriter WriteStringLiteral(this CodeWriter writer, ReadOnlyMemory<char> 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;
Expand Down Expand Up @@ -900,7 +900,7 @@ public static CodeWriter WriteSeparatedList<T>(this CodeWriter writer, string se
return writer;
}

private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory<char> literal)
private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory<char> literal, bool utf8 = false)
{
writer.Write("@\"");

Expand All @@ -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<char> literal)
private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory<char> literal, bool utf8 = false)
{
// From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM
writer.Write("\"");
Expand Down Expand Up @@ -983,6 +988,11 @@ private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory<c
writer.Write(literal);

writer.Write("\"");

if (utf8)
{
writer.Write("u8");
}
}

public struct CSharpCodeWritingScope : IDisposable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ void WriteLiteral(ReadOnlyMemory<char> content)
{
context.CodeWriter
.WriteStartMethodInvocation(WriteHtmlContentMethod)
.WriteStringLiteral(content)
.WriteStringLiteral(content, context.Options.WriteHtmlUtf8StringLiterals)
.WriteEndMethodInvocation();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> 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<string> 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;
}
}
}
}
Loading