Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.IntegrationTests;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor.Compiler.CSharp;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
Expand All @@ -30,6 +34,21 @@ public CodeGenerationIntegrationTest()

protected override RazorConfiguration Configuration => _configuration;

protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder)
{
base.ConfigureProjectEngine(builder);

// Register the UTF-8 WriteLiteral feature with a pre-computed support map.
var supportMap = new DefaultUtf8WriteLiteralFeature.Utf8SupportMap(
ImmutableSortedDictionary<string, string>.Empty,
ImmutableSortedDictionary.CreateRange(StringComparer.Ordinal, new[]
{
new KeyValuePair<string, bool>("MyUtf8PageBase", true),
new KeyValuePair<string, bool>("MyPageBase", false),
}));
builder.Features.Add(new DefaultUtf8WriteLiteralFeature { SupportMap = supportMap });
}

#region Runtime

[Fact]
Expand Down Expand Up @@ -926,6 +945,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 @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Compiler.CSharp;
using Moq;
using Xunit;

Expand Down Expand Up @@ -91,6 +92,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,56 @@ public static bool HasAddComponentParameter(this Compilation compilation)
t.GetMembers("AddComponentParameter")
.Any(static m => m.DeclaredAccessibility == Accessibility.Public));
}

/// <summary>
/// Determines whether the type identified by <paramref name="typeMetadataName"/> has a callable
/// instance <c>WriteLiteral(ReadOnlySpan&lt;byte&gt;)</c> overload accessible from that type.
/// </summary>
public static bool HasCallableUtf8WriteLiteralOverload(this Compilation compilation, string typeMetadataName)
{
var type = compilation.GetTypeByMetadataName(typeMetadataName);
if (type is null || type.TypeKind == TypeKind.Error)
{
return false;
}

return compilation.HasCallableUtf8WriteLiteralOverload(type);
}

/// <summary>
/// Determines whether the given <paramref name="type"/> has a callable
/// instance <c>WriteLiteral(ReadOnlySpan&lt;byte&gt;)</c> overload accessible from that type.
/// </summary>
public static bool HasCallableUtf8WriteLiteralOverload(this Compilation compilation, INamedTypeSymbol type)
{
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);

for (var currentType = type; currentType is not null; currentType = currentType.BaseType)
{
foreach (var member in currentType.GetMembers("WriteLiteral"))
{
if (member is IMethodSymbol
{
IsStatic: false,
ReturnsVoid: true,
Parameters: [{ Type: var paramType }]
} method &&
SymbolEqualityComparer.Default.Equals(paramType, readOnlySpanOfByte) &&
compilation.IsSymbolAccessibleWithin(method, type))
{
return true;
}
}
}

return false;
}
}
Loading