Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **DI registration map consumer seam**: `CaptiveDependencyAnalyzer` and `SingletonLifecycleAnalyzer` obtain the map only via `DiRegistrationMap.Build(Compilation)` at compilation analysis time; incremental `DiRegistrationMapBuilder` wiring is a private implementation detail of `Build`. Diagnostic behaviour for DP062/066/068–071 is unchanged. `Build` skips generated syntax trees (`.g.cs` / auto-generated headers) so visibility matches the prior `RegisterSyntaxNodeAction` + `GeneratedCodeAnalysisFlags.None` path.

## [0.2.4-preview1] - 2026-08-03

### Added
Expand Down
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ _Avoid_: Decorator (for this capability), middleware (ambiguous), open-generic g
**Terminal handler**:
The single `ICommandHandler` registered for a command type; the innermost stage of the command pipeline onion.
_Avoid_: subscriber, strategy

**DI registration map**:
A compile-time type→lifetime view of explicit MSDI/Autofac registrations plus attributed `RegisterDi` expansions, including Singleton factory-delegate entries.
_Avoid_: IServiceCollection snapshot, container dump, RegisterDi argument-pair check (DP060/061)
59 changes: 7 additions & 52 deletions DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using DesignPatterns.Analyzers.Di;
using DesignPatterns.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

Expand Down Expand Up @@ -35,61 +34,17 @@ public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(OnCompilationStart);
context.RegisterCompilationAction(AnalyzeCompilation);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
private static void AnalyzeCompilation(CompilationAnalysisContext context)
{
var attributedTypes = AttributedRegistration.CollectByCategory(context.Compilation);
var mapBuilder = new DiRegistrationMapBuilder(attributedTypes);

context.RegisterSyntaxNodeAction(
syntaxContext => CollectRegistration(syntaxContext, mapBuilder),
SyntaxKind.InvocationExpression);

context.RegisterCompilationEndAction(
endContext => AnalyzeRegistrations(endContext, mapBuilder));
}

private static void CollectRegistration(
SyntaxNodeAnalysisContext context,
DiRegistrationMapBuilder mapBuilder)
{
var invocation = (InvocationExpressionSyntax)context.Node;

if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
{
return;
}

var methodName = memberAccess.Name.Identifier.ValueText;

if (methodName is "AddSingleton" or "AddScoped" or "AddTransient" or "TryAdd"
or "RegisterType" or "Register" or "RegisterDi")
{
mapBuilder.TryCollect(invocation, context.SemanticModel);
}
}

private static void AnalyzeRegistrations(
CompilationAnalysisContext context,
DiRegistrationMapBuilder mapBuilder)
{
var map = mapBuilder.Build();
var map = DiRegistrationMap.Build(context.Compilation);
if (map.Entries.Count == 0 && map.FactoryDelegates.Count == 0)
{
return;
}

// Build the registration map: type → lifetime (last wins).
var lifetimeMap = new Dictionary<INamedTypeSymbol, Lifetime>(
SymbolEqualityComparer.Default);

foreach (var pair in map.Lifetimes)
{
lifetimeMap[pair.Key] = pair.Value;
}

// DP062: Singleton constructor analysis for all map entries
// (explicit container registrations and attributed RegisterDi).
foreach (var reg in map.Entries)
Expand All @@ -99,13 +54,13 @@ private static void AnalyzeRegistrations(
continue;
}

AnalyzeSingleton(context, reg.ImplementationType, reg.Invocation, lifetimeMap);
AnalyzeSingleton(context, reg.ImplementationType, reg.Invocation, map.Lifetimes);
}

// DP066: Singleton factory delegates collected on the map.
foreach (var factory in map.FactoryDelegates)
{
AnalyzeFactoryDelegate(context, factory, lifetimeMap);
AnalyzeFactoryDelegate(context, factory, map.Lifetimes);
}
}

Expand All @@ -120,7 +75,7 @@ private static void AnalyzeRegistrations(
private static void AnalyzeFactoryDelegate(
CompilationAnalysisContext context,
FactoryDelegateRegistration factory,
Dictionary<INamedTypeSymbol, Lifetime> lifetimeMap)
IReadOnlyDictionary<INamedTypeSymbol, Lifetime> lifetimeMap)
{
var semanticModel = factory.SemanticModel;

Expand Down Expand Up @@ -174,7 +129,7 @@ private static void AnalyzeSingleton(
CompilationAnalysisContext context,
INamedTypeSymbol implType,
InvocationExpressionSyntax invocation,
Dictionary<INamedTypeSymbol, Lifetime> lifetimeMap)
IReadOnlyDictionary<INamedTypeSymbol, Lifetime> lifetimeMap)
{
// Skip if not a class or struct (e.g. interface, delegate)
if (implType.TypeKind is not (TypeKind.Class or TypeKind.Struct))
Expand Down
Loading
Loading