// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. // Adapted from https://github.com/eiriktsarpalis/PolyType/blob/main/src/PolyType.Roslyn/SourceWriter.cs using System; using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.CodeAnalysis.Text; namespace Product.Analyzers; /// /// A utility class for generating consistently indented source code. /// internal sealed class SourceWriter { // Fixed newlines make generated output deterministic across platforms. private const string NewLine = "\r\n"; private readonly StringBuilder builder = new(); private int indentation; /// /// Initializes a new instance of the class. /// public SourceWriter() : this('\t', 1) { } /// /// Initializes a new instance of the class. /// /// The whitespace character used for indentation. /// The number of characters in each indentation level. public SourceWriter(char indentationChar, int charsPerIndentation) { if (!char.IsWhiteSpace(indentationChar)) { throw new ArgumentOutOfRangeException(nameof(indentationChar)); } if (charsPerIndentation < 1) { throw new ArgumentOutOfRangeException(nameof(charsPerIndentation)); } this.IndentationChar = indentationChar; this.CharsPerIndentation = charsPerIndentation; } /// /// Gets the character used for indentation. /// public char IndentationChar { get; } /// /// Gets the number of characters per indentation level. /// public int CharsPerIndentation { get; } /// /// Gets the length of the generated source. /// public int Length => this.builder.Length; /// /// Gets or sets the current indentation level. /// public int Indentation { get => this.indentation; set { if (value < 0) { throw new ArgumentOutOfRangeException(nameof(value)); } this.indentation = value; } } /// /// Writes a character followed by a newline. /// /// The character to write. public void WriteLine(char value) { this.AddIndentation(); this.builder.Append(value); this.builder.Append(NewLine); } /// /// Writes text followed by a newline. /// /// The C# source to write. /// Whether to suppress current indentation. public void WriteLine( [StringSyntax("c#-test")] string text, bool disableIndentation = false) { bool isFinalLine; ReadOnlySpan remainingText = text.AsSpan(); do { ReadOnlySpan nextLine = GetNextLine(ref remainingText, out isFinalLine); if (!disableIndentation && this.indentation > 0) { this.AddIndentation(); } this.AppendSpan(nextLine); this.builder.Append(NewLine); } while (!isFinalLine); } /// /// Writes a newline. /// public void WriteLine() => this.builder.Append(NewLine); /// /// Returns the generated source text. /// /// The generated source. public SourceText ToSourceText() { if (this.builder.Length == 0) { throw new InvalidOperationException("Nothing was written."); } if (this.indentation != 0) { throw new InvalidOperationException($"Indentation level expected to be 0 but is {this.indentation}."); } return SourceText.From(this.builder.ToString(), Encoding.UTF8); } private static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out bool isFinalLine) { if (remainingText.IsEmpty) { isFinalLine = true; return default; } int lineLength = remainingText.IndexOf('\n'); ReadOnlySpan rest; if (lineLength == -1) { lineLength = remainingText.Length; isFinalLine = true; rest = default; } else { rest = remainingText[(lineLength + 1)..]; isFinalLine = false; } if ((uint)lineLength > 0 && remainingText[lineLength - 1] == '\r') { lineLength--; } ReadOnlySpan next = remainingText[..lineLength]; remainingText = rest; return next; } private void AddIndentation() => this.builder.Append(this.IndentationChar, this.CharsPerIndentation * this.indentation); private void AppendSpan(ReadOnlySpan span) => this.builder.Append(span.ToString()); }