Skip to content

Latest commit

 

History

History
194 lines (141 loc) · 13.7 KB

File metadata and controls

194 lines (141 loc) · 13.7 KB

NuGet .NET Standard 2.0 License: MIT

ANcpLua.Roslyn.Utilities

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.

Packages

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

Quickstart — writing a source generator

dotnet add package ANcpLua.Roslyn.Utilities.Sources

A 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 caching
  • ReportAndStop — drain the flow into ReportDiagnostic and yield values
  • CollectAsEquatableArray / ToEquatableArray / ToEquatableArrayOrDefault — value-equatable collections so the generator cache actually hits
  • AddSources — emit FileWithName records without boilerplate
  • Match.Type() / Match.Method() — fluent symbol matching, no string comparisons
  • IndentedStringBuilderBeginBlock()/EndBlock() scopes, no hardcoded indentation strings
  • GeneratedCodeHelpers<auto-generated/> headers, #nullable enable, pragma helpers

Testing an agent or a MAF workflow

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.

What's in the box

Incremental pipeline (generator authors)

  • DiagnosticFlow<T> + DiagnosticFlowReportingExtensions — railway-oriented flow with value + accumulated DiagnosticInfo
  • IncrementalValuesProviderExtensionsReportAndStop, CollectAsEquatableArray, AddSources, and other pipeline verbs
  • SyntaxValueProviderExtensions — ergonomic ForAttributeWithMetadataName wrappers
  • SourceProductionContextExtensions — context helpers
  • EquatableArray<T> + EquatableArrayExtensions (.ToEquatableArray, .ToEquatableArrayOrDefault, .AsEquatableArray) — value equality so the cache actually hits
  • FileWithName — record for emitted-file payload
  • DiagnosticInfo + LocationInfo + EquatableMessageArgs — value-equatable diagnostic carriers
  • ResultWithDiagnostics<T> — result payload paired with diagnostics, for pipelines where the value survives errors

Symbol analysis & matching

  • Match fluent DSLMatch.Type() / Match.Method() / Match.Property() / Match.Field() / Match.Parameter() with the per-kind matchers under Matching/ (MethodMatcher, TypeMatcher, etc.)
  • Invoke / InvocationMatcher — invocation-shape matching
  • SymbolExtensions, TypeSymbolExtensions, MethodSymbolExtensions, NamespaceExtensionsIsEqualTo, ImplementsInterface, GetAllBaseTypesAndSelf, and the usual suspects
  • AttributeExtensionsGetAttribute, HasAttribute, TryGetAttribute with constructor-arg accessors
  • InvocationExtensions / OperationExtensions / OperationHelperIInvocationOperation analysis helpers
  • SemanticGuard<T> + SemanticGuard — chain semantic predicates with early-exit
  • SemanticModelExtensions / CompilationExtensions / SyntaxExtensions

Code emission

  • IndentedStringBuilder + IndentScopeBeginBlock()/EndBlock() scopes, no hardcoded indentation
  • GeneratedCodeHelpers<auto-generated/> header, nullable/pragma directives
  • ValueStringBuilder — stack-allocated StringBuilder for hot paths
  • SyntaxBuilders (Analyzers/) — fluent builders for common code-fix patterns (ExtensionCall, StaticCall, NameOf)

Analyzer authoring

  • DiagnosticAnalyzerBase — convention base class
  • DiagnosticCategories / DiagnosticSeverities — category/severity constants
  • DeprecatedOtelAttributes — 50+ attribute-name mappings synced to OTel GenAI semconv 1.40 (catches gen_ai.system and friends during analysis)
  • IncrementalPipelineHelpers — pipeline wiring utilities
  • MappingRegistry / TypeCache<TEnum> — cached lookups for analyzer hot paths

Async & time

  • AsyncSequenceExtensions — LINQ-ish operators over IAsyncEnumerable<T>
  • ParallelAsyncExtensions — channel-based parallel enumeration with ordering control
  • ExpiringCache<TKey,TValue> — TTL cache for analyzer results
  • TimeConversions — OTel nanosecond helpers

Dictionary & collection ergonomics

  • DictionaryExtensionsGetOrInsert(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/MaxBy
  • ListExtensions — in-place mutation helpers

Guards & null handling

  • Guard — null / range / path / type guards
  • NullableExtensionsMap-style combinators for T?
  • TryExtensionsTryParseX, dictionary GetOrNull / GetOrDefault / GetOrElse

Models & OTel enums

  • SpanKind, SpanStatusCode — OTel enums
  • PagedResult<T> — standard paginated response shape

Polyfills

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>

Testing — what ships in .Testing

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, SarifFile parsers, 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 FakeLoggerExtensions for structured log capture
  • 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

AOT testing — what ships in .Testing.Aot

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; TrimMode enum selects Partial / Full
  • TrimAssert.TypePreserved(...) / TypeRemoved(...) — runtime assertions against the trimmed binary
  • FeatureSwitches — 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

Separate: AOT reflection generator

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.

Design principles

  • Compile-time over runtime reflection. EquatableArray<T> for generator caching, DiagnosticFlow<T> for pipeline error accumulation, IndentedStringBuilder for emission — the shape of an incremental generator the compiler cache actually likes.
  • Allocation-conscious in analyzer hot paths. ValueStringBuilder for stack-allocated string building, Boxes for cached boxed primitives, closure-free GetOrInsert<TContext> for dictionary inserts.
  • OTel semconv 1.40 native. DeprecatedOtelAttributes catches gen_ai.system and 50+ other deprecated attribute names during analysis.
  • Test discipline. GeneratorCachingReport over hand-rolled cache assertions. Test<TGenerator> fluent runner over hand-rolled CSharpGeneratorDriver plumbing. The shipped-internally ForbiddenTypeAnalyzer catches ISymbol/Compilation snuck into pipeline models. Agent-specific test doubles live in the sibling ANcpLua.Agents package.
  • Source-only delivery for generators. The .Sources package rewrites the utilities to internal via a pack-time PowerShell transform (Transform-Sources.ps1), because source generators can't load NuGet DLLs at runtime.

Documentation

ancplua.mintlify.app/utilities — full API reference with examples.

Related

  • 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

License

MIT © Alexander Nachtmann