File: Language\CodeGeneration\CodeWriterExtensions.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 System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
 
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration;
 
internal static class CodeWriterExtensions
{
    private static readonly ReadOnlyMemory<char> s_true = "true".AsMemory();
    private static readonly ReadOnlyMemory<char> s_false = "false".AsMemory();
 
    private static readonly ReadOnlyMemory<char> s_zeroes = "0000000000".AsMemory(); // 10 zeros
 
    // This table contains string representations of numbers from 0 to 999.
    private static readonly ImmutableArray<ReadOnlyMemory<char>> s_integerTable = InitializeIntegerTable();
 
    private static readonly char[] CStyleStringLiteralEscapeChars =
    {
        '\r',
        '\t',
        '\"',
        '\'',
        '\\',
        '\0',
        '\n',
        '\u2028',
        '\u2029',
    };
 
    private static ImmutableArray<ReadOnlyMemory<char>> InitializeIntegerTable()
    {
        var array = new ReadOnlyMemory<char>[1000];
 
        // Fill entries 100 to 999.
        for (var i = 100; i < 1000; i++)
        {
            array[i] = i.ToString(CultureInfo.InvariantCulture).AsMemory();
        }
 
        // Fill entries 10 to 99 with two-digit strings sliced from entries 110 to 199.
        for (var i = 10; i < 100; i++)
        {
            array[i] = array[i + 100][^2..];
        }
 
        // Fill 1 to 9 with slices of the last character from entries 11 to 19.
        for (var i = 1; i < 10; i++)
        {
            array[i] = array[i + 10][^1..];
        }
 
        // Finally, fill the entry for 0 with a slice from s_zeroes.
        array[0] = s_zeroes[..1];
 
        return ImmutableCollectionsMarshal.AsImmutableArray(array);
    }
 
    public static bool IsAtBeginningOfLine(this CodeWriter writer)
    {
        return writer.LastChar is '\n';
    }
 
    public static void EnsureNewLine(this CodeWriter writer)
    {
        if (!IsAtBeginningOfLine(writer))
        {
            writer.WriteLine();
        }
    }
 
    public static CodeWriter WritePadding(this CodeWriter writer, int offset, SourceSpan? span, CodeRenderingContext context)
    {
        if (span == null)
        {
            return writer;
        }
 
        if (context.SourceDocument.FilePath != null &&
            !string.Equals(context.SourceDocument.FilePath, span.Value.FilePath, StringComparison.OrdinalIgnoreCase))
        {
            // We don't want to generate padding for nodes from imports.
            return writer;
        }
 
        var basePadding = CalculatePadding();
        var resolvedPadding = Math.Max(basePadding - offset, 0);
 
        writer.Indent(resolvedPadding);
 
        return writer;
 
        int CalculatePadding()
        {
            var spaceCount = 0;
            for (var i = span.Value.AbsoluteIndex - 1; i >= 0; i--)
            {
                var @char = context.SourceDocument.Text[i];
                if (@char == '\n' || @char == '\r')
                {
                    break;
                }
                else
                {
                    // Note that a tab is also replaced with a single space so character indices match.
                    spaceCount++;
                }
            }
 
            return spaceCount;
        }
    }
 
    public static CodeWriter WriteVariableDeclaration(this CodeWriter writer, string type, string name, string value)
    {
        writer.Write(type).Write(" ").Write(name);
        if (!string.IsNullOrEmpty(value))
        {
            writer.Write(" = ").Write(value);
        }
        else
        {
            writer.Write(" = null");
        }
 
        writer.WriteLine(";");
 
        return writer;
    }
 
    public static CodeWriter WriteBooleanLiteral(this CodeWriter writer, bool value)
    {
        return writer.Write(value ? s_true : s_false);
    }
 
