A focused utility library for writing Roslyn source generators, analyzers, and code-fix providers. The shape is opinionated: the types here are the ones that make an incremental generator actually cache, a symbol comparison actually readable, and a code emission actually deterministic.
Agent test doubles, MAF conformance suites, and workflow fixtures live in the sibling ANcpLua.Agents package. This repo focuses on Roslyn/MSBuild/OTel-for-analyzer-authors.
The library is the author's own. Use it, improve it (the upstream is this repo), or propose switching directions — don't reinvent locally.
| Package | Target | What it's for |
|---|---|---|
ANcpLua.Roslyn.Utilities |
netstandard2.0 | Runtime + Roslyn utilities as a normal NuGet reference |
ANcpLua.Roslyn.Utilities.Sources |
source-only | Embeds the utilities as internal source into source generators (generators can't load NuGet DLLs at runtime) |
ANcpLua.Roslyn.Utilities.Polyfills |
source-only | init, required, Index/Range, nullable & trim attributes for netstandard2.0 |
ANcpLua.Roslyn.Utilities.Testing |
net10.0 | Generator/analyzer/codefix test infrastructure, MSBuild/NuGet integration tests, cross-framework web testing (xUnit/NUnit/TUnit/Bunit), OTel instrumentation helpers, BitNetFixture live-LLM integration |
ANcpLua.Roslyn.Utilities.Testing.Aot |
netstandard2.0 | AOT/trim test attributes ([AotTest], [TrimTest], [AotSafe], [TrimSafe]), TrimAssert, FeatureSwitches, MSBuild orchestration for standalone AOT verification |
dotnet add package ANcpLua.Roslyn.Utilities.SourcesA canonical incremental generator is ~25 lines when the utilities do their job — see src/ANcpLua.AotReflection/AotReflectionGenerator.cs in this repo for the reference shape:
[Generator]
public sealed class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var flows = context.SyntaxProvider.ForAttributeWithMetadataName(
"MyNamespace.TracedAttribute",
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, ct) => TypeExtractor.Extract(ctx, ct));
var types = flows.ReportAndStop(context);
var files = types
.Select(static (m, _) => OutputGenerator.Generate(m))
.CollectAsEquatableArray();
files.AddSources(context);
}
}What the library provides at each step:
ForAttributeWithMetadataName— filter syntax before expensive semantic work (Roslyn built-in; the library wraps for ergonomics)DiagnosticFlow<T>— railway-oriented pipeline carrying value + accumulated diagnostics, value-equatable for incremental cachingReportAndStop— drain the flow intoReportDiagnosticand yield valuesCollectAsEquatableArray/ToEquatableArray/ToEquatableArrayOrDefault— value-equatable collections so the generator cache actually hitsAddSources— emitFileWithNamerecords without boilerplateMatch.Type()/Match.Method()— fluent symbol matching, no string comparisonsIndentedStringBuilder—BeginBlock()/EndBlock()scopes, no hardcoded indentation stringsGeneratedCodeHelpers—<auto-generated/>headers,#nullable enable, pragma helpers
Agent test doubles (FakeChatClient, FakeReplayAgent, ActivityCollector), the provider-agnostic MAF conformance suite (RunTests, RunStreamingTests, ChatClientAgentRunTests, StructuredOutputRunTests), reference fixtures for OpenAI / Azure / Anthropic / Ollama / Gemini / OpenRouter, and the WorkflowFixture<TInput> harness ship in the sibling ANcpLua.Agents package.
DiagnosticFlow<T>+DiagnosticFlowReportingExtensions— railway-oriented flow with value + accumulatedDiagnosticInfoIncrementalValuesProviderExtensions—ReportAndStop,CollectAsEquatableArray,AddSources, and other pipeline verbsSyntaxValueProviderExtensions— ergonomicForAttributeWithMetadataNamewrappersSourceProductionContextExtensions— context helpersEquatableArray<T>+EquatableArrayExtensions(.ToEquatableArray,.ToEquatableArrayOrDefault,.AsEquatableArray) — value equality so the cache actually hitsFileWithName— record for emitted-file payloadDiagnosticInfo+LocationInfo+EquatableMessageArgs— value-equatable diagnostic carriersResultWithDiagnostics<T>— result payload paired with diagnostics, for pipelines where the value survives errors
Matchfluent DSL —Match.Type()/Match.Method()/Match.Property()/Match.Field()/Match.Parameter()with the per-kind matchers underMatching/(MethodMatcher,TypeMatcher, etc.)Invoke/InvocationMatcher— invocation-shape matchingSymbolExtensions,TypeSymbolExtensions,MethodSymbolExtensions,NamespaceExtensions—IsEqualTo,ImplementsInterface,GetAllBaseTypesAndSelf, and the usual suspectsAttributeExtensions—GetAttribute,HasAttribute,TryGetAttributewith constructor-arg accessorsInvocationExtensions/OperationExtensions/OperationHelper—IInvocationOperationanalysis helpersSemanticGuard<T>+SemanticGuard— chain semantic predicates with early-exitSemanticModelExtensions/CompilationExtensions/SyntaxExtensions
IndentedStringBuilder+IndentScope—BeginBlock()/EndBlock()scopes, no hardcoded indentationGeneratedCodeHelpers—<auto-generated/>header, nullable/pragma directivesValueStringBuilder— stack-allocatedStringBuilderfor hot pathsSyntaxBuilders(Analyzers/) — fluent builders for common code-fix patterns (ExtensionCall,StaticCall,NameOf)
DiagnosticAnalyzerBase— convention base classDiagnosticCategories/DiagnosticSeverities— category/severity constantsDeprecatedOtelAttributes— 50+ attribute-name mappings synced to OTel GenAI semconv 1.40 (catchesgen_ai.systemand friends during analysis)IncrementalPipelineHelpers— pipeline wiring utilitiesMappingRegistry/TypeCache<TEnum>— cached lookups for analyzer hot paths
AsyncSequenceExtensions— LINQ-ish operators overIAsyncEnumerable<T>ParallelAsyncExtensions— channel-based parallel enumeration with ordering controlExpiringCache<TKey,TValue>— TTL cache for analyzer resultsTimeConversions— OTel nanosecond helpers
DictionaryExtensions—GetOrInsert(key, context, factory)(closure-free, hot-path),GetOrAdd(key)/GetOrAdd(key, factory)(ergonomic),MapIfAbsent(src, dst, transform)(cross-key copy for vendor-adapter telemetry mappers)EnumerableExtensions— dedup, partition, chunking,MinBy/MaxByListExtensions— in-place mutation helpers
Guard— null / range / path / type guardsNullableExtensions—Map-style combinators forT?TryExtensions—TryParseX, dictionaryGetOrNull/GetOrDefault/GetOrElse
SpanKind,SpanStatusCode— OTel enumsPagedResult<T>— standard paginated response shape
Language and API backports for netstandard2.0 consumers (via the separate .Polyfills package):
| Polyfill | Enables | Opt-out |
|---|---|---|
Index / Range |
array[^1], array[1..3] |
InjectIndexRangeOnLegacy=false |
IsExternalInit |
record types, init setters |
InjectIsExternalInitOnLegacy=false |
| Nullable attributes | [NotNull], [MaybeNull], [MemberNotNull], [NotNullWhen] |
InjectNullabilityAttributesOnLegacy=false |
| Trim/AOT attributes | [RequiresUnreferencedCode], [DynamicallyAccessedMembers] |
InjectTrimAttributesOnLegacy=false |
TimeProvider |
Testable time abstraction | InjectTimeProviderPolyfill=false |
Lock |
System.Threading.Lock |
InjectLockPolyfill=false |
required, params collections, CallerArgumentExpression, UnreachableException, StackTraceHidden, ExperimentalAttribute |
C# 11–13 language features on ns2.0 | per-feature MSBuild props |
Disable all at once: <InjectAllPolyfillsOnLegacy>false</InjectAllPolyfillsOnLegacy>
Generator, analyzer, codefix, refactoring, MSBuild integration, and cross-framework web testing.
- Generator tests:
Test<TGenerator>(fluent entry),GeneratorTestHelper.RunGenerator<TGenerator>()(assertion-oriented one-liners),GeneratorCachingReport(compares two runs, detects cache hits),Compile/CompileResult(dynamic compilation with Shouldly-style assertions) - Analyzer / CodeFix / Refactoring tests:
AnalyzerTest<TAnalyzer>,CodeFixTest<TAnalyzer,TCodeFix>,CodeFixTestWithEditorConfig<TAnalyzer,TCodeFix>,RefactoringTest<TRefactoring>,SolutionRefactoringTest<TRefactoring>,LogAssert - MSBuild integration tests:
ProjectBuilder,PackageProjectBuilder,PackageTestBase<TFixture>/NuGetPackageTestBase,NuGetPackageFixture,BuildResult+BuildResultAssertions,SarifFileparsers,DotNetSdkHelpers/NetSdkVersion,MSBuildConstants - Web testing — one base per framework:
- xUnit (root):
IntegrationTestBase<TProgram>,KestrelTestBase<TProgram> WebTesting/NUnit/,WebTesting/TUnit/,WebTesting/Bunit/— per-framework equivalents- Plus
FakeLoggerExtensionsfor structured log capture
- xUnit (root):
- OTel instrumentation:
ActivityInstrumentation,MetricsInstrumentation,LoggingConventions,LogEnricherInfrastructure,DataClassificationHelpers(PII/Secret redaction) - Live-LLM integration:
BitNetFixture,BitNetAttribute,BitNetTestGroup— xUnit v3 collection fixture for llama.cpp BitNet server
Separate package (netstandard2.0, zero runtime deps). Attributes + MSBuild props for verifying AOT/trim behavior.
[AotTest],[AotSafe],[AotUnsafe]— mark methods as AOT-verified or AOT-excluded[TrimTest],[TrimSafe],[TrimUnsafe]— same for IL trimming;TrimModeenum selectsPartial/FullTrimAssert.TypePreserved(...)/TypeRemoved(...)— runtime assertions against the trimmed binaryFeatureSwitches— constants for common AOT feature switches (e.g.JsonReflection)AotRuntime— runtime AOT/trim detection- MSBuild orchestration (
build/*.props+*.targets+ProjectTemplate.csproj.txt) ships inside the package itself; no external SDK dependency
This repo also ships ANcpLua.Analyzers.AotReflection — an incremental source generator that replaces runtime typeof(T).GetProperties() with compile-time metadata, making types NativeAOT- and trim-safe. Mark a type [AotReflection] and a generated FooMetadata class appears with strongly-typed getter/setter/invoker delegates. Pair with ANcpLua.Roslyn.Utilities.Testing.Aot to verify the output survives PublishAot=true and PublishTrimmed=true.
- Compile-time over runtime reflection.
EquatableArray<T>for generator caching,DiagnosticFlow<T>for pipeline error accumulation,IndentedStringBuilderfor emission — the shape of an incremental generator the compiler cache actually likes. - Allocation-conscious in analyzer hot paths.
ValueStringBuilderfor stack-allocated string building,Boxesfor cached boxed primitives, closure-freeGetOrInsert<TContext>for dictionary inserts. - OTel semconv 1.40 native.
DeprecatedOtelAttributescatchesgen_ai.systemand 50+ other deprecated attribute names during analysis. - Test discipline.
GeneratorCachingReportover hand-rolled cache assertions.Test<TGenerator>fluent runner over hand-rolledCSharpGeneratorDriverplumbing. The shipped-internallyForbiddenTypeAnalyzercatchesISymbol/Compilationsnuck into pipeline models. Agent-specific test doubles live in the siblingANcpLua.Agentspackage. - Source-only delivery for generators. The
.Sourcespackage rewrites the utilities tointernalvia a pack-time PowerShell transform (Transform-Sources.ps1), because source generators can't load NuGet DLLs at runtime.
ancplua.mintlify.app/utilities — full API reference with examples.
- ANcpLua.NET.Sdk — opinionated .NET SDK wrapper built on the same conventions
- ANcpLua.Analyzers — the author's analyzer catalogue, built on these utilities
- ANcpLua.Agents — agent test doubles, MAF conformance, workflow fixtures
MIT © Alexander Nachtmann