Overview
Haitch.Roslyn is a set of source-only utilities for Roslyn incremental source generators. The package ships .cs files that compile into your generator project as internal types, so nothing is added to your generator’s runtime dependencies.
It covers the parts of a generator that are easy to get subtly wrong: value-equal models so incremental caching works, a one-item-per-type attribute provider, cache-safe diagnostics, and indented source writing. A second, regular package, Haitch.Roslyn.Testing, provides test helpers.
Requirements
Section titled “Requirements”- C# 14 or later (
<LangVersion>14</LangVersion>orlatest), because the Polyfill dependency uses C# 14 extension blocks. - Your own reference to
Microsoft.CodeAnalysis.CSharp4.12.0 or later. Generator projects already have one; the package deliberately does not pass Roslyn on as a dependency. - Polyfill arrives as a transitive dependency, so there is nothing to add for it.
The package ships its source as contentFiles, which compile in netstandard2.0 generator projects.
Installation
Section titled “Installation”dotnet add package Haitch.Roslyn --version 0.6.1dotnet add package Haitch.Roslyn.Testing --version 0.6.1 # in your test project<PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>14</LangVersion></PropertyGroup><ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" /> <PackageReference Include="Haitch.Roslyn" Version="0.6.1" PrivateAssets="all" /></ItemGroup>What’s inside
Section titled “What’s inside”| Namespace | Types | Page |
|---|---|---|
Haitch.Roslyn.Types | EquatableArray<T> | Models |
Haitch.Roslyn.Models | TypeRef, AttributeModel, WellKnownAttributes, ConstantValue, TypeModel, NewTypeModel, ContainingTypeModel, member models (including EventModel), SyntaxInfo, BuildProperties, AdditionalFileModel | Models |
Haitch.Roslyn.Generators | ForTypesWithAttribute, ForMethodsWithAttribute, ForPropertiesWithAttribute, ForFieldsWithAttribute, ForBuildProperty, ForBuildProperties, ForAdditionalFiles, AddMarkerAttribute, AddEmbeddedAttributeDefinition, HintName | Pipeline helpers |
Haitch.Roslyn.Diagnostics | LocationInfo, DiagnosticInfo, Result<T>, Result.Combine/Collect, ReportDiagnostics, PartialTypeValidation | Diagnostics |
Haitch.Roslyn.Writing | SourceWriter, the typed scoped writer, attribute and new-type rendering, statement scopes | Source writing, Attributes, new types and statements |
Haitch.Roslyn.Testing | GeneratorHarness, GeneratorHarnessInput, GeneratorHarnessResult and its assertions, CacheabilityOptions, CachingHazardWalker | Testing |
For a complete generator that uses all of these, see the Walkthrough.
What’s new in 0.6.1
Section titled “What’s new in 0.6.1”A bug-fix release. One change can break a caller: MethodModel has a new positional IsPartial parameter after IsPartialDefinition, so code that constructs it positionally must add it.
- The writer throws
ArgumentExceptionfor a method that is not an ordinary method or an explicit interface implementation, instead of writing output that does not compile. See Methods. MethodModel.IsPartialistruefor both parts of a partial method, and the writer emitspartialfrom it, so a written implementation part compiles next to its definition. See Methods.- Diagnostics reported through the
ReportDiagnosticspipeline extension are bound to the compilation’s syntax tree, so#pragma warning disablesuppresses them. See ReportDiagnostics.
What’s new in 0.6.0
Section titled “What’s new in 0.6.0”The Roslyn 4.12 floor is unchanged. The release lets member discovery see the sibling members of the containing type, and models C# 14 partial events. One change can break a caller: includeContainingTypeMembers sits before predicate on the three member providers, so a call that passed the predicate positionally must name it, as in predicate: ....
Pipeline
ForFieldsWithAttribute,ForPropertiesWithAttributeandForMethodsWithAttributetakeincludeContainingTypeMembers(defaultfalse). Whentrue,ContainingTypecarries fields, properties, methods, events and member names, so a generator can check for clashes without a second step. See Member discovery.
Models
TypeModel.MemberNameslists the distinct, ordinal-sorted name of every member, including nested types, accessors, indexers, constructors, operators and compiler-made members. It is filled only withincludeMembers. See Member names.- A C# 14 partial event yields one
EventModelwithIsPartialset, andIsFieldLikeis alwaysfalsefor it. See Base types, interfaces and events.
What was new in 0.5.0
Section titled “What was new in 0.5.0”The Roslyn 4.12 floor is unchanged. The release finishes the testing harness and adds a raw-line writer for type bodies. Three changes can break a test: the harness overloads that took additionalReferences and parseOptions are gone, AssertCacheable now fails a step that holds the options provider by reference, and AdditionalTexts now holds AdditionalText, so write new HarnessAdditionalText(...) rather than target-typed new(...). HarnessAdditionalText is no longer a record, so it has no with, value equality or deconstruction.
Testing
- The harness entry points are exactly
Run(generator, GeneratorHarnessInput),Run(generator, params string[] sources),AssertCacheable(generator, GeneratorHarnessInput, steps, options)andAssertCacheable(generator, sources, params string[] trackedStepNames). UseGeneratorHarnessInputfor references and parse options. See Testing generators. GeneratorHarnessInput.AdditionalTextstakes anyAdditionalText.UnreadableAdditionalTexttests files the compiler cannot read, andHarnessAdditionalTextis now a sealed class. See Additional texts.- Every
AssertCacheablererun uses a new options provider built from the same options, as the IDE does. See AssertCacheable.
Writing
TypeScope.Linewrites a raw line, such as a#pragma, at the type body’s indent, andLineon every scope now defaultstextto"". See Raw lines.
What was new in 0.4.0
Section titled “What was new in 0.4.0”The Roslyn 4.12 floor is unchanged, and the 0.3 Run and AssertCacheable overloads still compile (removed in 0.5.0). The release serves generator authors end to end: testing with realistic input, finding members, and reading options.
Testing
GeneratorHarnessInputfeeds additional files, global and per-file analyzer options, and anAllowInputErrorsswitch to the harness.- Diagnostic assertions (
AssertNoDiagnostics,AssertDiagnostic) and expected-output assertions (AssertSource,AssertSourceFile, withHAITCH_ACCEPT=1). See Testing generators.
Pipeline
ForMethodsWithAttribute,ForPropertiesWithAttributeandForFieldsWithAttribute, and an optional syntax predicate on all fourFor...WithAttributemethods. See Member discovery.ForBuildProperty,ForBuildPropertiesandForAdditionalFiles, which return equatable models. See Build properties and Additional files.
Models and writing
- Typed
ConstantValueaccessors,AttributeModel.TryGetNamedArgument,TryGetConstructorArgumentandMetadataName, andEquatableArray<AttributeModel>.Find. See Reading attribute arguments. TypeModel.BaseType,Interfaces,AllInterfacesandEvents, andEventModel. See Base types, interfaces and events.TypeScope.Event, and a base-list parameter on theType(...)writers. See Events and base lists.PartialTypeValidation.ValidateContainingTypes, for members. See Validating members.
Smaller changes
EquatableArray<T>now constrainsT : IEquatable<T>?, soEquatableArray<string?>works.- The method renderer writes
abstract overridefor an abstract override.
What was new in 0.3.0
Section titled “What was new in 0.3.0”The package is built against the C# 15 release candidate; the Roslyn 4.12 floor is unchanged. Behaviour changes from 0.2.0 come first.
Breaking and behaviour changes
- Scoped-writer misuse now throws. The caller errors that 0.2.0 left to the compiler (writing to a parent while a child is open, chaining with a nested block open, a switch section that falls through, a bare
try, a danglingAttribute()) are detected. They always throw, not only in debug builds. See Writer guards. MethodModel.Nameis the unqualified name for explicit interface implementations. It wasSystem.IDisposable.Dispose; it is nowDispose, with the interface inExplicitInterface.TypeModel.From(includeMembers: true)now includes explicit interface methods and properties, soNameis no longer unique withinMethodsorProperties.WriteNewTypeDeclarationalways opens a body. A positional record is written asrecord Person(string Name) { }; use the newWriteBodylessNewTypeDeclarationfor the;form.TypeModel.FromthrowsArgumentExceptionfor an extension block.TypeDeclarationKindhas a newUnionmember. Aswitchover it needs a new arm, and a union is no longer reported as a struct.- Ref properties with
setorinitaccessors are rejected byProperty, andAutoPropertyrejects any ref-returning property.
New
- C# 15 unions and
closedtypes inTypeModelandNewTypeModel, and labeledbreak/continuein the scoped writer. See C# 15 support. TypeParameterModel.VarianceandNewTypeModel.PrimaryConstructorParameters.- An optional
CancellationTokenonTypeModel.FromandMethodModel.From, passed through byForTypesWithAttribute. FieldModel.IsVolatileandPropertyModel.ReturnRefKind.HintName.For(..., disambiguateCase: true). See HintName.
Why source-only
Section titled “Why source-only”A generator’s dependencies must be shipped and loaded inside the compiler host alongside the generator. Because the types here are compiled into your assembly as internal, there is no extra DLL to bundle, and no version clash when several generators in one build use different versions of the library.