Source Generator

  • 새 코드를 추가 가능.
  • 기존 코드 수정 불가능
  • C# 혹은 추가 파일에 접근 가능

Unity

  • 버전
    • .NET 버전
    • IDE의 컴파일러 버전
    • Microsoft.CodeAnalysis.CSharp 버전
      • /Editor/Data/DotNetSdkRoslyn/Microsoft.CodeAnalysis.CSharp.dll 버전보다 높은 버전의 것을 참조하면 움직이지 않는다.

Roslyn - 4.3.1 Visual Studio 2022 - 17.3

.NET Compiler Platform SDK

  • etc: Improved Interpolated Strings가 C# 10.0

  • 빌드된.dll

    • 의존성 관리가 힘들 수 도 있으니 하나의 dll로 만드는게 좋음.
    • label: RoslynAnalyzer
    • dll배치는 Runtime/ 쪽에 놔두는 경향이 있음

ISourceGenerator & IIncrementalGenerator

  • deprecated - ISourceGenerator: 전체 코드 베이스
  • IIncrementalGenerator: 변경된 부분
Microsoft.CodeAnalysis.CSharpUnity
3.xISourceGenerator
4.xIIncrementalGenerator2022.2~, 2023.1~

Sample: SourceGenerator

mkdir SourceGenerator
cd SourceGenerator
dotnet new gitignore
dotnet new classlib -o SourceGeneratorGen
dotnet new console -o SourceGeneratorSrc
dotnet new sln
dotnet sln add SourceGeneratorGen
dotnet sln add SourceGeneratorSrc

SourceGeneratorGen

  • 우클릭 소스제너레이터 프로젝트
  • Properties > Debug > Open debug launch profiles UI
  • 보이는 프로파일 삭제
  • 추가 클릭
    • Roslyn component 선택
    • 타겟 프로젝트에서 콘솔 어플리케이션 프로젝트 선택
    • UI 닫기
  • 비주얼 스튜디오 재시작
  • 재생 버튼 옆 디버그 프로파일 드롭다운에서 소스제너레이터 프로젝트 선택
  • 디버거가 멈추는지 확인하기 위해 소스제너레이터에 브레이크 포인트를 설정
  • 재생 클릭
<!-- SourceGeneratorGen/SourceGeneratorGen.csproj-->

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <ImplicitUsings>disable</ImplicitUsings>
        <Nullable>disable</Nullable>

        <LangVersion>11</LangVersion>

        <!-- IsRoslynComponent: true => | Properties > Debug > Launch > Roslyn Component -->
        <IsRoslynComponent>true</IsRoslynComponent>
        <AnalyzerLanguage>cs</AnalyzerLanguage>

        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
    </ItemGroup>
</Project>
// SourceGeneratorGen/Generator.cs

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;

namespace SourceGeneratorGen
{
    [Generator(LanguageNames.CSharp)]
    public sealed class Generator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            // === IncrementalValue[s]Provider<T>
            //context.CompilationProvider
            //context.AdditionalTextsProvider
            //context.AnalyzerConfigOptionsProvider
            //context.MetadataReferencesProvider
            //context.ParseOptionsProvider
            // === SyntaxValueProvider
            // context.SyntaxProvider
            // === Outputting values
            //context.RegisterSourceOutput               // 사용자 컴파일에 포함될 소스 파일과 진단을 생성할 수 있다
            //context.RegisterImplementationSourceOutput // RegisterSourceOutput랑 비슷. 단, 유저코드나 다른 변환기에 의해 실행되지 않음. 코드 분석에 영향을 주지 않음.
            //context.RegisterPostInitializationOutput   // 다른 변환이 실행되기 전에 컴파일에 포함됨

            context.RegisterPostInitializationOutput(Callback);

            IncrementalValuesProvider<GeneratorAttributeSyntaxContext> source = context.SyntaxProvider.ForAttributeWithMetadataName(
                fullyQualifiedMetadataName: "GeneratedNamespace.GenerateToStringAttribute",
                predicate: static (node, token) => true,
                transform: static (context, token) => context);

