@@ -29,7 +29,7 @@ dotnet restore Splat.DI.SourceGenerator.slnx
2929# Build the solution
3030dotnet build Splat.DI.SourceGenerator.slnx -c Release
3131
32- # Build with warnings as errors (includes StyleCop violations)
32+ # Build with warnings as errors (includes StyleSharp/PerformanceSharp/SecuritySharp violations)
3333dotnet build Splat.DI.SourceGenerator.slnx -c Release -warnaserror
3434
3535# Clean the solution
@@ -149,14 +149,23 @@ See https://tunit.dev/docs/reference/command-line-flags for complete TUnit flag
149149Splat.DI.SourceGenerator is a high-performance C# source generator that produces compile-time dependency injection registrations for Splat. It eliminates runtime reflection, provides full native AOT support, and includes intelligent analyzers with automatic code fixes.
150150
151151** Generator Project (` Splat.DependencyInjection.SourceGenerator/ ` )**
152- - ` Generator.cs ` - IIncrementalGenerator entry point with CreateSyntaxProvider pipeline
153- - ` Models/ ` - Value-equatable POCO records (no ISymbol/SyntaxNode references)
154- - ` RegistrationInfo.cs ` - Base record for all registration types
155- - ` TransientRegistrationInfo.cs ` , ` LazySingletonRegistrationInfo.cs ` , ` ConstantRegistrationInfo.cs `
156- - ` ConstructorParameter.cs ` , ` PropertyInjection.cs `
157- - ` EquatableArray.cs ` - Value-equatable array wrapper for pipeline caching
158- - ` CodeGeneration/CodeGenerator.cs ` - String-based code generation (not SyntaxFactory)
159- - ` Constants.cs ` - Attribute definitions with ` [Embedded] ` attribute
152+ - ` Generator.cs ` - IIncrementalGenerator entry point: one syntax provider for both marker methods, then a split
153+ into a code output (registrations only) and a diagnostics output (registrations with locations)
154+ - ` RoslynHelpers.cs ` - The syntax predicate and small symbol helpers
155+ - ` MetadataExtractor.cs ` - The transform: turns a bound marker call into a ` RegistrationSite `
156+ - ` WellKnownSymbols.cs ` - The symbols extraction compares against, cached per compilation in a ` ConditionalWeakTable `
157+ - ` RegistrationValidator.cs ` - SPLATDI005/006/007, which need the whole registration graph
158+ - ` Models/ ` - Value-equatable pipeline models (no ISymbol, SyntaxNode or Location)
159+ - ` RegistrationInfo.cs ` - Everything the generated code needs for one registration (` RegistrationKind ` says which)
160+ - ` RegistrationSite.cs ` - A registration plus its ` LocationInfo ` , for the diagnostics
161+ - ` LocationInfo.cs ` - A location held as values (path, span, line span)
162+ - ` ConstructorParameter.cs ` , ` PropertyInjection.cs ` - ` readonly record struct ` s
163+ - ` EquatableArray.cs ` - Value-equatable array wrapper with a cached hash, read by index
164+ - ` CodeGeneration/ ` - Emission through ` SourceWriter ` (see Writing Generated Code)
165+ - ` CodeGenerator.cs ` - The registrations file
166+ - ` MarkerSource.cs ` - The fixed marker methods and attributes, added in post-initialization
167+ - ` SourceWriter.cs ` , ` SourceWriterExtensions.cs ` , ` PooledBuilder.cs ` - The writer and its pooled builder
168+ - ` Constants.cs ` - Names matched against and file names (linked into the analyzer, so constants only)
160169- ` DiagnosticWarnings.cs ` - Shared diagnostic descriptors
161170
162171** Analyzer Project (` Splat.DependencyInjection.Analyzer/ ` )**
@@ -166,31 +175,67 @@ Splat.DI.SourceGenerator is a high-performance C# source generator that produces
166175- ` CodeFixes/PropertyCodeFixProvider.cs ` - Fixes property setter accessibility
167176
168177** Test Projects**
169- - ` Splat.DependencyInjection.SourceGenerator.Tests/ ` - 342 snapshot tests using Verify.SourceGenerators
170- - ` Splat.DependencyInjection.Analyzer.Tests/ ` - 45 analyzer and code fix tests
178+ - ` Splat.DependencyInjection.SourceGenerator.Tests/ ` - Snapshot tests (` GeneratorSnapshot ` ) plus unit tests of
179+ the pipeline, extraction, validation, emission and caching
180+ - ` Splat.DependencyInjection.Analyzer.Tests/ ` - Analyzer and code fix tests
181+
182+ ** Benchmarks (` benchmarks/Splat.DI.SourceGenerator.Benchmarks/ ` )** - see ` benchmarks/README.md `
171183
172184### Key Architectural Patterns
173185
174186** Incremental Generator Pipeline (IIncrementalGenerator)**
175- - ** Predicate functions** - Fast syntax-only checks (e.g., ` IsRegisterInvocation ` )
176- - ** Transform functions** - Semantic analysis + POCO extraction (no ISymbol in output!)
177- - ** Generation functions** - String-based code output using StringBuilder
187+ - ** Predicate** (` RoslynHelpers.IsRegistrationInvocation ` ) - syntax only, allocation free. Every marker is generic
188+ and cannot infer its type arguments, and none takes a lambda, so calls without a type argument list or with a
189+ lambda argument (Splat's own ` resolver.Register<T>(() => ...) ` ) never reach the transform.
190+ - ** Transform** (` MetadataExtractor.Extract ` ) - binds the call. A ` CreateSyntaxProvider ` transform re-runs for every
191+ matching node on every compilation change, so it turns calls away as early and cheaply as it can.
192+ - ** Outputs** - the generated file is built from ` RegistrationInfo ` alone, so an edit that only moves a registration
193+ regenerates nothing. The graph diagnostics are reported from their own output, which also carries the locations.
194+ With no registrations, no registrations file is added (the partial method is removed by the compiler).
195+ - Steps carry tracking names (` Generator.SitesStep ` and so on); ` GeneratorTests ` asserts what an edit re-runs.
178196
179197** Value-Equatable Models (Critical for Caching)**
180- - ALL pipeline models must implement ` IEquatable<T> `
181- - NEVER include ISymbol or SyntaxNode references in pipeline outputs
198+ - Pipeline models are ` sealed record ` or ` readonly record struct ` types with value equality
199+ - NEVER include ISymbol, SyntaxNode or ` Location ` in pipeline outputs; use ` LocationInfo `
182200- Use ` EquatableArray<T> ` for array equality in records
183201- Extract strings from symbols using ` ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) `
202+ - Records on netstandard2.0 need ` IsExternalInit ` , which is inlined in ` src/Polyfills/ ` and linked into the
203+ generator and analyzer projects, as elsewhere in rxui
184204
185205** Generic-First API (AOT Compatible)**
186206- Generated code uses ` resolver.Register<T>() ` instead of ` Register(factory, typeof(T)) `
187207- Eliminates boxing for value types
188208- Full Native AOT and trimming support
189209
190- ** Code Generation Strategy**
191- - Uses StringBuilder with raw string literals (` """ ` )
192- - 3-5x faster than SyntaxFactory approach
193- - No dependency on ReactiveMarbles.RoslynHelpers or ILRepack
210+ ** What the generated code allocates**
211+ - Resolving a service allocates the objects it constructs and nothing else from the generated code; Splat's
212+ ` GetServices ` collection is Splat's. A missing dependency throws through a ` ThrowNotRegistered ` helper written once
213+ per file, so factories stay small.
214+ - A factory that needs nothing from the resolver captures nothing, so its delegate is cached once by the compiler.
215+ - ` SetupIOC ` allocates one closure (for ` resolver ` and every lazy), one delegate per capturing registration, and for
216+ each lazy singleton its ` Lazy<T> ` and three delegates. Lazies are method-level locals (` lazy0 ` , ` lazy1 ` , ...), not
217+ locals of a block per registration: a block adds a closure object each.
218+
219+ ** Interceptors are not used.** The marker calls are empty methods that registration never runs through, and
220+ ` SetupIOC ` is a direct call to the generated method, so there is no call to redirect. Intercepting the marker calls
221+ would change when and on which resolver registration happens.
222+
223+ ### Writing Generated Code
224+
225+ ` SourceWriter ` owns the layout of every generated file. An emitter says what it writes, and the writer decides the
226+ indentation.
227+
228+ - ** The writer tracks the level.** A line is indented when its first character is written. A blank line carries no
229+ whitespace. Every line ends with ` \n ` . No string literal in an emitter starts with spaces.
230+ - ** Blocks change the level.** ` OpenBlock ` and ` CloseBlock ` write the braces and move one level in and out.
231+ ` OpenContinuation ` /` CloseContinuation ` put the arguments of a multi-line call one level deeper.
232+ - ** Each emitter method writes at the level it is given** and leaves it as it found it; its doc comment says where
233+ it leaves the writer.
234+ - ** C# constructs have names** in ` SourceWriterExtensions ` (namespaces, continuations, argument separators). Domain
235+ shapes live in ` CodeGenerator ` (` AppendFactoryCall ` , ` AppendFactory ` , ` AppendService ` ).
236+ - ** Fixed text is written with ` Lines ` ** from a raw string literal whose indentation is relative to column zero.
237+ - ` SourceWriter.Rent ` takes its ` StringBuilder ` from ` PooledBuilder ` 's per-thread slot, and ` ToStringAndReturn `
238+ gives it back.
194239
195240** Analyzer Separation (Roslyn Best Practice)**
196241- Generator focuses on code generation only
@@ -213,8 +258,11 @@ Splat.DI.SourceGenerator is a high-performance C# source generator that produces
213258### Style Enforcement
214259
215260- EditorConfig rules (` .editorconfig ` ) - comprehensive C# formatting and naming conventions
216- - StyleCop Analyzers - builds fail on violations
217- - Roslynator Analyzers - additional code quality rules
261+ - StyleSharp.Analyzers (SST), PerformanceSharp.Analyzers (PSH) and SecuritySharp.Analyzers (SES) - builds fail on
262+ violations. Rule docs: https://github.com/glennawatson/RoslynCommonAnalyzers/tree/main/docs/rules
263+ - Fix the code. Do not suppress rules, do not disable them in ` .editorconfig ` , and do not use
264+ ` #pragma warning disable ` (the ` #pragma ` for obsolete members inside the generated output is part of what the
265+ generator emits, not our source)
218266- Analysis level: latest with enhanced .NET analyzers
219267- ** All public APIs require XML documentation comments** (including protected methods of public classes)
220268
@@ -239,7 +287,15 @@ See `.github/COPILOT_INSTRUCTIONS.md` for complete style guide.
239287- Test projects: ` Splat.DependencyInjection.Analyzer.Tests ` and ` Splat.DependencyInjection.SourceGenerator.Tests `
240288- Coverage configured in ` src/testconfig.json ` (Cobertura format)
241289- Parallel test execution disabled (` "parallel": false ` in testconfig.json)
242- - Snapshot testing uses Verify.SourceGenerators with ` *.verified.cs ` files
290+ - Snapshot tests compare every generated registrations file with a ` *.verified.cs ` snapshot beside the test class,
291+ through ` GeneratorSnapshot.cs ` . A snapshot is named ` {type}.{method}_{arguments}#{hint name}.verified.cs ` ; generator
292+ diagnostics go to ` ...#Diagnostics.verified.txt ` . The fixed marker source and embedded attribute are stored once, by
293+ ` SnapshotTests.WritesFixedSources ` . An output that differs or has no snapshot is written beside it as ` .received `
294+ and fails the test, as does a snapshot the run no longer produces
295+ - To accept new or changed snapshots, run the tests with ` ACCEPT_SNAPSHOTS=1 ` , then run them again without it
296+ - Snapshot paths must fit the Windows path limit on a CI runner (` D:\a\{repo}\{repo}\... ` , under 256 characters);
297+ ` SnapshotTests.SnapshotPathsFitWindowsLimit ` checks this, so keep test names and arguments short
298+ - Line endings are LF everywhere (` .gitattributes ` ); the analyzers fail the build on CRLF
243299- Always write unit tests for new features or bug fixes
244300- Follow existing test patterns in test projects
245301- Use ` TestUtilities.AreEquivalent() ` for newline-agnostic source code comparison
@@ -259,9 +315,9 @@ See `.github/COPILOT_INSTRUCTIONS.md` for complete style guide.
2593152 . ** Create value-equatable POCOs** - no ISymbol/SyntaxNode references
2603163 . Create failing tests first (snapshot tests in SourceGenerator.Tests)
2613174 . Implement minimal functionality in Generator.cs
262- 5 . Update code generation in CodeGenerator.cs (use StringBuilder, not SyntaxFactory)
318+ 5 . Update code generation in CodeGenerator.cs through ` SourceWriter ` (not a raw StringBuilder, not SyntaxFactory)
2633196 . Ensure generic-first API usage: ` resolver.Register<T>() ` not ` typeof(T) `
264- 7 . Verify snapshots match expected output (C# 7.3 compatible)
320+ 7 . Verify snapshots match expected output; a passing snapshot test also requires the generated code to compile
2653218 . Add XML documentation to all public APIs
2663229 . Run formatting validation before committing
267323
@@ -277,27 +333,28 @@ See `.github/COPILOT_INSTRUCTIONS.md` for complete style guide.
277333
278334### Fixing Bugs
279335
280- 1 . Create reproduction test (use Verify snapshots)
336+ 1 . Create reproduction test (use snapshots)
2813372 . Fix with minimal changes
2823383 . Ensure pipeline still caches properly (POCOs value-equatable)
2833394 . Verify no regression in existing tests
2843405 . Accept snapshot changes if expected
285341
286342### Updating Generated Code Format
287343
288- 1 . Modify code generation in ` CodeGeneration/CodeGenerator.cs `
289- 2 . Use raw string literals (` """ ` ) for multi-line code
344+ 1 . Modify code generation in ` CodeGeneration/CodeGenerator.cs ` through ` SourceWriter `
345+ 2 . Write fixed multi-line text as raw string literals (` """ ` ) passed to ` Lines `
2903463 . Ensure generic-first API: ` resolver.Register<T>() ` not ` typeof(T) `
291- 4 . Run all snapshot tests - expect 342 failing tests
292- 5 . Review each ` .verified.cs ` diff carefully
293- 6 . Accept snapshots only if changes are correct
294- 7 . Ensure C# 7.3 compatibility (no file-scoped namespaces, no init properties )
347+ 4 . Run all snapshot tests - expect every ` Splat.DI.Reg.g ` snapshot to fail
348+ 5 . Review each received file carefully and accept ( ` ACCEPT_SNAPSHOTS=1 ` ) only if the changes are correct
349+ 6 . Keep file-scoped namespaces, init properties and similar newer syntax out of the generated code
350+ 7 . Measure with the benchmarks (see ` benchmarks/README.md ` )
295351
296352## What to Avoid
297353
298354- ** ISymbol/SyntaxNode in pipeline outputs** - breaks incremental caching
299355- ** Runtime reflection** in generated code - breaks AOT compatibility
300- - ** SyntaxFactory for code generation** - 3-5x slower than StringBuilder
356+ - ** SyntaxFactory or a raw StringBuilder for code generation** - write through ` SourceWriter `
357+ - ** LINQ in the generator** - use loops; the transform runs for every marker call on every compilation change
301358- ** Type-based API** - use ` resolver.Register<T>() ` not ` Register(factory, typeof(T)) `
302359- ** Diagnostics in generator** - use separate analyzer project instead
303360- ** Heavy dependencies** - keep generator lightweight (netstandard2.0 target)
@@ -308,9 +365,9 @@ See `.github/COPILOT_INSTRUCTIONS.md` for complete style guide.
308365
309366- ** Value-Equatable POCOs:** CRITICAL for incremental generator caching - never include ISymbol/SyntaxNode
310367- ** Generic-First API:** All generated code must use ` resolver.Register<T>() ` for AOT compatibility
311- - ** String-Based Generation:** Use StringBuilder with raw string literals , not SyntaxFactory
368+ - ** Generation:** Write through ` SourceWriter ` , not SyntaxFactory
312369- ** Separate Analyzer:** Diagnostics in separate project following Roslyn best practices
313- - ** No shallow clones:** Repository requires full clone for git version information used by Nerdbank.GitVersioning
370+ - ** No shallow clones:** Repository requires full clone for git version information used by MinVer
314371- ** Required .NET SDKs:** .NET 8.0, 9.0, and 10.0 (all three required for full build)
315372- ** Snapshot Testing:** Review ` .verified.cs ` diffs carefully before accepting
316373- ** Comprehensive Instructions:** ` .github/COPILOT_INSTRUCTIONS.md ` contains detailed development guidelines
0 commit comments