    /// <summary>
    ///  Writes an integer literal to the code writer using optimized precomputed lookup tables
    ///  and efficient grouping for large numbers. This avoids string allocation and formatting overhead.
    /// </summary>
    /// <param name="writer">The code writer to write to.</param>
    /// <param name="value">The integer value to write as a literal.</param>
    /// <returns>
    ///  The code writer for method chaining.
    /// </returns>
    /// <remarks>
    ///  Performance optimizations:
    ///  <list type="bullet">
    ///   <item>Zero is written directly from a precomputed slice</item>
    ///   <item>Numbers -999 to 999 use a precomputed lookup table</item>
    ///   <item>Larger numbers are decomposed into groups of 3 digits, each using the lookup table</item>
    ///   <item>Uses long arithmetic to handle int.MinValue correctly (avoids overflow when negating)</item>
    ///  </list>
    /// </remarks>
    public static CodeWriter WriteIntegerLiteral(this CodeWriter writer, int value)
    {
        // Handle zero as a special case
        if (value == 0)
        {
            return writer.Write(s_integerTable[0]);
        }
 
        var isNegative = value < 0;
        if (isNegative)
        {
            // For negative numbers, write the minus sign first
            writer.Write("-");
        }
 
        // Fast path: For small numbers (-999 to 999), use the precomputed lookup table directly
        if (value is > -1000 and < 1000)
        {
            var index = isNegative ? -value : value;
            return writer.Write(s_integerTable[index]);
        }
 
        // Slow path: For larger numbers, decompose into groups of three digits using the precomputed table.
        // This approach avoids string formatting while maintaining readability of the output.
 
        // Extract digits and write groups from most significant to least significant.
        // Note: Use long to handle int.MinValue correctly. Math.Abs(int.MinValue) would throw.
        var remaining = isNegative ? -(long)value : value;
        long divisor = 1;
 
        // Find the highest power of 1000 needed (1, 1000, 1000000, 1000000000)
        // This determines how many 3-digit groups we need
        while (remaining >= divisor * 1000)
        {
            divisor *= 1000;
        }
 
        // Process each group of 3 digits from most significant to least significant
        var first = true;
        while (divisor > 0)
        {
            var group = (int)(remaining / divisor);
            remaining %= divisor;
            divisor /= 1000;
 
            Debug.Assert(group >= 0 && group < 1000, "Digit group should be in the range [0, 999]");
 
            if (group == 0)
            {
                Debug.Assert(!first, "The first group should never be 0.");
 
                // Entire group is zero: add "000" for proper place value
                writer.Write(s_zeroes[..3]);
                continue;
            }
 
            if (first)
            {
                // First group: no leading zeros needed (e.g., "123" not "0123")
                writer.Write(s_integerTable[group]);
                first = false;
                continue;
            }
 
            // Groups after the first one with values 1-99 need leading zeros for proper formatting
            // Example: 1234567 becomes "1" + "234" + "567", but 1000067 becomes "1" + "000" + "067"
            var leadingZeros = group switch
            {
                < 10 => 2,  // 1-9: needs "00" prefix (e.g., "007")
                < 100 => 1, // 10-99: needs "0" prefix (e.g., "067") 
                _ => 0      // 100-999: no leading zeros needed
            };
 
            if (leadingZeros > 0)
            {
                writer.Write(s_zeroes[..leadingZeros]); // Add "00" or "0"
            }
 
            writer.Write(s_integerTable[group]); // Add the actual digit group
        }
 
        return writer;
    }
 
    public static CodeWriter WriteStartAssignment(
        this CodeWriter writer,
        [InterpolatedStringHandlerArgument(nameof(writer))] ref CodeWriter.WriteInterpolatedStringHandler left)
    {
        return writer.Write(ref left).Write(" = ");
    }
 
    public static CodeWriter WriteStartAssignment(this CodeWriter writer, string left)
    {
        return writer.Write(left).Write(" = ");
    }
 
    public static CodeWriter WriteParameterSeparator(this CodeWriter writer)
    {
        return writer.Write(", ");
    }
 
    public static CodeWriter WriteStartNewObject(this CodeWriter writer, string typeName)
    {
        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, ReadOnlyMemory<char> literal)
    {
        if (literal.Length >= 256 && literal.Length <= 1500 && literal.Span.IndexOf('\0') == -1)
        {
            WriteVerbatimStringLiteral(writer, literal);
        }
        else
        {
            WriteCStyleStringLiteral(writer, literal);
        }
 
        return writer;
    }
 
    public static CodeWriter WriteUsing(this CodeWriter writer, string name)
    {
        return WriteUsing(writer, name, endLine: true);
    }
 
    public static CodeWriter WriteUsing(this CodeWriter writer, string name, bool endLine)
    {
        writer.Write("using ");
        writer.Write(name);
 
        if (endLine)
        {
            writer.WriteLine(";");
        }
 
        return writer;
    }
 