            context.RegisterSourceOutput(source, Emit);
        }

        private void Callback(IncrementalGeneratorPostInitializationContext context)
        {
            string code = """
using System;

namespace GeneratedNamespace
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    internal sealed class GenerateToStringAttribute : Attribute
    {
    }
}
""";
            context.AddSource("Generated.cs", code);
        }

        private void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
        {
            INamedTypeSymbol typeSymbol = (INamedTypeSymbol)source.TargetSymbol;
            TypeDeclarationSyntax typeNode = (TypeDeclarationSyntax)source.TargetNode;

            if (typeSymbol.GetMembers("ToString").Length != 0)
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ExistsOverrideToString, typeNode.Identifier.GetLocation(), typeSymbol.Name));
                return;
            }

            string ns;
            if (typeSymbol.ContainingNamespace.IsGlobalNamespace)
            {
                ns = string.Empty;
            }
            else
            {
                ns = $"{typeSymbol.ContainingNamespace}";
            }

            string fullType = typeSymbol
                .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                .Replace("global::", "")
                .Replace("<", "_")
                .Replace(">", "_");

            IEnumerable<string> publicMembers = typeSymbol
                .GetMembers()
                .Where(x => x is (IFieldSymbol or IPropertySymbol)
                             and { IsStatic: false, DeclaredAccessibility: Accessibility.Public, IsImplicitlyDeclared: false, CanBeReferencedByName: true })
                .Select(x => $"{x.Name}:{{{x.Name}}}"); // MyProperty:{MyProperty}

            string toString = string.Join(", ", publicMembers);

            // multiline string interpolation
            string code = $$"""
// ========================== auto-generated
#pragma warning disable CS8600
#pragma warning disable CS8601
#pragma warning disable CS8602
#pragma warning disable CS8603
#pragma warning disable CS8604

namespace {{ns}}
{
    partial class {{typeSymbol.Name}}
    {
        public override string ToString()
        {
            return $"{{toString}}";
        }
    }
}
#pragma warning restore CS8604
#pragma warning restore CS8603
#pragma warning restore CS8602
#pragma warning restore CS8601
#pragma warning restore CS8600
""";

            context.AddSource($"{fullType}.SampleGenerator.g.cs", code);
        }
    }


    public static class DiagnosticDescriptors
    {
        private const string CATEGORY = "GeneratedNamespace";

        public static readonly DiagnosticDescriptor ExistsOverrideToString = new DiagnosticDescriptor(
            id: "SAMPLE001",
            title: "ToString override",
            messageFormat: "The GenerateToString class '{0}' has ToString override but it is not allowed",
            category: CATEGORY,
            defaultSeverity: DiagnosticSeverity.Error,
            isEnabledByDefault: true
        );
    }
}

SourceGeneratorSrc

<!-- SourceGeneratorSrc/SourceGeneratorSrc.csproj -->
<!-- TODO: https://stevetalkscode.co.uk/debug-source-generators-with-vs2019-1610 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\SourceGeneratorGen\SourceGeneratorGen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
// SourceGeneratorSrc/Program.cs

using GeneratedNamespace;
using System;

namespace SourceGeneratorSrc
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            MyClass mc = new MyClass() { Hoge = 10, Bar = "tako" };
            Console.WriteLine(mc);
        }
    }

    [GenerateToString]
    public partial class MyClass
    {
        public int Hoge { get; set; }
        public string Bar { get; set; }
    }
}

Etc

// IDE 경고내기

private const string CATEGORY = "GeneratedNamespace";
public static readonly DiagnosticDescriptor ExistsOverrideToString = new DiagnosticDescriptor(
    id:            "SAMPLE001",
    title:         "ToString override",
    messageFormat: "The GenerateToString class '{0}' has ToString override but it is not allowed",
    category: CATEGORY,
    defaultSeverity: DiagnosticSeverity.Error,
    isEnabledByDefault: true
);

SourceProductionContext context;
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ExistsOverrideToString, typeNode.Identifier.GetLocation(), typeSymbol.Name));
ReportDiagnostic Enum
Default0Report a diagnostic by default.
Error1Report a diagnostic as an error.
Warn2Report a diagnostic as a warning even though /warnaserror is specified.
Info3Report a diagnostic as an info.
Hidden4Report a diagnostic as hidden.
Suppress5Suppress a diagnostic.

로슬린

컴파일

Ref

Source Generator ex