File: DefaultRazorCSharpLoweringPhaseTest.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.AspNetCore.Razor.Language\test\Microsoft.AspNetCore.Razor.Language.UnitTests.csproj (Microsoft.AspNetCore.Razor.Language.UnitTests)
// 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.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
public class DefaultRazorCSharpLoweringPhaseTest : RazorProjectEngineTestBase
{
    protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest;
 
    [Fact]
    public void Execute_ThrowsForMissingDependency_IRDocument()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source));
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() =>
            ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument));
 
        Assert.Equal(
            $"The '{nameof(DefaultRazorCSharpLoweringPhase)}' phase requires a '{nameof(DocumentIntermediateNode)}' " +
            $"provided by the '{nameof(RazorCodeDocument)}'.",
             exception.Message);
    }
 
    [Fact]
    public void Execute_ThrowsForMissingDependency_CodeTarget()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source));
 
        var documentNode = new DocumentIntermediateNode()
        {
            DocumentKind = "test",
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() =>
            ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument));
        Assert.Equal(
            $"The document of kind 'test' does not have a '{nameof(CodeTarget)}'. " +
            $"The document classifier must set a value for '{nameof(DocumentIntermediateNode.Target)}'.",
            exception.Message);
    }
 
    [Fact]
    public void Execute_CollatesIRDocumentDiagnosticsFromSourceDocument()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateCodeDocument("<p class=@(");
        codeDocument = codeDocument.WithSyntaxTree(RazorSyntaxTree.Parse(codeDocument.Source));
 
        var documentNode = new DocumentIntermediateNode()
        {
            DocumentKind = "test",
            Target = CodeTarget.CreateDefault(codeDocument),
            Options = codeDocument.CodeGenerationOptions
        };
 
        var expectedDiagnostic = RazorDiagnostic.Create(
            new RazorDiagnosticDescriptor("1234", "I am an error.", RazorDiagnosticSeverity.Error),
            new SourceSpan("SomeFile.cshtml", 11, 0, 11, 1));
 
        documentNode.AddDiagnostic(expectedDiagnostic);
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
 
        // Assert
        var csharpDocument = codeDocument.GetRequiredCSharpDocument();
        var diagnostic = Assert.Single(csharpDocument.Diagnostics);
        Assert.Same(expectedDiagnostic, diagnostic);
    }
 
    [Fact] // This test covers the whole process including actual hashing.
    public void Execute_EndToEnd_WritesChecksumAndMarksAutoGenerated()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_SHA1_WritesChecksumAndMarksAutoGenerated()
    {
        // Arrange
        var sourceText = SourceText.From("", checksumAlgorithm: SourceHashAlgorithm.Sha1);
        var source = RazorSourceDocument.Create(sourceText, RazorSourceDocumentProperties.Create("test.cshtml", null));
        var codeDocument = ProjectEngine.CreateCodeDocument(source);
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "da39a3ee5e6b4b0d3255bfef95601890afd80709"
            // <auto-generated/>
            #pragma warning disable 1591
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void WriteDocument_SHA256_WritesChecksumAndMarksAutoGenerated()
    {
        // Arrange
        var sourceText = SourceText.From("", checksumAlgorithm: SourceHashAlgorithm.Sha256);
        var source = RazorSourceDocument.Create(sourceText, RazorSourceDocumentProperties.Create("test.cshtml", null));
        var codeDocument = ProjectEngine.CreateCodeDocument(source);
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
            // <auto-generated/>
            #pragma warning disable 1591
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_Empty_SuppressChecksumTrue_DoesnotWriteChecksum()
    {
        // Arrange
        var projectEngine = CreateProjectEngine(builder =>
        {
            builder.ConfigureCodeGenerationOptions(builder =>
            {
                builder.SuppressChecksum = true;
            });
        });
 
        var codeDocument = projectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
@"// <auto-generated/>
#pragma warning disable 1591
#pragma warning restore 1591
",
            csharp);
    }
 
    [Fact]
    public void Execute_WritesNamespace()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new NamespaceDeclarationIntermediateNode()
        {
            Name = "TestNamespace",
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            namespace TestNamespace
            {
                #line hidden
            }
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WritesClass()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new ClassDeclarationIntermediateNode()
        {
            Modifiers = ["internal"],
            BaseType = new BaseTypeWithModel("TestBase"),
            Interfaces = [IntermediateNodeFactory.CSharpToken("IFoo"), IntermediateNodeFactory.CSharpToken("IBar")],
            TypeParameters = [
                new("TKey"),
                new("TValue")
            ],
            Name = "TestClass"
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            internal class TestClass<TKey,TValue> : TestBase, IFoo, IBar
            {
            }
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WithNullableContext_WritesClass()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new ClassDeclarationIntermediateNode()
        {
            Modifiers = ["internal"],
            BaseType = new BaseTypeWithModel("TestBase"),
            Interfaces = [IntermediateNodeFactory.CSharpToken("IFoo"), IntermediateNodeFactory.CSharpToken("IBar")],
            TypeParameters = [
                new("TKey"),
                new("TValue")
            ],
            Name = "TestClass",
            NullableContext = true
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            #nullable restore
            internal class TestClass<TKey,TValue> : TestBase, IFoo, IBar
            #nullable disable
            {
            }
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WritesClass_ConstrainedGenericTypeParameters()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new ClassDeclarationIntermediateNode()
        {
            Modifiers = ["internal"],
            BaseType = new BaseTypeWithModel("TestBase"),
            Interfaces = [IntermediateNodeFactory.CSharpToken("IFoo"), IntermediateNodeFactory.CSharpToken("IBar")],
            TypeParameters = [
                new("TKey", "where TKey : class"),
                new("TValue", "where TValue : class")
            ],
            Name = "TestClass"
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            internal class TestClass<TKey,TValue> : TestBase, IFoo, IBar
            where TKey : class
            where TValue : class
            {
            }
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WritesMethod()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new MethodDeclarationIntermediateNode()
        {
            Modifiers = ["internal", "virtual", "async"],
            Name = "TestMethod",
            Parameters = [
                new(name: "a", type: "int", modifiers: ["readonly", "ref"]),
                new(name: "b", type: "string")
            ],
            ReturnType = "string"
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            #pragma warning disable 1998
            internal virtual async string TestMethod(readonly ref int a, string b)
            {
            }
            #pragma warning restore 1998
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WritesField()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new FieldDeclarationIntermediateNode()
        {
            Modifiers = ["internal", "readonly"],
            Name = "_foo",
            Type = "string",
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            internal readonly string _foo;
            #pragma warning restore 1591
 
            """,
            csharp);
    }
 
    [Fact]
    public void Execute_WritesProperty()
    {
        // Arrange
        var codeDocument = ProjectEngine.CreateEmptyCodeDocument();
 
        var documentNode = new DocumentIntermediateNode()
        {
            Target = CodeTarget.CreateDefault(codeDocument)
        };
 
        codeDocument = codeDocument.WithDocumentNode(documentNode);
 
        var builder = IntermediateNodeBuilder.Create(documentNode);
        builder.Add(new PropertyDeclarationIntermediateNode()
        {
            Modifiers = ["internal", "virtual"],
            Name = "Foo",
            Type = IntermediateNodeFactory.CSharpToken("string"),
            ExpressionBody = "default"
        });
 
        // Act
        codeDocument = ProjectEngine.ExecutePhase<DefaultRazorCSharpLoweringPhase>(codeDocument);
        var result = codeDocument.GetRequiredCSharpDocument();
 
        // Assert
        var csharp = result.Text.ToString();
        AssertEx.AssertEqualToleratingWhitespaceDifferences(
            """
            #pragma checksum "test.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "f1945cd6c19e56b3c1c78943ef5ec18116907a4ca1efc40a57d48ab1db7adfc5"
            // <auto-generated/>
            #pragma warning disable 1591
            internal virtual string Foo => default;
            #pragma warning restore 1591
 
            """,
            csharp);
    }
}