    public static CodeWriter WriteEnhancedLineNumberDirective(this CodeWriter writer, SourceSpan span, int characterOffset, bool ensurePathBackslashes)
    {
        // All values here need to be offset by 1 since #line uses a 1-indexed numbering system.
        writer.Write("#line (")
            .WriteIntegerLiteral(span.LineIndex + 1)
            .Write(",")
            .WriteIntegerLiteral(span.CharacterIndex + 1)
            .Write(")-(")
            .WriteIntegerLiteral(span.LineIndex + 1 + span.LineCount)
            .Write(",")
            .WriteIntegerLiteral(span.EndCharacterIndex + 1)
            .Write(") ");
 
        // an offset of zero is indicated by its absence.
        if (characterOffset != 0)
        {
            writer.WriteIntegerLiteral(characterOffset).Write(" ");
        }
 
        return writer.Write("\"").WriteFilePath(span.FilePath, ensurePathBackslashes).WriteLine("\"");
    }
 
    public static CodeWriter WriteLineNumberDirective(this CodeWriter writer, SourceSpan span, bool ensurePathBackslashes)
    {
        if (writer.Length >= writer.NewLine.Length && !IsAtBeginningOfLine(writer))
        {
            writer.WriteLine();
        }
 
        return writer
            .Write("#line ")
            .WriteIntegerLiteral(span.LineIndex + 1)
            .Write(" \"")
            .WriteFilePath(span.FilePath, ensurePathBackslashes)
            .WriteLine("\"");
    }
 
    private static CodeWriter WriteFilePath(this CodeWriter writer, string filePath, bool ensurePathBackslashes)
    {
        if (!ensurePathBackslashes)
        {
            return writer.Write(filePath);
        }
 
        // ISSUE: https://github.com/dotnet/razor/issues/9108
        // The razor tooling normalizes paths to be forward slash based, regardless of OS.
        // If you try and use the line pragma in the design time docs to map back to the original file it will fail,
        // as the path isn't actually valid on windows. As a workaround we apply a simple heuristic to switch the
        // paths back when writing out the design time paths.
        var filePathMemory = filePath.AsMemory();
        var forwardSlashIndex = filePathMemory.Span.IndexOf('/');
        while (forwardSlashIndex >= 0)
        {
            writer.Write(filePathMemory[..forwardSlashIndex]);
            writer.Write("\\");
 
            filePathMemory = filePathMemory[(forwardSlashIndex + 1)..];
            forwardSlashIndex = filePathMemory.Span.IndexOf('/');
        }
 
        writer.Write(filePathMemory);
 
        return writer;
    }
 
    public static CodeWriter WriteStartMethodInvocation(this CodeWriter writer, string methodName)
    {
        writer.Write(methodName);
 
        return writer.Write("(");
    }
 
    public static CodeWriter WriteStartMethodInvocation(
        this CodeWriter writer,
        [InterpolatedStringHandlerArgument(nameof(writer))] ref CodeWriter.WriteInterpolatedStringHandler methodName)
    {
        writer.Write(ref methodName);
 
        return writer.Write("(");
    }
 
    public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer)
    {
        return WriteEndMethodInvocation(writer, endLine: true);
    }
 
    public static CodeWriter WriteEndMethodInvocation(this CodeWriter writer, bool endLine)
    {
        writer.Write(")");
        if (endLine)
        {
            writer.WriteLine(";");
        }
 
        return writer;
    }
 
    // Writes a method invocation for the given instance name.
    public static CodeWriter WriteInstanceMethodInvocation(
        this CodeWriter writer,
        string instanceName,
        string methodName,
        params ImmutableArray<string> arguments)
        => writer.WriteInstanceMethodInvocation(instanceName, methodName, endLine: true, arguments);
 
    // Writes a method invocation for the given instance name.
    public static CodeWriter WriteInstanceMethodInvocation(
        this CodeWriter writer,
        string instanceName,
        string methodName,
        bool endLine,
        params ImmutableArray<string> arguments)
        => writer.WriteMethodInvocation($"{instanceName}.{methodName}", endLine, arguments);
 
    public static CodeWriter WriteStartInstanceMethodInvocation(this CodeWriter writer, string instanceName, string methodName)
        => writer.WriteStartMethodInvocation($"{instanceName}.{methodName}");
 
