File: Language\Extensions\DesignTimeDirectiveTargetExtension.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
 
namespace Microsoft.AspNetCore.Razor.Language.Extensions;
 
internal class DesignTimeDirectiveTargetExtension : IDesignTimeDirectiveTargetExtension
{
    private const string DirectiveTokenHelperMethodName = "__RazorDirectiveTokenHelpers__";
    private const string TypeHelper = "__typeHelper";
 
    public void WriteDesignTimeDirective(CodeRenderingContext context, DesignTimeDirectiveIntermediateNode directiveNode)
    {
        context.CodeWriter
            .WriteLine("#pragma warning disable 219")
            .WriteLine($"private void {DirectiveTokenHelperMethodName}() {{");
 
        for (var i = 0; i < directiveNode.Children.Count; i++)
        {
            if (directiveNode.Children[i] is DirectiveTokenIntermediateNode directiveTokenNode)
            {
                WriteDesignTimeDirectiveToken(context, directiveNode, directiveTokenNode, currentIndex: i);
            }
        }
 
        context.CodeWriter
            .WriteLine("}")
            .WriteLine("#pragma warning restore 219");
    }
 
    private void WriteDesignTimeDirectiveToken(CodeRenderingContext context, DesignTimeDirectiveIntermediateNode parent, DirectiveTokenIntermediateNode node, int currentIndex)
    {
        var tokenKind = node.DirectiveToken.Kind;
        if (!node.Source.HasValue ||
            !string.Equals(
                context.SourceDocument?.FilePath,
                node.Source.Value.FilePath,
                StringComparison.OrdinalIgnoreCase))
        {
            // We don't want to handle directives from imports.
            return;
        }
 
        if (tokenKind == DirectiveTokenKind.Attribute)
        {
            // We don't need to do anything special here.
            // We let the Roslyn take care of providing syntax errors for C# attributes.
            return;
        }
 
        // Wrap the directive token in a lambda to isolate variable names.
        context.CodeWriter
            .Write("((global::")
            .Write(typeof(Action).FullName)
            .Write(")(");
        using (context.CodeWriter.BuildLambda())
        {
            var originalIndent = context.CodeWriter.CurrentIndent;
            context.CodeWriter.CurrentIndent = 0;
            switch (tokenKind)
            {
                case DirectiveTokenKind.Type:
 
                    if (string.IsNullOrEmpty(node.Content))
                    {
                        // This is most likely a marker token.
                        WriteMarkerToken(context, node);
                        break;
                    }
 
                    // {node.Content} __typeHelper = default({node.Content});
                    using (context.BuildLinePragma(node.Source))
                    {
                        context.AddSourceMappingFor(node);
                        context.CodeWriter
                            .Write(node.Content)
                            .Write(" ")
                            .WriteStartAssignment(TypeHelper)
                            .Write("default");
 
                        if (!context.Options.SuppressNullabilityEnforcement)
                        {
                            context.CodeWriter.Write("!");
                        }
 
                        context.CodeWriter.WriteLine(";");
                    }
                    break;
 
                case DirectiveTokenKind.Member:
 
                    if (string.IsNullOrEmpty(node.Content))
                    {
                        // This is most likely a marker token.
                        WriteMarkerToken(context, node);
                        break;
                    }
 
                    // Type parameters are mapped to actual source, so no need to generate design-time code for them here
                    if (node.DirectiveToken.Name == ComponentResources.TypeParamDirective_Token_Name)
                    {
                        break;
                    }
 
                    // global::System.Object {node.content} = null;
                    using (context.BuildLinePragma(node.Source))
                    {
                        context.CodeWriter
                        .Write("global::")
                        .Write(typeof(object).FullName)
                        .Write(" ");
 
                        context.AddSourceMappingFor(node);
                        context.CodeWriter
                            .Write(node.Content)
                            .Write(" = null");
 
                        if (!context.Options.SuppressNullabilityEnforcement)
                        {
                            context.CodeWriter.Write("!");
                        }
 
                        context.CodeWriter.WriteLine(";");
                    }
                    break;
 
                case DirectiveTokenKind.Namespace
                or DirectiveTokenKind.IdentifierOrExpression:
 
                    if (string.IsNullOrEmpty(node.Content))
                    {
                        // This is most likely a marker token.
                        WriteMarkerToken(context, node);
                        break;
                    }
 
                    // global::System.Object __typeHelper = nameof({node.Content});
                    using (context.BuildLinePragma(node.Source))
                    {
                        context.CodeWriter
                        .Write("global::")
                        .Write(typeof(object).FullName)
                        .Write(" ")
                        .WriteStartAssignment(TypeHelper);
 
                        context.CodeWriter.Write("nameof(");
 
                        context.AddSourceMappingFor(node);
                        context.CodeWriter
                            .Write(node.Content)
                            .WriteLine(");");
                    }
                    break;
 
                case DirectiveTokenKind.String:
 
                    // Add a string syntax to the directive if the document is a Razor page or a Blazor component.
                    // language=Route tells Roslyn that this string is a route template. A classifier that's
                    // part of ASP.NET Core will run that colorizes the route string.
                    var stringSyntax = context.DocumentKind switch
                    {
                        "mvc.1.0.razor-page" => "Route",
                        "component.1.0" => "Route,Component",
                        _ => null
                    };
                    if (stringSyntax is not null)
                    {
                        context.CodeWriter.Write("// language=").Write(stringSyntax);
                    }
 
                    // global::System.Object __typeHelper = "{node.Content}";
                    using (context.BuildLinePragma(node.Source))
                    {
                        context.CodeWriter
                        .Write("global::")
                        .Write(typeof(object).FullName)
                        .Write(" ")
                        .WriteStartAssignment(TypeHelper);
 
                        if (node.Content.StartsWith("\"", StringComparison.Ordinal))
                        {
                            context.AddSourceMappingFor(node);
                            context.CodeWriter.Write(node.Content);
                        }
                        else
                        {
                            context.CodeWriter.Write("\"");
                            context.AddSourceMappingFor(node);
                            context.CodeWriter
                                .Write(node.Content)
                                .Write("\"");
                        }
 
                        context.CodeWriter.WriteLine(";");
                    }
                    break;
 
                case DirectiveTokenKind.Boolean:
                    // global::System.Boolean __typeHelper = {node.Content};
                    using (context.BuildLinePragma(node.Source))
                    {
                        context.CodeWriter
                        .Write("global::")
                        .Write(typeof(bool).FullName)
                        .Write(" ")
                        .WriteStartAssignment(TypeHelper);
 
                        context.AddSourceMappingFor(node);
                        context.CodeWriter.Write(node.Content);
                        context.CodeWriter.WriteLine(";");
                    }
                    break;
            }
            context.CodeWriter.CurrentIndent = originalIndent;
        }
        context.CodeWriter.WriteLine("))();");
    }
 
    private void WriteMarkerToken(CodeRenderingContext context, DirectiveTokenIntermediateNode node)
    {
        // Marker tokens exist to be filled with other content a user might write. In an end-to-end
        // scenario markers prep the Razor documents C# projections to have an empty projection that
        // can be filled with other user content. This content can trigger a multitude of other events,
        // such as completion. In the case of completion, a completion session can occur when a marker
        // hasn't been filled and then we will fill it as a user types. The line pragma is necessary
        // for consistency so when a C# completion session starts, filling user code doesn't result in
        // a previously non-existent line pragma from being added and destroying the context in which
        // the completion session was started.
        using (context.BuildLinePragma(node.Source))
        {
            context.AddSourceMappingFor(node);
            context.CodeWriter.Write(" ");
        }
    }
}