Third-Party Analyzer'lar ve Kendi Roslyn Analyzer'ını Yazmak: BannedApiAnalyzers, Popüler Paketler, DiagnosticAnalyzer ve CodeFix
Bir kural yalnız 'şu API'yi kullanma' diyorsa BannedApiAnalyzers yeter; code fix, ekibe özgü mantık ya da bağlama göre mesaj gerekiyorsa kendi Roslyn analyzer'ını yazarsın. Bookstore'da Meziantou, Roslynator ve SonarAnalyzer 87 uyarı ve 14 kat analiz süresi getirdi ama DateTime.UtcNow'ı varsayılan ayarlarla yakalamadı. Yazı TimeProvider kuralını operation callback'li bir analyzer, ayrı assembly'de code fix, Microsoft.CodeAnalysis.Testing testleri ve NuGet paketiyle yazıyor.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (SDK 10.0.402) ile derlenip denenmiştir; analyzer'lar Microsoft.CodeAnalysis.CSharp 4.14.0 ile yazılmış, Microsoft.CodeAnalysis.Testing 1.1.4 ve xUnit.net v3 4.0.3 ile test edilmiştir. Karşılaştırılan paketler: Meziantou.Analyzer 3.0.340, Roslynator.Analyzers 5.0.1, SonarAnalyzer.CSharp 10.36.0 ve Microsoft.CodeAnalysis.BannedApiAnalyzers 5.6.0.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) 30 gündür dokunulmamış sepetleri silen bir back office işi yazılıyor. Ayşe'nin PR'ı (pull request; değişikliğin ana branch'e alınması için açılan inceleme isteği):
using Bookstore.Data;
using Microsoft.EntityFrameworkCore;
namespace Bookstore.Ordering;
// 30 gündür dokunulmamış sepetleri back office işi siler.
public sealed class CartCleanup(BookstoreContext db, TimeProvider clock)
{
public static readonly TimeSpan MaxAge = TimeSpan.FromDays(30);
public Task<int> DeleteExpiredAsync(CancellationToken ct)
{
DateTime cutoff = DateTime.UtcNow - MaxAge;
return db.Carts.Where(c => c.UpdatedAtUtc < cutoff).ExecuteDeleteAsync(ct);
}
}
Bookstore'un kuralı açık: zamanı TimeProvider'dan al, çünkü testler saati kontrol edebilmeli. clock inject edilmiş ama kullanılmamış; kod doğrudan sistem saatini okuyor. İkinci yazıdaki ayarlarla build ne der? Projeye en çok kullanılan üç analyzer paketini, Meziantou.Analyzer, Roslynator ve SonarAnalyzer.CSharp'ı eklesek hangisi bu satırı yakalar?
Üçüncü Parti Paketler: Ne Getirir, Ne Götürür?
Önce mevcut durum. SDK'nın kurallarından hiçbiri sistem saatini okumayı yasaklamıyor; build'in tek söylediği, dolaylı bir ipucu:
src/Bookstore.Api/Ordering/CartCleanup.cs(7,67): warning CS9113: Parameter 'clock' is unread.
Derleyici primary constructor parametresinin okunmadığını fark ediyor. Faydalı, ama asıl kuralı söylemiyor; biri clock'u başka bir satırda kullansa uyarı da kaybolur. Şimdi üç paketi Central Package Management ile ekliyoruz. Analyzer paketlerinde PrivateAssets="all" önemli: paket yalnız derleme sırasında kullanılır ve Bookstore'u referans eden başka bir projeye bağımlılık olarak geçmez.
<Project>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
<PackageReference Include="Roslynator.Analyzers" PrivateAssets="all" />
<PackageReference Include="SonarAnalyzer.CSharp" PrivateAssets="all" />
</ItemGroup>
</Project>
Bu Directory.Build.targets ile, CartCleanup eklenmeden önceki temiz Bookstore'u derlediğimizde 87 warning çıktı:
| Paket | Sürüm, son yayın | Build'de warning | Analyzer süresi |
|---|---|---|---|
| Meziantou.Analyzer | 3.0.340, 14 Kasım 2026 | 85 | 0,129 sn |
| Roslynator.Analyzers | 5.0.1, 4 Ekim 2026 | 0 | 0,108 sn |
| SonarAnalyzer.CSharp | 10.36.0, 11 Kasım 2026 | 2 | 0,524 sn |
| SDK'nın NetAnalyzers'ı (karşılaştırma için) | SDK 10.0.402 | 0 | 0,033 sn |
Üçü de aktif olarak bakılan paketler; tabloda son yayın tarihleri NuGet'ten. Bu bir seçim ölçütü: Roslyn'in her sürümünde yeni syntax geliyor ve bakımı durmuş bir analyzer yeni C# özelliklerinde ya yanlış alarm verir ya da çöker. Meziantou'nun 85 uyarısının 47'si MA0004 (await edilen task'ta ConfigureAwait(false)), 31'i MA0048 (dosya adı içindeki type'la aynı olmalı). Roslynator kurallarının çoğu varsayılan olarak yalnız IDE'de suggestion seviyesinde, bu yüzden build'de görünmüyor. Ve sorunun cevabı: üçünden hiçbiri CartCleanup'taki DateTime.UtcNow'ı varsayılan ayarlarla yakalamadı.
Burada dürüst olmak gerek: SonarAnalyzer'da tam bu iş için bir kural var. S6354 ("Use a testable (date) time provider instead") varsayılan olarak kapalı; .globalconfig'te açınca yakalıyor:
src/Bookstore.Api/Ordering/CartCleanup.cs(13,27): warning S6354: Use a testable (date) time provider instead.
Yani kendi analyzer'ını yazmadan önce mevcut paketlerin kural listesine bakmak gerekir. Bookstore yine de kendi kuralını yazacak; nedenlerini aşağıda göreceğiz: code fix, kurala bağlanabilen ekip mantığı ve sadece bir kural için bütün analizin %64'ünü harcayan bir paketi taşımamak.
Kural Çakışmaları ve Build Süresi
Birden fazla analyzer paketi aynı sorunu farklı ID'lerle raporlayabilir. Bookstore'da üç örnek çıktı:
src/Bookstore.Api/Catalog/Isbn.cs(7,51): warning MA0009: Regular expressions should not be vulnerable to Denial of Service attacks
src/Bookstore.Api/Catalog/Isbn.cs(7,51): warning S6444: Pass a timeout to limit the execution time.
src/Bookstore.Api/Program.cs(48,1): warning S6966: Await RunAsync instead.
İlk iki satır aynı satır, aynı sütun ve aynı öneri: regex'e timeout ver. Üçüncüsü, ilk yazıda All modda gördüğümüz CA1849'un Sonar karşılığı. MA0004 ise daha ilginç: CA2007'yi ASP.NET Core uygulamasında anlamsız olduğu için ikinci yazıda bilerek kapatmıştık; Meziantou aynı kuralı başka bir ID ile 47 yerde geri getirdi. Paket eklemek, o paketin kural setini kabul etmek demek değil; her paket için ikinci yazıdaki gibi bir karar listesi gerekir: hangi kural error, hangisi warning, hangisi başka bir paketin kopyası olduğu için none.
Süreyi ölçmek için derleyicinin kendi raporu var: -p:ReportAnalyzer=true ve detaylı log seviyesi:
$ dotnet build src/Bookstore.Api --no-incremental -p:ReportAnalyzer=true -v:d
Total analyzer execution time: 0.812 seconds.
Time (s) % Analyzer
0.524 64 SonarAnalyzer.CSharp, Version=10.36.0.0
0.129 15 Meziantou.Analyzer, Version=3.0.340.0
0.108 13 Roslynator.CSharp.Analyzers, Version=5.0.1.0
0.033 4 Microsoft.CodeAnalysis.NetAnalyzers, Version=10.0.402.0
Paketler olmadan aynı ölçüm 0,056 saniyeydi. Bookstore'un küçük API projesinde bu fark hissedilmez, ama oran önemli: analiz süresi yaklaşık 14 katına çıktı ve bunun üçte ikisi tek bir paketten geliyor. Rapor kural bazında da döküm veriyor; Sonar'ın en pahalı kuralı Bookstore'da S5344'tü (parola saklama). Büyük bir solution'da bu döküm, hangi kuralı kapatırsan en çok kazanacağını söyler.
Kodsuz Yol: BannedApiAnalyzers
Microsoft'un BannedApiAnalyzers paketi bir metin dosyasındaki API'leri yasaklar. Debugging ve Problem Çözme serisinin "Root Cause Analysis" yazısında bu paketi, banker's rounding yapan iki parametreli Math.Round overload'unu yasaklamak için kullanmıştık. Saat kuralı için BannedSymbols.txt:
P:System.DateTime.Now;Use an injected TimeProvider: clock.GetLocalNow()
P:System.DateTime.UtcNow;Use an injected TimeProvider: clock.GetUtcNow()
P:System.DateTime.Today;Use an injected TimeProvider: clock.GetLocalNow().Date
P:System.DateTimeOffset.Now;Use an injected TimeProvider: clock.GetLocalNow()
P:System.DateTimeOffset.UtcNow;Use an injected TimeProvider: clock.GetUtcNow()
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
<AdditionalFiles Include="BannedSymbols.txt" />
</ItemGroup>
src/Bookstore.Api/Ordering/CartCleanup.cs(13,27): warning RS0030: The symbol 'DateTime.UtcNow' is banned in this project: Use an injected TimeProvider: clock.GetUtcNow()
Satır başındaki P: property demek; format, C#'ın XML dokümantasyon ID'leri. Beş satır, sıfır kod. Bir kural yalnız "şu API'yi kullanma" diyorsa ilk tercih bu olmalı. Sınırları da belli. Code fix yok, geliştirici düzeltmeyi elle yapar. Kural proje bazında açılır ya da kapanır; "bu namespace'te serbest" gibi bir mantık kuramazsın. Mesaj sabit; kodun bağlamına göre değişmez. Bu sınırlardan biri seni zorluyorsa sıra kendi analyzer'ında.
Kendi Analyzer'ın: DiagnosticAnalyzer
Analyzer'lar derleyicinin içinde çalışır; Visual Studio, Rider ve dotnet build aynı derleyiciyi kullandığı için kural her yerde aynı davranır. Bu yüzden analyzer projesi netstandard2.0 hedefler ve derleyicinin API'si için Microsoft.CodeAnalysis.CSharp paketini referans eder:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Analyzer'ları derleyici yükler, bu yüzden netstandard2.0 hedeflerler. -->
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Project>
Microsoft.CodeAnalysis.Analyzers analyzer yazanlar için analyzer'lar getirir: RS ile başlayan kurallar, örneğin bir analyzer'ın thread-safe olmayan state tutmasını ya da yasak API kullanmasını yakalar. İki AnalyzerReleases dosyası, her kural ID'sinin hangi sürümde çıktığını kaydeden release tracking için. EnforceExtendedAnalyzerRules da analyzer'ın derleyicinin ortamında kullanamayacağı API'leri (dosya sistemi, environment gibi) yasaklar. Analyzer:
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Bookstore.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UseTimeProviderAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "BS0001";
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: "Use TimeProvider instead of the system clock",
messageFormat: "'{0}' reads the system clock; use an injected TimeProvider",
category: "Reliability",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Bookstore code gets the current time from TimeProvider so that tests can control the clock.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(start =>
{
ImmutableHashSet<IPropertySymbol> clockProperties = FindClockProperties(start.Compilation);
if (clockProperties.IsEmpty)
{
return;
}
start.RegisterOperationAction(
operation => AnalyzePropertyReference(operation, clockProperties),
OperationKind.PropertyReference);
});
}
private static ImmutableHashSet<IPropertySymbol> FindClockProperties(Compilation compilation)
{
ImmutableHashSet<IPropertySymbol>.Builder builder =
ImmutableHashSet.CreateBuilder<IPropertySymbol>(SymbolEqualityComparer.Default);
AddProperties(builder, compilation.GetSpecialType(SpecialType.System_DateTime), "Now", "UtcNow", "Today");
AddProperties(builder, compilation.GetTypeByMetadataName("System.DateTimeOffset"), "Now", "UtcNow");
return builder.ToImmutable();
}
private static void AddProperties(
ImmutableHashSet<IPropertySymbol>.Builder builder, INamedTypeSymbol? type, params string[] names)
{
if (type is null)
{
return;
}
foreach (string name in names)
{
foreach (IPropertySymbol property in type.GetMembers(name).OfType<IPropertySymbol>())
{
builder.Add(property);
}
}
}
private static void AnalyzePropertyReference(
OperationAnalysisContext context, ImmutableHashSet<IPropertySymbol> clockProperties)
{
IPropertyReferenceOperation reference = (IPropertyReferenceOperation)context.Operation;
if (!clockProperties.Contains(reference.Property))
{
return;
}
string name = $"{reference.Property.ContainingType.Name}.{reference.Property.Name}";
context.ReportDiagnostic(Diagnostic.Create(Rule, reference.Syntax.GetLocation(), name));
}
}
Üç karar dikkat istiyor. Birincisi callback türü. Syntax callback'i koddaki metne bakar: DateTime.UtcNow yazısını arardın ve using static System.DateTime; ile yazılmış tek kelimelik UtcNow'ı, ya da bir type alias'ını kaçırırdın. Operation callback'i derleyicinin semantik modelini görür: "şu property'ye bir referans" bilgisi, nasıl yazıldığından bağımsız. İkincisi symbol karşılaştırması. Property'leri bir kez, compilation başında buluyor ve SymbolEqualityComparer.Default ile bir set'te tutuyoruz; her operation'da isim string'i karşılaştırmak hem yavaş hem kırılgan olurdu. Üçüncüsü EnableConcurrentExecution ve ConfigureGeneratedCodeAnalysis: ilki analyzer'ın paralel çalışmasına izin verir, ikincisi üretilmiş kodu atlar. Microsoft.CodeAnalysis.Analyzers, bu ikisini çağırmayan bir analyzer için uyarı verir.
CodeFixProvider
Code fix'i ilk denemede aynı projeye koyduk ve Microsoft.CodeAnalysis.CSharp.Workspaces paketini ekledik:
Bookstore.Analyzers/UseTimeProviderAnalyzer.cs(8,2): warning RS1038: This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. The Microsoft.CodeAnalysis.Workspaces assembly is not provided during command line compilation scenarios, so references to it could cause the compiler extension to behave unpredictably.
Uyarı haklı: dotnet build sırasında derleyici Workspaces assembly'sini yüklemez, bu yüzden analyzer'ı ve code fix'i ayrı assembly'lerde tutmak gerekir. Code fix projesi analyzer projesini referans eder; code fix'in kendisi:
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseTimeProviderCodeFix)), Shared]
public sealed class UseTimeProviderCodeFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(UseTimeProviderAnalyzer.DiagnosticId);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
SemanticModel? model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null || model is null)
{
return;
}
if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not MemberAccessExpressionSyntax access
|| ClockCall(model, access) is not { } call)
{
return;
}
// Fix yalnız scope'ta zaten bir TimeProvider varsa önerilir (field, property, parametre ya da local).
INamedTypeSymbol? timeProvider = model.Compilation.GetTypeByMetadataName("System.TimeProvider");
string? clock = model.LookupSymbols(access.SpanStart)
.Where(s => s is IFieldSymbol or IPropertySymbol or IParameterSymbol or ILocalSymbol)
.Where(s => SymbolEqualityComparer.Default.Equals(TypeOf(s), timeProvider))
.Select(s => s.Name)
.OrderBy(n => n, StringComparer.Ordinal)
.FirstOrDefault();
if (clock is null)
{
return;
}
string replacement = $"{clock}.{call}";
context.RegisterCodeFix(
CodeAction.Create(
title: $"Use '{replacement}'",
createChangedDocument: _ => Task.FromResult(context.Document.WithSyntaxRoot(
root.ReplaceNode(access, SyntaxFactory.ParseExpression(replacement).WithTriviaFrom(access)))),
equivalenceKey: nameof(UseTimeProviderCodeFix)),
context.Diagnostics);
}
private static string? ClockCall(SemanticModel model, MemberAccessExpressionSyntax access) =>
model.GetSymbolInfo(access).Symbol is IPropertySymbol property
? (property.ContainingType.Name, property.Name) switch
{
("DateTime", "UtcNow") => "GetUtcNow().UtcDateTime",
("DateTime", "Now") => "GetLocalNow().DateTime",
("DateTime", "Today") => "GetLocalNow().Date",
("DateTimeOffset", "UtcNow") => "GetUtcNow()",
("DateTimeOffset", "Now") => "GetLocalNow()",
_ => null,
}
: null;
private static ITypeSymbol? TypeOf(ISymbol symbol) => symbol switch
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
IParameterSymbol parameter => parameter.Type,
ILocalSymbol local => local.Type,
_ => null,
};
}
Fix bilinçli olarak dar. Scope'ta bir TimeProvider yoksa hiçbir şey önermiyor, çünkü TimeProvider.System'ı koda gömmek kuralın amacını boşa çıkarır; doğru düzeltme dependency injection ile bir TimeProvider almak ve bu bir tasarım kararı. Bir code fix'in otomatik yapabileceği tek şey, kararı zaten verilmiş bir değişikliği yazmak. LookupSymbols, primary constructor parametrelerini de görür; CartCleanup'taki clock tam olarak böyle bulunur.
Microsoft.CodeAnalysis.Testing ile Test
Analyzer'ı Bookstore'a takıp elle denemek yavaş ve eksik kalır. Roslyn ekibinin test kütüphanesi, kaynak kodu bellekte derleyip beklenen diagnostic'leri ve code fix sonucunu karşılaştırır. Markup syntax'ında {|BS0001:...|}, o aralıkta tam olarak o diagnostic'in beklendiğini söyler:
public sealed class UseTimeProviderTests
{
[Fact]
public async Task UtcNow_is_replaced_with_the_injected_clock()
{
const string before = """
using System;
public sealed class CartCleanup(TimeProvider clock)
{
public DateTime Cutoff() => {|BS0001:DateTime.UtcNow|}.AddDays(-30);
}
""";
const string after = """
using System;
public sealed class CartCleanup(TimeProvider clock)
{
public DateTime Cutoff() => clock.GetUtcNow().UtcDateTime.AddDays(-30);
}
""";
await VerifyFixAsync(before, after);
}
[Fact]
public async Task Using_static_is_reported_too()
{
const string source = """
using System;
using static System.DateTime;
public static class Clock
{
public static DateTime Now() => {|BS0001:UtcNow|};
}
""";
await VerifyFixAsync(source, source);
}
[Fact]
public async Task Other_DateTime_members_are_allowed()
{
const string source = """
using System;
public static class Dates
{
public static DateTime AsUtc(DateTime value) => DateTime.SpecifyKind(value, DateTimeKind.Utc);
public static DateTime Epoch => DateTime.UnixEpoch;
}
""";
await VerifyFixAsync(source, source);
}
private static Task VerifyFixAsync(string source, string fixedSource)
{
CSharpCodeFixTest<UseTimeProviderAnalyzer, UseTimeProviderCodeFix, DefaultVerifier> test = new()
{
TestCode = source,
FixedCode = fixedSource,
ReferenceAssemblies = ReferenceAssemblies.Net.Net100,
};
return test.RunAsync(TestContext.Current.CancellationToken);
}
}
Dosyada iki test daha var: DateTimeOffset.Now'ın bir field üzerinden time.GetLocalNow()'a çevrilmesi ve scope'ta TimeProvider yokken diagnostic verilip fix önerilmemesi. FixedCode'un TestCode ile aynı olması "fix uygulanınca hiçbir şey değişmemeli" demek. ReferenceAssemblies.Net.Net100, test kodunun hangi .NET sürümünün referans assembly'leriyle derleneceğini açıkça seçer; böylece testler kütüphanenin varsayılanına değil, Bookstore'un hedeflediği net10.0'a karşı koşar. DefaultVerifier test framework'üne bağlı değil; Bookstore'un diğer testleri gibi xUnit.net v3 ile koşuyor:
$ dotnet test --project tests/Bookstore.Analyzers.Tests
Test run summary: Passed!
total: 5
failed: 0
succeeded: 5
skipped: 0
Üçüncü test kuralın sınırını belgeliyor: BookstoreContext'teki DateTime.SpecifyKind gibi saat okumayan üyeler serbest. Bir analyzer'ın false positive'i bu testlerde yakalanır, ekibin build'inde değil.
Bookstore'a Bağlamak ve NuGet Paketi
Aynı solution'daki bir projede analyzer'ı kullanmak için ProjectReference'ı analyzer olarak işaretlemek yeterli:
<ItemGroup>
<!-- Bookstore'un kendi kuralları derleyicide çalışır; assembly uygulamaya referans edilmez. -->
<ProjectReference Include="..\Bookstore.Analyzers\Bookstore.Analyzers.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Bookstore.Analyzers.CodeFixes\Bookstore.Analyzers.CodeFixes.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
Referansı yalnız Bookstore.Api'ye ekledik; "domain kodunda sistem saati yok" kuralı test projelerini kapsamıyor. .globalconfig'e dotnet_diagnostic.BS0001.severity = error satırını ekledikten sonra Ayşe'nin PR'ı:
src/Bookstore.Api/Ordering/CartCleanup.cs(13,27): error BS0001: 'DateTime.UtcNow' reads the system clock; use an injected TimeProvider
src/Bookstore.Api/Ordering/CartCleanup.cs(7,67): warning CS9113: Parameter 'clock' is unread.
Code fix IDE'de ampul olarak görünür; komut satırında da dotnet format ile uygulanabilir:
$ dotnet format analyzers Bookstore.slnx --diagnostics BS0001 --severity warn -v n
Formatting code files in workspace 'Bookstore.slnx'.
Formatted code file 'src/Bookstore.Api/Ordering/CartCleanup.cs'.
Format complete in 3174ms.
DateTime cutoff = clock.GetUtcNow().UtcDateTime - MaxAge;
Hem BS0001 hem CS9113 kayboldu. Kuralı başka repo'larla paylaşmak için NuGet paketi gerekir. Analyzer paketinin düzeni standart: DLL'ler (derlenmiş .NET assembly dosyaları) lib/ altında değil, analyzers/dotnet/cs/ altında durur. Bunun için ayrı bir paketleme projesi kurduk; IncludeBuildOutput=false, DevelopmentDependency=true ve iki DLL'i doğru klasöre koyan küçük bir MSBuild target'ı:
$ dotnet pack src/Bookstore.Analyzers.Package -o artifacts/packages
Successfully created package 'artifacts/packages/Bookstore.CodeAnalysis.1.0.0.nupkg'.
$ unzip -l artifacts/packages/Bookstore.CodeAnalysis.1.0.0.nupkg
_rels/.rels
Bookstore.CodeAnalysis.nuspec
analyzers/dotnet/cs/Bookstore.Analyzers.CodeFixes.dll
analyzers/dotnet/cs/Bookstore.Analyzers.dll
[Content_Types].xml
package/services/metadata/core-properties/nuget.psmdcp
Paketi yerel bir klasörden ayrı bir projeye ekleyip DateTime.Now kullanan bir Shipping sınıfı derlediğimizde warning BS0001: 'DateTime.Now' reads the system clock geldi. Paket ID'sini Bookstore.Analyzers yapmayı da denedik; solution'da aynı adlı bir proje olduğu için restore "Ambiguous project name" hatası verdi, bu yüzden paketin adı Bookstore.CodeAnalysis.
Üretimde Dikkat
Roslyn sürümünü düşük tut. Analyzer'ın referans ettiği Microsoft.CodeAnalysis.CSharp sürümü, onu yükleyecek derleyiciden yeni olamaz. Denedik: paketi 5.12.0'a çıkarıp SDK 10.0.402 ile derlediğimizde build başarılı oldu ama şu satırla:
CSC : warning CS9057: Analyzer assembly 'src/Bookstore.Analyzers/bin/Debug/netstandard2.0/Bookstore.Analyzers.dll' cannot be used because it references version '5.12.0.0' of the compiler, which is newer than the currently running version '5.9.0.0'.
Tehlikeli olan bu: BS0001 error olarak ayarlı olduğu hâlde build yeşil, çünkü analyzer hiç yüklenmedi ve tek iz bir warning. Bookstore 4.14.0'ı seçti; bu sürüm .NET 10 SDK'sının ve ekibin kullandığı IDE'lerin derleyicisinden eski. Analyzer'ı başka ekiplere dağıtıyorsan desteklediğin en eski IDE ve SDK'ya göre seç ve CS9057'yi CI'da error yap.
IDE performansını izle. Visual Studio ve Rider analyzer'ları her tuş vuruşundan sonra arka planda çalıştırır. Build'de 0,5 saniye süren bir paket, büyük bir solution'da editörde gecikme olarak hissedilir. Visual Studio'nun arka plan analizinin kapsamı (yalnız açık dosyalar ya da bütün solution) ayarlanabilir; ama önce ReportAnalyzer dökümüyle pahalı kuralı bul.
Kendi analyzer'ına da sürüm ver. Kuralın davranışını değiştirdiğinde (yeni bir property'yi yasaklamak gibi) bu, ikinci yazıdaki SDK yükseltmesiyle aynı etkiyi yaratır: kodu değişmemiş repo'larda build kırılır. Release tracking dosyaları ve semantik sürüm bunun için var.
Sık Düşülen Tuzaklar
netstandard2.0 ile Nullable çakışması. Bookstore'un Directory.Build.props'u bütün projelerde nullable'ı açıyor; netstandard2.0'ın varsayılan C# sürümü ise 7.3. Paketleme projesinde LangVersion eksik kaldığında build error CS8630: Invalid 'nullable' value: 'Enable' for C# 7.3 ile kırıldı.
Test paketinin getirdiği transitive bağımlılıkları görmemek. Microsoft.CodeAnalysis.Testing, NuGet client kütüphaneleri üzerinden System.Security.Cryptography.Pkcs 9.0.6'yı getiriyor ve NuGetAudit bunun için NU1903 (yüksek seviyeli güvenlik bildirimi) verdi. Central Package Management'ın transitive pinning özelliğini açmak ilk akla gelen çözüm; ama bütün projelere uygulandığı için Microsoft.EntityFrameworkCore.Design'ın Roslyn 5.0 bağımlılığıyla çakıştı ve restore NU1107 verdi. Çözüm, düzeltilmiş sürümü yalnız test projesinde doğrudan referans etmek oldu.
Analyzer ile source generator'ı karıştırmak. İkisi aynı Roslyn API'sini kullanır ve aynı şekilde paketlenir, ama generator kod üretir, analyzer yalnız rapor eder. "Bu kodu yazmak yerine üretelim" diyorsan C# İleri Seviye serisinin "C#'ta Source Generator" yazısına bak; analyzer'ın içinde kod üretmeye kalkma.
Code fix'i analyzer'ın yerine koymak. Code fix yalnız IDE'de ve dotnet format'ta çalışır; build'de kural ihlalini yakalayan analyzer'dır. Testleri olmayan bir code fix, kodu derlenmeyen bir hâle getirebilir; yukarıdaki testlerin yarısı bu yüzden "fix sonrası kod" karşılaştırması.
Küçük Bir Deneme
★☆☆ Kendi projende -p:ReportAnalyzer=true -v:d ile bir build al ve en pahalı üç analyzer'ı ve en pahalı üç kuralı bul. Bu kurallardan biri ekibinin hiç işine yaramıyorsa kapatıp süreyi yeniden ölç.
★★☆ BS0001'e ikinci bir kural ekle: BS0002, Program.cs dışında TimeProvider.System kullanımını raporlasın. İpucu: context.Operation.Syntax.SyntaxTree.FilePath. İkisi için de önce testi yaz.
★★★ Bookstore'un module sınırını bir analyzer'la koru: Bookstore.Ordering namespace'indeki kod Bookstore.Catalog'dan yalnız ICatalogService ve BookInfo'yu kullanabilsin. BannedApiAnalyzers bunu yapabilir mi? Yapamıyorsa analyzer'ın hangi callback'i kullanmalı?
Böylece .NET tarafında SDK kuralları, üçüncü parti paketler ve Bookstore'a özgü kurallar tek bir .globalconfig'te yönetiliyor. Sıradaki yazıda Angular istemcisine geçiyoruz: ESLint 10'un flat config'i, typescript-eslint'in tip bilgisi kullanan kuralları ve angular-eslint'in template kuralları.