#nullable enable
 
    public static CodeWriter WriteFieldDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        string type,
        string name,
        string? expression = null)
    {
        expression = expression.IsNullOrEmpty() ? "null" : expression;
 
        return writer
            .WriteModifierList(modifiers)
            .WriteLine($"{type} {name} = {expression};");
    }
 
    private static CodeWriter WriteModifierList(this CodeWriter writer, ImmutableArray<string> modifiers)
    {
        if (!modifiers.IsDefaultOrEmpty)
        {
            foreach (var modifier in modifiers)
            {
                writer.Write($"{modifier} ");
            }
        }
 
        return writer;
    }
 
#nullable disable
 
    public static CodeWriter WriteField(
        this CodeWriter writer,
        ImmutableArray<string> suppressWarnings,
        ImmutableArray<string> modifiers,
        string type,
        string name)
    {
        if (!suppressWarnings.IsDefaultOrEmpty)
        {
            foreach (var suppressWarning in suppressWarnings)
            {
                writer.WriteLine($"#pragma warning disable {suppressWarning}");
            }
        }
 
        writer.WriteModifierList(modifiers);
 
        writer.WriteLine($"{type} {name};");
 
        if (!suppressWarnings.IsDefaultOrEmpty)
        {
            for (var i = suppressWarnings.Length - 1; i >= 0; i--)
            {
                writer.WriteLine($"#pragma warning restore {suppressWarnings[i]}");
            }
        }
 
        return writer;
    }
 
    public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, params ImmutableArray<string> arguments)
        => writer.WriteMethodInvocation(methodName, endLine: true, arguments);
 
    public static CodeWriter WriteMethodInvocation(
        this CodeWriter writer,
        [InterpolatedStringHandlerArgument(nameof(writer))] ref CodeWriter.WriteInterpolatedStringHandler methodName,
        params ImmutableArray<string> arguments)
        => writer.WriteMethodInvocation(ref methodName, endLine: true, arguments);
 
    public static CodeWriter WriteMethodInvocation(this CodeWriter writer, string methodName, bool endLine, params ImmutableArray<string> arguments)
        => writer
            .WriteStartMethodInvocation(methodName)
            .WriteCommaSeparatedList(arguments)
            .WriteEndMethodInvocation(endLine);
 
    public static CodeWriter WriteMethodInvocation(
        this CodeWriter writer,
        [InterpolatedStringHandlerArgument(nameof(writer))] ref CodeWriter.WriteInterpolatedStringHandler methodName,
        bool endLine,
        params ImmutableArray<string> arguments)
        => writer
            .WriteStartMethodInvocation(ref methodName)
            .WriteCommaSeparatedList(arguments)
            .WriteEndMethodInvocation(endLine);
 
    public static CodeWriter WritePropertyDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        IntermediateToken type,
        string name,
        string expressionBody,
        CodeRenderingContext context)
    {
        WritePropertyDeclarationPreamble(writer, modifiers, type.Content, name, type.Source, nameSpan: null, context);
 
        writer.WriteLine($" => {expressionBody};");
 
        return writer;
    }
 
    public static CodeWriter WriteAutoPropertyDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        string type,
        string name,
        SourceSpan? typeSpan = null,
        SourceSpan? nameSpan = null,
        CodeRenderingContext context = null,
        bool privateSetter = false,
        bool defaultValue = false)
    {
        WritePropertyDeclarationPreamble(writer, modifiers, type, name, typeSpan, nameSpan, context);
 
        writer.Write(" { get;");
 
        if (privateSetter)
        {
            writer.Write(" private");
        }
 
        writer.WriteLine(" set; }");
 
        if (defaultValue && context?.Options is { SuppressNullabilityEnforcement: false, DesignTime: false })
        {
            writer.WriteLine(" = default!;");
        }
 
        return writer;
    }
 
    private static void WritePropertyDeclarationPreamble(
        CodeWriter writer,
        ImmutableArray<string> modifiers,
        string type,
        string name,
        SourceSpan? typeSpan,
        SourceSpan? nameSpan,
        CodeRenderingContext context)
    {
        writer.WriteModifierList(modifiers);
 
        WriteToken(writer, type, typeSpan, context);
        writer.Write(" ");
        WriteToken(writer, name, nameSpan, context);
 
        static void WriteToken(CodeWriter writer, string content, SourceSpan? span, CodeRenderingContext context)
        {
            if (span is not null && context?.Options.DesignTime == false)
            {
                using (context.BuildEnhancedLinePragma(span))
                {
                    writer.Write(content);
                }
            }
            else
            {
                writer.Write(content);
            }
        }
    }
 
    /// <summary>
    /// Writes an "@" character if the provided identifier needs escaping in c#
    /// </summary>
    public static CodeWriter WriteIdentifierEscapeIfNeeded(this CodeWriter writer, string identifier)
    {
        if (IdentifierRequiresEscaping(identifier))
        {
            writer.Write("@");
        }
        return writer;
    }
 
    public static bool IdentifierRequiresEscaping(this string identifier)
    {
        return CodeAnalysis.CSharp.SyntaxFacts.GetKeywordKind(identifier) != CodeAnalysis.CSharp.SyntaxKind.None ||
            CodeAnalysis.CSharp.SyntaxFacts.GetContextualKeywordKind(identifier) != CodeAnalysis.CSharp.SyntaxKind.None;
    }
 
    public static CSharpCodeWritingScope BuildScope(this CodeWriter writer)
    {
        return new CSharpCodeWritingScope(writer);
    }
 
    public static CSharpCodeWritingScope BuildLambda(this CodeWriter writer)
        => writer.WriteLambdaHeader().BuildScope();
 
    public static CSharpCodeWritingScope BuildLambda(this CodeWriter writer, string parameterName)
        => writer.WriteLambdaHeader(parameterName).BuildScope();
 
    public static CSharpCodeWritingScope BuildLambda<T>(this CodeWriter writer, T parameterName)
        where T : IWriteableValue
        => writer.WriteLambdaHeader(parameterName).BuildScope();
 
    public static CSharpCodeWritingScope BuildAsyncLambda(this CodeWriter writer)
        => writer.WriteAsyncLambdaHeader().BuildScope();
 
    public static CSharpCodeWritingScope BuildAsyncLambda(this CodeWriter writer, string parameterName)
        => writer.WriteAsyncLambdaHeader(parameterName).BuildScope();
 
    public static CSharpCodeWritingScope BuildAsyncLambda<T>(this CodeWriter writer, T parameterName)
        where T : IWriteableValue
        => writer.WriteAsyncLambdaHeader(parameterName).BuildScope();
 
    public static CodeWriter WriteLambdaHeader(this CodeWriter writer)
        => writer.Write("() => ");
 
    public static CodeWriter WriteLambdaHeader(this CodeWriter writer, string parameterName)
        => writer.Write($"({parameterName}) => ");
 
    public static CodeWriter WriteLambdaHeader<T>(this CodeWriter writer, T parameterName)
        where T : IWriteableValue
    {
        writer.Write("(");
        parameterName.WriteTo(writer);
        writer.Write(") => ");
 
        return writer;
    }
 
    public static CodeWriter WriteAsyncLambdaHeader(this CodeWriter writer)
        => writer.Write($"async() => ");
 
    public static CodeWriter WriteAsyncLambdaHeader(this CodeWriter writer, string parameterName)
        => writer.Write($"async({parameterName}) => ");
 
    public static CodeWriter WriteAsyncLambdaHeader<T>(this CodeWriter writer, T parameterName)
        where T : IWriteableValue
    {
        writer.Write("async(");
        parameterName.WriteTo(writer);
        writer.Write(") => ");
 
        return writer;
    }
 
