【C#】분석기 소스 발생기 개발의 요점

https://github.com/naminodarie/ac-library-csharp
https://github.com/naminodarie/SourceExpander
분석기원 발생기 몇 개를 개발했기 때문에 지식을 총결해 봅시다.
C# 이외의NET 언어도 마찬가지라고 생각해요. C#의 개발이니까 C#로 보도하죠.
제목 중 [A/S] 중 하나는 분석기 원본 발생기 공통이고 [A]는 분석기만 표시하며 [S]는 원본 발생기를 대상으로 하는 항목만 표시한다.

공식 창고의 쿡북[S] 보기


https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md
소스 발생기만 있고 쿡쿡이 있어서 아주 유용해요.

주문을 csproj에 [A/S]


분석기 소스 발생기는 csproj에 대해 다양한 주문을 필요로 한다.

누겟 패키지 만들기


Analyzer 소스 발생기는 NuGet 포장을 평소와 다르게 만듭니다.
그래서 다양한 주문이 있다.
<PropertyGroup>
  <IncludeBuildOutput>false</IncludeBuildOutput>
  <IncludeSymbols>false</IncludeSymbols>
 <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);PackBuildOutputs</TargetsForTfmSpecificContentInPackage>
  <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
  <DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
</ItemGroup>


<Target Name="GetDependencyTargetPaths">
<ItemGroup>
    <TargetPathWithTargetPlatformMoniker Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

<Target Name="PackBuildOutputs" DependsOnTargets="SatelliteDllsProjectOutputGroup;DebugSymbolsProjectOutputGroup">
<ItemGroup>
    <TfmSpecificPackageFile Include="$(TargetDir)\*.dll" PackagePath="analyzers\dotnet\cs" />
    <TfmSpecificPackageFile Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" PackagePath="analyzers\dotnet\cs" />
    <TfmSpecificPackageFile Include="@(SatelliteDllsProjectOutputGroupOutput->'%(FinalOutputPath)')" PackagePath="analyzers\dotnet\cs\%(SatelliteDllsProjectOutputGroupOutput.Culture)\" />
</ItemGroup>
</Target>
설정
설명
IncludeBuildOutput=false
패키지에 DLL을 자동으로 추가하지 않음
IncludeSymbols=false
기호 정보 필요 없음
TargetsForTfmSpecificContentInPackage
매크로 패키지 구성
SuppressDependenciesWhenPacking=true
패키지 의존 관계 포함하지 않음
DevelopmentDependency=true
분석기로 포장하다DevelopmentDependencytrue로 설정되면 NuGet에서 분석기로 처리되며 사용자가 설치할 때도 자동으로 분석기용 설정으로 설치됩니다.
사용자가
<PackageReference Include="SourceExpander.Embedder" Version="3.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
DevelopmentDependencytrue인 패키지를 설치한 후 위에서 설명한 대로 PrivateAssets,IncludeAssets가 자동으로 설정됩니다.
이렇게 되면 분석기의 의존성은 사용자측의 라이브러리에 누설되지 않을 것이다.

Newtonsoft.Json 사용


Analyzer 소스 발생기가 사용하는 라이브러리에서는 NuGet의 종속성 문제를 해결할 수 없습니다.
따라서 DLL을 포함해야 합니다.
설정
설명
GetTargetPathDependsOn
종속 라이브러리 해결GeneratePathPropertytrue로 설정할 때 Newtonsoft.Json일 경우 $(PkgNewtonsoft_Json)라는 변수 이름으로 라이브러리를 다운로드하는 경로를 얻을 수 있으며 이것도nupkg에 삽입됩니다.
Newtonsoft.제이슨은 MIT 라이선스니까 제대로 표시해야 돼.
<PropertyGroup>
  <IncludeBuildOutput>false</IncludeBuildOutput>
  <IncludeSymbols>false</IncludeSymbols>
  <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);PackBuildOutputs</TargetsForTfmSpecificContentInPackage>
  <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
  <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
  <DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Newtonsoft.Json" Version="12.0.3" GeneratePathProperty="true" />
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
</ItemGroup>

<Target Name="GetDependencyTargetPaths">
<ItemGroup>
    <TargetPathWithTargetPlatformMoniker Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

<Target Name="PackBuildOutputs" DependsOnTargets="SatelliteDllsProjectOutputGroup;DebugSymbolsProjectOutputGroup">
<ItemGroup>
    <TfmSpecificPackageFile Include="$(TargetDir)\*.dll" PackagePath="analyzers\dotnet\cs" />
    <TfmSpecificPackageFile Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" PackagePath="analyzers\dotnet\cs" />
    <TfmSpecificPackageFile Include="@(SatelliteDllsProjectOutputGroupOutput->'%(FinalOutputPath)')" PackagePath="analyzers\dotnet\cs\%(SatelliteDllsProjectOutputGroupOutput.Culture)\" />
</ItemGroup>
</Target>

취소된 [A/S] 확인

SyntaxNodeAnalysisContextGeneratorExecutionContext 등이 받았다System.Threading.CancellationToken.
시끄럽지 않게 context.CancellationToken.ThrowIfCancellationRequested() 검사 취소해.
Visual Studio는 백그라운드에서 분석기 소스 발생기를 실행하기 때문에 자주 취소됩니다.그러나 검사를 취소하지 않으면 분석기 원본 발생기의 실행이 완료될 때까지 기다려야 한다.

Diagnostic의 출력 [A/S]


이 점에 대해서는 취향이 다를 것 같지만 DiagnosticDescriptor부터 제작Diagnostic하면 DiagnosticDescriptor하는 방법대로 제작할 수 있다.
정보에 전달되는 매개 변수의 수를 고정시키고 싶으므로 Diagnostic.Create에서 제작된 곳은 숨겨두세요.
분석기라면 납품DiagnosticDescriptor이 필요하기 때문에 private할 수 없지만 그 외에 사용하지 않는 것이 좋다고 생각합니다.
public static Diagnostic EMBED0001_UnknownError(string message)
    => Diagnostic.Create(EMBED0001_UnknownError_Descriptor, Location.None, message);
private static readonly DiagnosticDescriptor EMBED0001_UnknownError_Descriptor = new(
    "EMBED0001",
    new LocalizableResourceString(
        nameof(DiagnosticsResources.EMBED0001_Title),
        DiagnosticsResources.ResourceManager,
        typeof(DiagnosticsResources)),
    new LocalizableResourceString(
        nameof(DiagnosticsResources.EMBED0001_Body),
        DiagnosticsResources.ResourceManager,
        typeof(DiagnosticsResources)),
    "Error",
    DiagnosticSeverity.Warning,
    true);

멀티스레드 실행 [A/S]

compilation.Options.ConcurrentBuild에서 다중 루틴 환경에서 컴파일이 실행되었는지 확인할 수 있습니다.
다선정일 때도 병행 집행을 유연하게 활용하여 고속 동작을 하도록 해야 한다.
SyntaxTree[] newTrees;
if (compilation.Options.ConcurrentBuild)
    newTrees = compilation.SyntaxTrees.AsParallel().WithCancellation(cancellationToken)
        .Select(Rewrited).ToArray();
else
    newTrees = compilation.SyntaxTrees
        .Select(Rewrited).ToArray();
분석기의 경우AnalysisContext에서 실행context.EnableConcurrentExecution().

좋은 웹페이지 즐겨찾기