Add Roslyn analyzers and source generators skill 🤖🤖🤖 (#2739)

* Add Roslyn analyzers skill

* Address Roslyn skill review feedback

* Refine Roslyn compatibility guidance

* Move Roslyn templates to references
This commit is contained in:
Andrew Arnott
2026-08-26 10:22:37 +10:00
committed by GitHub
parent 419c37e80b
commit c29ac38fea
4 changed files with 729 additions and 0 deletions
@@ -0,0 +1,181 @@
// 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;
/// <summary>
/// A utility class for generating consistently indented source code.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="SourceWriter"/> class.
/// </summary>
public SourceWriter()
: this('\t', 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SourceWriter"/> class.
/// </summary>
/// <param name="indentationChar">The whitespace character used for indentation.</param>
/// <param name="charsPerIndentation">The number of characters in each indentation level.</param>
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;
}
/// <summary>
/// Gets the character used for indentation.
/// </summary>
public char IndentationChar { get; }
/// <summary>
/// Gets the number of characters per indentation level.
/// </summary>
public int CharsPerIndentation { get; }
/// <summary>
/// Gets the length of the generated source.
/// </summary>
public int Length => this.builder.Length;
/// <summary>
/// Gets or sets the current indentation level.
/// </summary>
public int Indentation
{
get => this.indentation;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
this.indentation = value;
}
}
/// <summary>
/// Writes a character followed by a newline.
/// </summary>
/// <param name="value">The character to write.</param>
public void WriteLine(char value)
{
this.AddIndentation();
this.builder.Append(value);
this.builder.Append(NewLine);
}
/// <summary>
/// Writes text followed by a newline.
/// </summary>
/// <param name="text">The C# source to write.</param>
/// <param name="disableIndentation">Whether to suppress current indentation.</param>
public void WriteLine(
[StringSyntax("c#-test")] string text,
bool disableIndentation = false)
{
bool isFinalLine;
ReadOnlySpan<char> remainingText = text.AsSpan();
do
{
ReadOnlySpan<char> nextLine = GetNextLine(ref remainingText, out isFinalLine);
if (!disableIndentation && this.indentation > 0)
{
this.AddIndentation();
}
this.AppendSpan(nextLine);
this.builder.Append(NewLine);
}
while (!isFinalLine);
}
/// <summary>
/// Writes a newline.
/// </summary>
public void WriteLine() => this.builder.Append(NewLine);
/// <summary>
/// Returns the generated source text.
/// </summary>
/// <returns>The generated source.</returns>
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<char> GetNextLine(ref ReadOnlySpan<char> remainingText, out bool isFinalLine)
{
if (remainingText.IsEmpty)
{
isFinalLine = true;
return default;
}
int lineLength = remainingText.IndexOf('\n');
ReadOnlySpan<char> 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<char> next = remainingText[..lineLength];
remainingText = rest;
return next;
}
private void AddIndentation()
=> this.builder.Append(this.IndentationChar, this.CharsPerIndentation * this.indentation);
private void AppendSpan(ReadOnlySpan<char> span)
=> this.builder.Append(span.ToString());
}