#nullable enable
    public static CSharpCodeWritingScope BuildNamespace(this CodeWriter writer, string? name, SourceSpan? span, CodeRenderingContext context)
    {
        if (name.IsNullOrEmpty())
        {
            return new CSharpCodeWritingScope(writer, writeBraces: false);
        }
 
        writer.Write("namespace ");
        if (context.Options.DesignTime || span is null)
        {
            writer.WriteLine(name);
        }
        else
        {
            writer.WriteLine();
            using (context.BuildEnhancedLinePragma(span))
            {
                writer.WriteLine(name);
            }
        }
        return new CSharpCodeWritingScope(writer);
    }
#nullable disable
 
    public static CSharpCodeWritingScope BuildClassDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        string name,
        BaseTypeWithModel baseType,
        ImmutableArray<IntermediateToken> interfaces,
        ImmutableArray<TypeParameter> typeParameters,
        CodeRenderingContext context,
        bool useNullableContext = false)
    {
        Debug.Assert(context == null || context.CodeWriter == writer);
 
        if (useNullableContext)
        {
            writer.WriteLine("#nullable restore");
        }
 
        writer.WriteModifierList(modifiers);
 
        writer.Write("class ");
        writer.Write(name);
 
        if (!typeParameters.IsDefaultOrEmpty)
        {
            writer.Write("<");
 
            for (var i = 0; i < typeParameters.Length; i++)
            {
                var typeParameter = typeParameters[i];
                WriteToken(typeParameter.Name);
 
                // Write ',' between parameters, but not after them
                if (i < typeParameters.Length - 1)
                {
                    writer.Write(",");
                }
            }
 
            writer.Write(">");
        }
 
        var hasBaseType = !string.IsNullOrWhiteSpace(baseType?.BaseType.Content);
        var hasInterfaces = !interfaces.IsDefaultOrEmpty;
 
        if (hasBaseType || hasInterfaces)
        {
            writer.Write(" : ");
 
            if (hasBaseType)
            {
                WriteToken(baseType.BaseType);
                WriteOptionalToken(baseType.GreaterThan);
                WriteOptionalToken(baseType.ModelType);
                WriteOptionalToken(baseType.LessThan);
 
                if (hasInterfaces)
                {
                    WriteParameterSeparator(writer);
                }
            }
 
            if (hasInterfaces)
            {
                WriteToken(interfaces[0]);
                for (var i = 1; i < interfaces.Length; i++)
                {
                    writer.Write(", ");
                    WriteToken(interfaces[i]);
                }
            }
        }
 
        writer.WriteLine();
 
        if (!typeParameters.IsDefaultOrEmpty)
        {
            foreach (var typeParameter in typeParameters)
            {
                WriteOptionalToken(typeParameter.Constraints, addLineBreak: true);
            }
        }
 
        if (useNullableContext)
        {
            writer.WriteLine("#nullable disable");
        }
 
        return new CSharpCodeWritingScope(writer);
 
        void WriteOptionalToken(IntermediateToken token, bool addLineBreak = false)
        {
            if (token is not null)
            {
                WriteToken(token, addLineBreak);
            }
        }
 
        void WriteToken(IntermediateToken token, bool addLineBreak = false)
        {
            if (token.Source is { } source)
            {
                WriteWithPragma(token.Content, context, source);
            }
            else
            {
                writer.Write(token.Content);
 
                if (addLineBreak)
                {
                    writer.WriteLine();
                }
            }
        }
 
        static void WriteWithPragma(string content, CodeRenderingContext context, SourceSpan source)
        {
            var writer = context.CodeWriter;
 
            if (context.Options.DesignTime)
            {
                using (context.BuildLinePragma(source))
                {
                    context.AddSourceMappingFor(source);
                    writer.Write(content);
                }
            }
            else
            {
                using (context.BuildEnhancedLinePragma(source))
                {
                    writer.Write(content);
                }
            }
        }
    }
 
    public static CSharpCodeWritingScope BuildConstructorDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        string typeName,
        ImmutableArray<MethodParameter> parameters)
    {
        writer
            .WriteModifierList(modifiers)
            .Write(typeName)
            .Write("(")
            .WriteCommaSeparatedList(parameters, static (w, v) =>
                w.WriteModifierList(v.Modifiers).Write($"{v.Type} {v.Name}"))
            .WriteLine(")");
 
        return new CSharpCodeWritingScope(writer);
    }
 
    public static CSharpCodeWritingScope BuildMethodDeclaration(
        this CodeWriter writer,
        ImmutableArray<string> modifiers,
        string returnType,
        string name,
        ImmutableArray<MethodParameter> parameters)
    {
        writer
            .WriteModifierList(modifiers)
            .Write(returnType)
            .Write(" ")
            .Write(name)
            .Write("(")
            .WriteCommaSeparatedList(parameters, static (w, v) =>
                w.WriteModifierList(v.Modifiers).Write($"{v.Type} {v.Name}"))
            .WriteLine(")");
 
        return new CSharpCodeWritingScope(writer);
    }
 
    public static CodeWriter WriteCommaSeparatedList(this CodeWriter writer, ImmutableArray<string> items)
        => writer.WriteSeparatedList(separator: ", ", items);
 
    public static CodeWriter WriteCommaSeparatedList<T>(this CodeWriter writer, ImmutableArray<T> items, Action<CodeWriter, T> elementWriter)
        => writer.WriteSeparatedList(separator: ", ", items, elementWriter);
 
    public static CodeWriter WriteSeparatedList(this CodeWriter writer, string separator, ImmutableArray<string> items)
    {
        var first = true;
 
        foreach (var item in items)
        {
            if (!first)
            {
                writer.Write(separator);
            }
            else
            {
                first = false;
            }
 
            writer.Write(item);
        }
 
        return writer;
    }
 
    public static CodeWriter WriteSeparatedList<T>(this CodeWriter writer, string separator, ImmutableArray<T> items, Action<CodeWriter, T> elementWriter)
    {
        var first = true;
 
        foreach (var item in items)
        {
            if (!first)
            {
                writer.Write(separator);
            }
            else
            {
                first = false;
            }
 
            elementWriter(writer, item);
        }
 
        return writer;
    }
 
    private static void WriteVerbatimStringLiteral(CodeWriter writer, ReadOnlyMemory<char> literal)
    {
        writer.Write("@\"");
 
        // We need to suppress indenting during the writing of the string's content. A
        // verbatim string literal could contain newlines that don't get escaped.
        var oldIndent = writer.CurrentIndent;
        writer.CurrentIndent = 0;
 
        // We need to find the index of each '"' (double-quote) to escape it.
        int index;
        while ((index = literal.Span.IndexOf('"')) >= 0)
        {
            writer.Write(literal[..index]);
            writer.Write("\"\"");
 
            literal = literal[(index + 1)..];
        }
 
        Debug.Assert(index == -1); // We've hit all of the double-quotes.
 
        // Write the remainder after the last double-quote.
        writer.Write(literal);
 
        writer.Write("\"");
 
        writer.CurrentIndent = oldIndent;
    }
 
    private static void WriteCStyleStringLiteral(CodeWriter writer, ReadOnlyMemory<char> literal)
    {
        // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM
        writer.Write("\"");
 
        // We need to find the index of each escapable character to escape it.
        int index;
        while ((index = literal.Span.IndexOfAny(CStyleStringLiteralEscapeChars)) >= 0)
        {
            writer.Write(literal[..index]);
 
            switch (literal.Span[index])
            {
                case '\r':
                    writer.Write("\\r");
                    break;
                case '\t':
                    writer.Write("\\t");
                    break;
                case '\"':
                    writer.Write("\\\"");
                    break;
                case '\'':
                    writer.Write("\\\'");
                    break;
                case '\\':
                    writer.Write("\\\\");
                    break;
                case '\0':
                    writer.Write("\\\0");
                    break;
                case '\n':
                    writer.Write("\\n");
                    break;
                case '\u2028':
                    writer.Write("\\u2028");
                    break;
                case '\u2029':
                    writer.Write("\\u2029");
                    break;
                default:
                    Debug.Assert(false, "Unknown escape character.");
                    break;
            }
 
            literal = literal[(index + 1)..];
        }
 
        Debug.Assert(index == -1); // We've hit all of chars that need escaping.
 
        // Write the remainder after the last escaped char.
        writer.Write(literal);
 
        writer.Write("\"");
    }
 
    public struct CSharpCodeWritingScope : IDisposable
    {
        private readonly CodeWriter _writer;
        private readonly bool _autoSpace;
        private readonly bool _writeBraces;
        private readonly int _tabSize;
        private int _startIndent;
 
        public CSharpCodeWritingScope(CodeWriter writer, bool autoSpace = true, bool writeBraces = true)
        {
            _writer = writer;
            _autoSpace = autoSpace;
            _writeBraces = writeBraces;
            _tabSize = writer.TabSize;
            _startIndent = -1; // Set in WriteStartScope
 
            WriteStartScope();
        }
 
        public void Dispose()
        {
            if (_writer is null)
            {
                return;
            }
 
            WriteEndScope();
        }
 
        private void WriteStartScope()
        {
            TryAutoSpace(" ");
 
            if (_writeBraces)
            {
                _writer.WriteLine("{");
            }
            else
            {
                _writer.WriteLine();
            }
 
            _writer.CurrentIndent += _tabSize;
            _startIndent = _writer.CurrentIndent;
        }
 
        private void WriteEndScope()
        {
            TryAutoSpace(_writer.NewLine);
 
            // Ensure the scope hasn't been modified
            if (_writer.CurrentIndent == _startIndent)
            {
                _writer.CurrentIndent -= _tabSize;
            }
 
            if (_writeBraces)
            {
                _writer.WriteLine("}");
            }
            else
            {
                _writer.WriteLine();
            }
        }
 
        private void TryAutoSpace(string spaceCharacter)
        {
            if (_autoSpace &&
                _writer.LastChar is char ch &&
                !char.IsWhiteSpace(ch))
            {
                _writer.Write(spaceCharacter);
            }
        }
    }
}