diff --git a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs
index b9ca0e2..d1bef90 100644
--- a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs
+++ b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs
@@ -57,13 +57,6 @@ private sealed class RegistrationEntry
public INamedTypeSymbol ImplementationType { get; set; } = null!;
public Lifetime Lifetime { get; set; }
public InvocationExpressionSyntax Invocation { get; set; } = null!;
-
- ///
- /// True for factory-delegate and instance registrations: the container
- /// never invokes the implementation constructor, so constructor analysis
- /// does not apply (the type still participates in the lifetime map).
- ///
- public bool SkipConstructorAnalysis { get; set; }
}
///
@@ -81,7 +74,8 @@ private sealed class FactoryDelegateEntry
private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
- var registrations = new List();
+ var mapBuilder = new DiRegistrationMapBuilder();
+ var registerDiEntries = new List();
var factoryDelegates = new List();
// Phase 2: Pre-scan all types for DesignPatterns registration attributes.
@@ -90,11 +84,16 @@ private static void OnCompilationStart(CompilationStartAnalysisContext context)
var attributedTypesByCategory = CollectAttributedImplementationTypes(context.Compilation);
context.RegisterSyntaxNodeAction(
- syntaxContext => CollectRegistration(syntaxContext, registrations, factoryDelegates, attributedTypesByCategory),
+ syntaxContext => CollectRegistration(
+ syntaxContext,
+ mapBuilder,
+ registerDiEntries,
+ factoryDelegates,
+ attributedTypesByCategory),
SyntaxKind.InvocationExpression);
context.RegisterCompilationEndAction(
- endContext => AnalyzeRegistrations(endContext, registrations, factoryDelegates));
+ endContext => AnalyzeRegistrations(endContext, mapBuilder, registerDiEntries, factoryDelegates));
}
///
@@ -154,7 +153,8 @@ private static Dictionary> Colle
private static void CollectRegistration(
SyntaxNodeAnalysisContext context,
- List registrations,
+ DiRegistrationMapBuilder mapBuilder,
+ List registerDiEntries,
List factoryDelegates,
Dictionary> attributedTypesByCategory)
{
@@ -167,42 +167,34 @@ private static void CollectRegistration(
var methodName = memberAccess.Name.Identifier.ValueText;
- if (methodName is "AddSingleton" or "AddScoped" or "AddTransient")
- {
- CollectDirectRegistration(context, invocation, methodName, registrations, factoryDelegates);
- }
- else if (methodName == "TryAdd")
+ if (methodName is "AddSingleton" or "AddScoped" or "AddTransient" or "TryAdd" or "RegisterType" or "Register")
{
- CollectTryAddRegistration(context, invocation, registrations);
+ if (mapBuilder.TryCollect(invocation, context.SemanticModel))
+ {
+ TryCollectFactoryDelegate(context, invocation, methodName, factoryDelegates);
+ }
}
else if (methodName == "RegisterDi")
{
- CollectRegisterDiRegistration(context, invocation, attributedTypesByCategory, registrations);
- }
- else if (methodName == "RegisterType")
- {
- CollectAutofacRegisterType(context, invocation, registrations);
- }
- else if (methodName == "Register")
- {
- CollectAutofacRegisterDelegate(context, invocation, registrations, factoryDelegates);
+ CollectRegisterDiRegistration(context, invocation, attributedTypesByCategory, registerDiEntries);
}
}
- private static void CollectDirectRegistration(
+ ///
+ /// Collects Singleton factory delegates for DP066. Lifetime map entries for
+ /// these registrations are owned by .
+ ///
+ private static void TryCollectFactoryDelegate(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocation,
string methodName,
- List registrations,
List factoryDelegates)
{
- var lifetime = methodName switch
+ var args = invocation.ArgumentList?.Arguments ?? default;
+ if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax lambda)
{
- "AddSingleton" => Lifetime.Singleton,
- "AddScoped" => Lifetime.Scoped,
- "AddTransient" => Lifetime.Transient,
- _ => Lifetime.Transient,
- };
+ return;
+ }
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
if (methodSymbol is null)
@@ -210,160 +202,38 @@ private static void CollectDirectRegistration(
return;
}
- INamedTypeSymbol? implType = null;
- var args = invocation.ArgumentList?.Arguments ?? new SeparatedSyntaxList();
-
- // AddSingleton() — 2 type args, TImpl is implementation
- if (methodSymbol.TypeArguments.Length == 2 &&
- methodSymbol.TypeArguments[1] is INamedTypeSymbol implFromGeneric)
+ if (methodName == "AddSingleton")
{
- implType = implFromGeneric;
- }
- // AddSingleton() — 1 type arg, TImpl is implementation
- else if (methodSymbol.TypeArguments.Length == 1 &&
- methodSymbol.TypeArguments[0] is INamedTypeSymbol singleGeneric)
- {
- // Factory/instance registration: the container never calls the
- // constructor, so skip constructor analysis but keep the type in
- // the lifetime map. Singleton factory lambdas are analyzed for DP066.
- if (args.Count > 0 && IsFactoryOrInstanceArg(args[0], context.SemanticModel))
+ if (methodSymbol.TypeArguments.Length != 1 ||
+ methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType ||
+ AutofacRegistration.IsOpenGeneric(serviceType))
{
- if (IsOpenGeneric(singleGeneric))
- {
- return;
- }
-
- registrations.Add(new RegistrationEntry
- {
- ImplementationType = singleGeneric,
- Lifetime = lifetime,
- Invocation = invocation,
- SkipConstructorAnalysis = true,
- });
-
- if (lifetime == Lifetime.Singleton &&
- args[0].Expression is AnonymousFunctionExpressionSyntax lambda)
- {
- factoryDelegates.Add(new FactoryDelegateEntry
- {
- ServiceType = singleGeneric,
- Lambda = lambda,
- SemanticModel = context.SemanticModel,
- });
- }
-
return;
}
- implType = singleGeneric;
- }
-
- if (implType is null)
- {
- return;
- }
-
- if (IsOpenGeneric(implType))
- {
- return;
- }
-
- registrations.Add(new RegistrationEntry
- {
- ImplementationType = implType,
- Lifetime = lifetime,
- Invocation = invocation,
- });
- }
-
- ///
- /// Collects Autofac RegisterType<TImpl>() / RegisterType(typeof(TImpl))
- /// registrations. Autofac methods are matched by containing namespace
- /// (no Autofac package reference). The lifetime is resolved from the
- /// fluent chain (SingleInstance etc.); Autofac defaults to
- /// InstancePerDependency (Transient).
- ///
- private static void CollectAutofacRegisterType(
- SyntaxNodeAnalysisContext context,
- InvocationExpressionSyntax invocation,
- List registrations)
- {
- var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
- if (!IsAutofacMethod(methodSymbol))
- {
+ factoryDelegates.Add(new FactoryDelegateEntry
+ {
+ ServiceType = serviceType,
+ Lambda = lambda,
+ SemanticModel = context.SemanticModel,
+ });
return;
}
- INamedTypeSymbol? implType = null;
- if (methodSymbol!.TypeArguments.Length == 1 &&
- methodSymbol.TypeArguments[0] is INamedTypeSymbol generic)
- {
- implType = generic;
- }
- else
+ if (methodName == "Register" && AutofacRegistration.IsAutofacMethod(methodSymbol))
{
- var args = invocation.ArgumentList?.Arguments ?? new SeparatedSyntaxList();
- if (args.Count == 1)
+ if (methodSymbol.TypeArguments.Length != 1 ||
+ methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType ||
+ AutofacRegistration.IsOpenGeneric(serviceType))
{
- implType = ResolveTypeofArg(args[0].Expression, context.SemanticModel);
+ return;
}
- }
-
- if (implType is null || IsOpenGeneric(implType))
- {
- return;
- }
-
- registrations.Add(new RegistrationEntry
- {
- ImplementationType = implType,
- Lifetime = ResolveAutofacChainLifetime(invocation),
- Invocation = invocation,
- });
- }
-
- ///
- /// Collects Autofac Register(c => ...) delegate registrations.
- /// The container never calls the implementation constructor, so the entry
- /// only feeds the lifetime map; Singleton delegates are analyzed for DP066.
- ///
- private static void CollectAutofacRegisterDelegate(
- SyntaxNodeAnalysisContext context,
- InvocationExpressionSyntax invocation,
- List registrations,
- List factoryDelegates)
- {
- var methodSymbol = context.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
- if (!IsAutofacMethod(methodSymbol))
- {
- return;
- }
-
- if (methodSymbol!.TypeArguments.Length != 1 ||
- methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType ||
- IsOpenGeneric(serviceType))
- {
- return;
- }
-
- var args = invocation.ArgumentList?.Arguments ?? new SeparatedSyntaxList();
- if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax lambda)
- {
- return;
- }
- var lifetime = ResolveAutofacChainLifetime(invocation);
-
- registrations.Add(new RegistrationEntry
- {
- ImplementationType = serviceType,
- Lifetime = lifetime,
- Invocation = invocation,
- SkipConstructorAnalysis = true,
- });
+ if (AutofacRegistration.ResolveChainLifetime(invocation) != Lifetime.Singleton)
+ {
+ return;
+ }
- if (lifetime == Lifetime.Singleton)
- {
factoryDelegates.Add(new FactoryDelegateEntry
{
ServiceType = serviceType,
@@ -373,98 +243,6 @@ methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType ||
}
}
- private static bool IsAutofacMethod(IMethodSymbol? methodSymbol) =>
- methodSymbol?.ContainingNamespace?.ToDisplayString()
- .StartsWith("Autofac", StringComparison.Ordinal) == true;
-
- ///
- /// Walks up the fluent chain from an Autofac registration call and returns
- /// the declared lifetime. Intermediate calls (e.g. As<T>())
- /// are skipped; without an explicit lifetime call Autofac defaults to
- /// InstancePerDependency (Transient).
- ///
- private static Lifetime ResolveAutofacChainLifetime(InvocationExpressionSyntax registrationCall)
- {
- var lifetime = Lifetime.Transient;
- SyntaxNode node = registrationCall;
-
- while (node.Parent is MemberAccessExpressionSyntax memberAccess &&
- memberAccess.Parent is InvocationExpressionSyntax parentInvocation)
- {
- switch (memberAccess.Name.Identifier.ValueText)
- {
- case "SingleInstance":
- lifetime = Lifetime.Singleton;
- break;
- case "InstancePerLifetimeScope":
- case "InstancePerMatchingLifetimeScope":
- case "InstancePerRequest":
- case "InstancePerOwned":
- lifetime = Lifetime.Scoped;
- break;
- case "InstancePerDependency":
- lifetime = Lifetime.Transient;
- break;
- }
-
- node = parentInvocation;
- }
-
- return lifetime;
- }
-
- private static bool IsOpenGeneric(INamedTypeSymbol type) =>
- type.IsUnboundGenericType ||
- (type.TypeParameters.Length > 0 && type.IsDefinition);
-
- private static void CollectTryAddRegistration(
- SyntaxNodeAnalysisContext context,
- InvocationExpressionSyntax invocation,
- List registrations)
- {
- var argList = invocation.ArgumentList;
- if (argList is null || argList.Arguments.Count == 0)
- {
- return;
- }
-
- // TryAdd(new ServiceDescriptor(typeof(TService), typeof(TImpl), ServiceLifetime.Singleton))
- var firstArg = argList.Arguments[0].Expression;
- if (firstArg is not ObjectCreationExpressionSyntax objectCreation)
- {
- return;
- }
-
- var descriptorArgList = objectCreation.ArgumentList;
- if (descriptorArgList is null || descriptorArgList.Arguments.Count < 3)
- {
- return;
- }
-
- var descriptorArgs = descriptorArgList.Arguments;
-
- // Second arg: typeof(TImpl)
- var implType = ResolveTypeofArg(descriptorArgs[1].Expression, context.SemanticModel);
- if (implType is null)
- {
- return;
- }
-
- // Third arg: lifetime constant (e.g. Singleton)
- var lifetime = LifetimeResolution.TryResolve(descriptorArgs[2].Expression, context.SemanticModel);
- if (lifetime is null)
- {
- return;
- }
-
- registrations.Add(new RegistrationEntry
- {
- ImplementationType = implType,
- Lifetime = lifetime.Value,
- Invocation = invocation,
- });
- }
-
///
/// Collects registrations from generated RegisterDi calls.
/// Extracts the implementation lifetime and applies it to the types
@@ -585,25 +363,34 @@ private static void CollectRegisterDiRegistration(
private static void AnalyzeRegistrations(
CompilationAnalysisContext context,
- List registrations,
+ DiRegistrationMapBuilder mapBuilder,
+ List registerDiEntries,
List factoryDelegates)
{
- if (registrations.Count == 0)
+ var map = mapBuilder.Build();
+ if (map.Entries.Count == 0 && registerDiEntries.Count == 0)
{
return;
}
// Build the registration map: type → lifetime (last wins).
+ // Explicit container registrations come from DiRegistrationMap;
+ // attributed RegisterDi entries are overlaid (still private until #234).
var lifetimeMap = new Dictionary(
SymbolEqualityComparer.Default);
- foreach (var reg in registrations)
+ foreach (var pair in map.Lifetimes)
+ {
+ lifetimeMap[pair.Key] = pair.Value;
+ }
+
+ foreach (var reg in registerDiEntries)
{
lifetimeMap[reg.ImplementationType] = reg.Lifetime;
}
- // For each Singleton, check constructor parameters.
- foreach (var reg in registrations)
+ // DP062: Singleton constructor analysis for explicit registrations from the map.
+ foreach (var reg in map.Entries)
{
if (reg.Lifetime != Lifetime.Singleton || reg.SkipConstructorAnalysis)
{
@@ -613,6 +400,17 @@ private static void AnalyzeRegistrations(
AnalyzeSingleton(context, reg.ImplementationType, reg.Invocation, lifetimeMap);
}
+ // DP062: RegisterDi path still uses the private entry list.
+ foreach (var reg in registerDiEntries)
+ {
+ if (reg.Lifetime != Lifetime.Singleton)
+ {
+ continue;
+ }
+
+ AnalyzeSingleton(context, reg.ImplementationType, reg.Invocation, lifetimeMap);
+ }
+
// For each Singleton factory delegate, check resolved services (DP066).
foreach (var factory in factoryDelegates)
{
@@ -726,46 +524,4 @@ private static void AnalyzeSingleton(
}
}
}
-
- private static INamedTypeSymbol? ResolveTypeofArg(
- ExpressionSyntax expr,
- SemanticModel semanticModel)
- {
- if (expr is not TypeOfExpressionSyntax typeofExpr)
- {
- return null;
- }
-
- var typeInfo = semanticModel.GetTypeInfo(typeofExpr.Type);
- return typeInfo.Type as INamedTypeSymbol;
- }
-
- private static bool IsFactoryOrInstanceArg(
- ArgumentSyntax arg,
- SemanticModel semanticModel)
- {
- var expr = arg.Expression;
-
- // Lambda → factory
- if (expr is SimpleLambdaExpressionSyntax or ParenthesizedLambdaExpressionSyntax)
- {
- return true;
- }
-
- // Check if it's a delegate type (Func)
- var typeInfo = semanticModel.GetTypeInfo(expr);
- if (typeInfo.Type is not null &&
- typeInfo.Type.TypeKind == TypeKind.Delegate)
- {
- return true;
- }
-
- // Object creation, method invocation → likely instance
- if (expr is ObjectCreationExpressionSyntax or InvocationExpressionSyntax)
- {
- return true;
- }
-
- return false;
- }
}
diff --git a/DesignPatterns.Analyzers/Di/AutofacRegistration.cs b/DesignPatterns.Analyzers/Di/AutofacRegistration.cs
new file mode 100644
index 0000000..436668e
--- /dev/null
+++ b/DesignPatterns.Analyzers/Di/AutofacRegistration.cs
@@ -0,0 +1,55 @@
+using System;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace DesignPatterns.Analyzers.Di;
+
+///
+/// Helpers for Autofac registration calls matched by namespace (no Autofac package reference).
+///
+internal static class AutofacRegistration
+{
+ internal static bool IsAutofacMethod(IMethodSymbol? methodSymbol) =>
+ methodSymbol?.ContainingNamespace?.ToDisplayString()
+ .StartsWith("Autofac", StringComparison.Ordinal) == true;
+
+ ///
+ /// Walks up the fluent chain from an Autofac registration call and returns
+ /// the declared lifetime. Intermediate calls (e.g. As<T>())
+ /// are skipped; without an explicit lifetime call Autofac defaults to
+ /// InstancePerDependency (Transient).
+ ///
+ internal static Lifetime ResolveChainLifetime(InvocationExpressionSyntax registrationCall)
+ {
+ var lifetime = Lifetime.Transient;
+ SyntaxNode node = registrationCall;
+
+ while (node.Parent is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Parent is InvocationExpressionSyntax parentInvocation)
+ {
+ switch (memberAccess.Name.Identifier.ValueText)
+ {
+ case "SingleInstance":
+ lifetime = Lifetime.Singleton;
+ break;
+ case "InstancePerLifetimeScope":
+ case "InstancePerMatchingLifetimeScope":
+ case "InstancePerRequest":
+ case "InstancePerOwned":
+ lifetime = Lifetime.Scoped;
+ break;
+ case "InstancePerDependency":
+ lifetime = Lifetime.Transient;
+ break;
+ }
+
+ node = parentInvocation;
+ }
+
+ return lifetime;
+ }
+
+ internal static bool IsOpenGeneric(INamedTypeSymbol type) =>
+ type.IsUnboundGenericType ||
+ (type.TypeParameters.Length > 0 && type.IsDefinition);
+}
diff --git a/DesignPatterns.Analyzers/Di/DiRegistration.cs b/DesignPatterns.Analyzers/Di/DiRegistration.cs
new file mode 100644
index 0000000..36366e3
--- /dev/null
+++ b/DesignPatterns.Analyzers/Di/DiRegistration.cs
@@ -0,0 +1,35 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace DesignPatterns.Analyzers.Di;
+
+///
+/// A single explicit container registration contributing to the registration map.
+///
+internal sealed class DiRegistration
+{
+ public DiRegistration(
+ INamedTypeSymbol implementationType,
+ Lifetime lifetime,
+ InvocationExpressionSyntax invocation,
+ bool skipConstructorAnalysis = false)
+ {
+ ImplementationType = implementationType;
+ Lifetime = lifetime;
+ Invocation = invocation;
+ SkipConstructorAnalysis = skipConstructorAnalysis;
+ }
+
+ public INamedTypeSymbol ImplementationType { get; }
+
+ public Lifetime Lifetime { get; }
+
+ public InvocationExpressionSyntax Invocation { get; }
+
+ ///
+ /// True for factory-delegate and instance registrations: the container
+ /// never invokes the implementation constructor, so constructor analysis
+ /// does not apply (the type still participates in the lifetime map).
+ ///
+ public bool SkipConstructorAnalysis { get; }
+}
diff --git a/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs b/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs
new file mode 100644
index 0000000..f213542
--- /dev/null
+++ b/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs
@@ -0,0 +1,305 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace DesignPatterns.Analyzers.Di;
+
+///
+/// Type→lifetime map built from explicit container registrations
+/// (MSDI Add*/TryAdd, Autofac RegisterType/Register).
+///
+internal sealed class DiRegistrationMap
+{
+ private DiRegistrationMap(
+ IReadOnlyList entries,
+ IReadOnlyDictionary lifetimes)
+ {
+ Entries = entries;
+ Lifetimes = lifetimes;
+ }
+
+ public IReadOnlyList Entries { get; }
+
+ ///
+ /// Last registration wins when the same implementation type appears more than once.
+ ///
+ public IReadOnlyDictionary Lifetimes { get; }
+
+ ///
+ /// Walks all invocation expressions in and
+ /// collects explicit container registrations into a map.
+ ///
+ public static DiRegistrationMap Build(Compilation compilation)
+ {
+ var builder = new DiRegistrationMapBuilder();
+ foreach (var tree in compilation.SyntaxTrees)
+ {
+ var model = compilation.GetSemanticModel(tree);
+ foreach (var invocation in tree.GetRoot().DescendantNodes().OfType())
+ {
+ builder.TryCollect(invocation, model);
+ }
+ }
+
+ return builder.Build();
+ }
+
+ internal static DiRegistrationMap FromEntries(IEnumerable entries)
+ {
+ var list = entries.ToImmutableArray();
+ var lifetimes = new Dictionary(SymbolEqualityComparer.Default);
+ foreach (var entry in list)
+ {
+ lifetimes[entry.ImplementationType] = entry.Lifetime;
+ }
+
+ return new DiRegistrationMap(list, lifetimes);
+ }
+}
+
+///
+/// Incrementally collects explicit container registrations for
+/// .
+///
+internal sealed class DiRegistrationMapBuilder
+{
+ private readonly List _entries = new();
+
+ ///
+ /// Attempts to collect an explicit MSDI or Autofac registration from
+ /// . Returns when an
+ /// entry was added. Does not handle attributed RegisterDi expansion.
+ ///
+ public bool TryCollect(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
+ {
+ if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
+ {
+ return false;
+ }
+
+ var methodName = memberAccess.Name.Identifier.ValueText;
+ var before = _entries.Count;
+
+ if (methodName is "AddSingleton" or "AddScoped" or "AddTransient")
+ {
+ CollectDirectRegistration(invocation, methodName, semanticModel);
+ }
+ else if (methodName == "TryAdd")
+ {
+ CollectTryAddRegistration(invocation, semanticModel);
+ }
+ else if (methodName == "RegisterType")
+ {
+ CollectAutofacRegisterType(invocation, semanticModel);
+ }
+ else if (methodName == "Register")
+ {
+ CollectAutofacRegisterDelegate(invocation, semanticModel);
+ }
+
+ return _entries.Count > before;
+ }
+
+ public DiRegistrationMap Build() => DiRegistrationMap.FromEntries(_entries);
+
+ private void CollectDirectRegistration(
+ InvocationExpressionSyntax invocation,
+ string methodName,
+ SemanticModel semanticModel)
+ {
+ var lifetime = methodName switch
+ {
+ "AddSingleton" => Lifetime.Singleton,
+ "AddScoped" => Lifetime.Scoped,
+ "AddTransient" => Lifetime.Transient,
+ _ => Lifetime.Transient,
+ };
+
+ var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
+ if (methodSymbol is null)
+ {
+ return;
+ }
+
+ INamedTypeSymbol? implType = null;
+ var args = invocation.ArgumentList?.Arguments ?? default;
+
+ if (methodSymbol.TypeArguments.Length == 2 &&
+ methodSymbol.TypeArguments[1] is INamedTypeSymbol implFromGeneric)
+ {
+ implType = implFromGeneric;
+ }
+ else if (methodSymbol.TypeArguments.Length == 1 &&
+ methodSymbol.TypeArguments[0] is INamedTypeSymbol singleGeneric)
+ {
+ if (args.Count > 0 && IsFactoryOrInstanceArg(args[0], semanticModel))
+ {
+ if (AutofacRegistration.IsOpenGeneric(singleGeneric))
+ {
+ return;
+ }
+
+ _entries.Add(new DiRegistration(
+ singleGeneric,
+ lifetime,
+ invocation,
+ skipConstructorAnalysis: true));
+ return;
+ }
+
+ implType = singleGeneric;
+ }
+
+ if (implType is null || AutofacRegistration.IsOpenGeneric(implType))
+ {
+ return;
+ }
+
+ _entries.Add(new DiRegistration(implType, lifetime, invocation));
+ }
+
+ private void CollectTryAddRegistration(
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel)
+ {
+ var argList = invocation.ArgumentList;
+ if (argList is null || argList.Arguments.Count == 0)
+ {
+ return;
+ }
+
+ var firstArg = argList.Arguments[0].Expression;
+ if (firstArg is not ObjectCreationExpressionSyntax objectCreation)
+ {
+ return;
+ }
+
+ var descriptorArgList = objectCreation.ArgumentList;
+ if (descriptorArgList is null || descriptorArgList.Arguments.Count < 3)
+ {
+ return;
+ }
+
+ var descriptorArgs = descriptorArgList.Arguments;
+ var implType = ResolveTypeofArg(descriptorArgs[1].Expression, semanticModel);
+ if (implType is null)
+ {
+ return;
+ }
+
+ var lifetime = LifetimeResolution.TryResolve(descriptorArgs[2].Expression, semanticModel);
+ if (lifetime is null)
+ {
+ return;
+ }
+
+ _entries.Add(new DiRegistration(implType, lifetime.Value, invocation));
+ }
+
+ private void CollectAutofacRegisterType(
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel)
+ {
+ var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
+ if (!AutofacRegistration.IsAutofacMethod(methodSymbol))
+ {
+ return;
+ }
+
+ INamedTypeSymbol? implType = null;
+ if (methodSymbol!.TypeArguments.Length == 1 &&
+ methodSymbol.TypeArguments[0] is INamedTypeSymbol generic)
+ {
+ implType = generic;
+ }
+ else
+ {
+ var args = invocation.ArgumentList?.Arguments ?? default;
+ if (args.Count == 1)
+ {
+ implType = ResolveTypeofArg(args[0].Expression, semanticModel);
+ }
+ }
+
+ if (implType is null || AutofacRegistration.IsOpenGeneric(implType))
+ {
+ return;
+ }
+
+ _entries.Add(new DiRegistration(
+ implType,
+ AutofacRegistration.ResolveChainLifetime(invocation),
+ invocation));
+ }
+
+ private void CollectAutofacRegisterDelegate(
+ InvocationExpressionSyntax invocation,
+ SemanticModel semanticModel)
+ {
+ var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
+ if (!AutofacRegistration.IsAutofacMethod(methodSymbol))
+ {
+ return;
+ }
+
+ if (methodSymbol!.TypeArguments.Length != 1 ||
+ methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType ||
+ AutofacRegistration.IsOpenGeneric(serviceType))
+ {
+ return;
+ }
+
+ var args = invocation.ArgumentList?.Arguments ?? default;
+ if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax)
+ {
+ return;
+ }
+
+ _entries.Add(new DiRegistration(
+ serviceType,
+ AutofacRegistration.ResolveChainLifetime(invocation),
+ invocation,
+ skipConstructorAnalysis: true));
+ }
+
+ private static INamedTypeSymbol? ResolveTypeofArg(
+ ExpressionSyntax expr,
+ SemanticModel semanticModel)
+ {
+ if (expr is not TypeOfExpressionSyntax typeofExpr)
+ {
+ return null;
+ }
+
+ var typeInfo = semanticModel.GetTypeInfo(typeofExpr.Type);
+ return typeInfo.Type as INamedTypeSymbol;
+ }
+
+ private static bool IsFactoryOrInstanceArg(
+ ArgumentSyntax arg,
+ SemanticModel semanticModel)
+ {
+ var expr = arg.Expression;
+
+ if (expr is SimpleLambdaExpressionSyntax or ParenthesizedLambdaExpressionSyntax)
+ {
+ return true;
+ }
+
+ var typeInfo = semanticModel.GetTypeInfo(expr);
+ if (typeInfo.Type is not null &&
+ typeInfo.Type.TypeKind == TypeKind.Delegate)
+ {
+ return true;
+ }
+
+ if (expr is ObjectCreationExpressionSyntax or InvocationExpressionSyntax)
+ {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs b/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs
new file mode 100644
index 0000000..e33a5e6
--- /dev/null
+++ b/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs
@@ -0,0 +1,281 @@
+using System.Collections.Generic;
+using System.Linq;
+using DesignPatterns.Analyzers.Di;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace DesignPatterns.Analyzers.Tests;
+
+public sealed class DiRegistrationMapTests
+{
+ [Fact]
+ public void Build_maps_msdi_add_methods_by_lifetime()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ class SingletonService { }
+ class ScopedService { }
+ class TransientService { }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddScoped();
+ services.AddTransient();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Singleton, GetLifetime(map, "SingletonService"));
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "ScopedService"));
+ Assert.Equal(Lifetime.Transient, GetLifetime(map, "TransientService"));
+ }
+
+ [Fact]
+ public void Build_maps_msdi_two_type_args_to_implementation_type()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ interface IService { }
+ class ServiceImpl : IService { }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.AddSingleton();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Singleton, GetLifetime(map, "ServiceImpl"));
+ Assert.DoesNotContain(map.Lifetimes.Keys, t => t.Name == "IService");
+ }
+
+ [Fact]
+ public void Build_maps_tryadd_service_descriptor()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ class MyService { }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.TryAdd(new ServiceDescriptor(typeof(MyService), typeof(MyService), ServiceLifetime.Scoped));
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "MyService"));
+ }
+
+ [Fact]
+ public void Build_maps_factory_registration_with_skip_constructor_analysis()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ class ScopedService { }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.AddScoped(_ => new ScopedService());
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "ScopedService"));
+ var entry = Assert.Single(map.Entries);
+ Assert.True(entry.SkipConstructorAnalysis);
+ }
+
+ [Fact]
+ public void Build_maps_autofac_register_type_fluent_lifetimes()
+ {
+ const string source = """
+ using Autofac;
+
+ class SingletonService { }
+ class ScopedService { }
+ class TransientService { }
+
+ static class Startup
+ {
+ public static void Configure(ContainerBuilder builder)
+ {
+ builder.RegisterType().SingleInstance();
+ builder.RegisterType().InstancePerLifetimeScope();
+ builder.RegisterType();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Singleton, GetLifetime(map, "SingletonService"));
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "ScopedService"));
+ Assert.Equal(Lifetime.Transient, GetLifetime(map, "TransientService"));
+ }
+
+ [Fact]
+ public void Build_maps_autofac_register_type_typeof_argument()
+ {
+ const string source = """
+ using System;
+ using Autofac;
+
+ class ScopedService { }
+
+ static class Startup
+ {
+ public static void Configure(ContainerBuilder builder)
+ {
+ builder.RegisterType(typeof(ScopedService)).InstancePerLifetimeScope();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "ScopedService"));
+ }
+
+ [Fact]
+ public void Build_maps_autofac_register_delegate_with_skip_constructor_analysis()
+ {
+ const string source = """
+ using Autofac;
+
+ class ScopedService { }
+
+ static class Startup
+ {
+ public static void Configure(ContainerBuilder builder)
+ {
+ builder.Register(c => new ScopedService()).InstancePerLifetimeScope();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "ScopedService"));
+ var entry = Assert.Single(map.Entries);
+ Assert.True(entry.SkipConstructorAnalysis);
+ }
+
+ [Fact]
+ public void Build_last_registration_wins_for_same_type()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ class MyService { }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddTransient();
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Transient, GetLifetime(map, "MyService"));
+ Assert.Equal(2, map.Entries.Count);
+ }
+
+ [Fact]
+ public void Build_ignores_register_di_calls()
+ {
+ const string source = """
+ using Microsoft.Extensions.DependencyInjection;
+
+ class MyService { }
+
+ static class StrategyRegistryHolder
+ {
+ public static IServiceCollection RegisterDi(
+ IServiceCollection services,
+ ServiceLifetime implementationLifetime = ServiceLifetime.Singleton,
+ ServiceLifetime registryLifetime = ServiceLifetime.Singleton)
+ => services;
+ }
+
+ static class Startup
+ {
+ public static void Configure(IServiceCollection services)
+ {
+ services.AddScoped();
+ StrategyRegistryHolder.RegisterDi(services);
+ }
+ }
+ """;
+
+ var map = DiRegistrationMap.Build(CreateCompilation(source));
+
+ Assert.Equal(Lifetime.Scoped, GetLifetime(map, "MyService"));
+ Assert.Single(map.Entries);
+ }
+
+ private static Lifetime GetLifetime(DiRegistrationMap map, string typeName)
+ {
+ var type = map.Lifetimes.Keys.Single(t => t.Name == typeName);
+ return map.Lifetimes[type];
+ }
+
+ private static CSharpCompilation CreateCompilation(string source)
+ {
+ var parseOptions = new CSharpParseOptions(LanguageVersion.Latest);
+ var tree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "Test.cs");
+ var references = new List
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(ServiceLifetime).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(ServiceCollection).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Autofac.ContainerBuilder).Assembly.Location),
+ };
+
+ var trustedPlatformAssemblies = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
+ if (!string.IsNullOrEmpty(trustedPlatformAssemblies))
+ {
+ foreach (var assemblyPath in trustedPlatformAssemblies.Split(Path.PathSeparator))
+ {
+ if (assemblyPath.EndsWith("System.Runtime.dll", StringComparison.OrdinalIgnoreCase) ||
+ assemblyPath.EndsWith("netstandard.dll", StringComparison.OrdinalIgnoreCase) ||
+ assemblyPath.EndsWith("System.Collections.dll", StringComparison.OrdinalIgnoreCase) ||
+ assemblyPath.EndsWith($"{Path.DirectorySeparatorChar}System.ComponentModel.dll", StringComparison.OrdinalIgnoreCase))
+ {
+ references.Add(MetadataReference.CreateFromFile(assemblyPath));
+ }
+ }
+ }
+
+ return CSharpCompilation.Create(
+ "DiRegistrationMapTests",
+ new[] { tree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+}