diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e810cf..0c13ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTEXT.md b/CONTEXT.md index e33933c..0f3ca7e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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) diff --git a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs index dab962f..b331ab2 100644 --- a/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs +++ b/DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs @@ -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; @@ -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( - 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) @@ -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); } } @@ -120,7 +75,7 @@ private static void AnalyzeRegistrations( private static void AnalyzeFactoryDelegate( CompilationAnalysisContext context, FactoryDelegateRegistration factory, - Dictionary lifetimeMap) + IReadOnlyDictionary lifetimeMap) { var semanticModel = factory.SemanticModel; @@ -174,7 +129,7 @@ private static void AnalyzeSingleton( CompilationAnalysisContext context, INamedTypeSymbol implType, InvocationExpressionSyntax invocation, - Dictionary lifetimeMap) + IReadOnlyDictionary lifetimeMap) { // Skip if not a class or struct (e.g. interface, delegate) if (implType.TypeKind is not (TypeKind.Class or TypeKind.Struct)) diff --git a/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs b/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs index 2cd67e6..c1e96e4 100644 --- a/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs +++ b/DesignPatterns.Analyzers/Di/DiRegistrationMap.cs @@ -1,17 +1,27 @@ +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace DesignPatterns.Analyzers.Di; /// -/// Type→lifetime map built from explicit container registrations -/// (MSDI Add*/TryAdd, Autofac RegisterType/Register) and attributed -/// RegisterDi expansions (holder-category matching), plus Singleton -/// factory-delegate registrations for DP066. +/// DI registration map: type→lifetime view built from explicit container +/// registrations (MSDI Add*/TryAdd, Autofac RegisterType/Register) and +/// attributed RegisterDi expansions (holder-category matching), plus +/// Singleton factory-delegate registrations for DP066. /// +/// +/// Consumers obtain a map only via . Incremental collection +/// is a private implementation detail of that entry point. +/// skips generated syntax trees so callers that configure +/// GeneratedCodeAnalysisFlags.None keep the same registration visibility +/// they had with RegisterSyntaxNodeAction. +/// internal sealed class DiRegistrationMap { private DiRegistrationMap( @@ -40,16 +50,21 @@ private DiRegistrationMap( public IReadOnlyList FactoryDelegates { get; } /// - /// Walks all invocation expressions in and - /// collects explicit container registrations plus attributed - /// RegisterDi expansions into a map. + /// Walks invocation expressions in non-generated syntax trees of + /// and collects explicit container + /// registrations plus attributed RegisterDi expansions into a map. /// public static DiRegistrationMap Build(Compilation compilation) { var attributedTypes = AttributedRegistration.CollectByCategory(compilation); - var builder = new DiRegistrationMapBuilder(attributedTypes); + var builder = new Builder(attributedTypes); foreach (var tree in compilation.SyntaxTrees) { + if (IsGeneratedSyntaxTree(tree)) + { + continue; + } + var model = compilation.GetSemanticModel(tree); foreach (var invocation in tree.GetRoot().DescendantNodes().OfType()) { @@ -60,385 +75,450 @@ public static DiRegistrationMap Build(Compilation compilation) return builder.Build(); } - internal static DiRegistrationMap FromEntries( - IEnumerable entries, - IEnumerable? factoryDelegates = null) + /// + /// Mirrors the generated-code heuristics Roslyn applies when analyzers use + /// GeneratedCodeAnalysisFlags.None with syntax-node actions: generated + /// file-name suffixes and leading auto-generated comments. + /// + private static bool IsGeneratedSyntaxTree(SyntaxTree tree) { - var list = entries.ToImmutableArray(); - var lifetimes = new Dictionary(SymbolEqualityComparer.Default); - foreach (var entry in list) + if (IsGeneratedFilePath(tree.FilePath)) { - lifetimes[entry.ImplementationType] = entry.Lifetime; + return true; } - var delegates = factoryDelegates?.ToImmutableArray() - ?? ImmutableArray.Empty; - return new DiRegistrationMap(list, lifetimes, delegates); - } -} - -/// -/// Incrementally collects explicit container registrations and attributed -/// RegisterDi expansions for . -/// -internal sealed class DiRegistrationMapBuilder -{ - private readonly List _explicitEntries = new(); - private readonly List _attributedEntries = new(); - private readonly List _factoryDelegates = new(); - private readonly IReadOnlyDictionary> _attributedTypesByCategory; + var root = tree.GetRoot(); + if (root.HasLeadingTrivia && BeginsWithAutoGeneratedComment(root.GetLeadingTrivia())) + { + return true; + } - public DiRegistrationMapBuilder( - IReadOnlyDictionary> attributedTypesByCategory) - { - _attributedTypesByCategory = attributedTypesByCategory; + // File-scoped / first-token trivia when the root itself has none. + var firstToken = root.GetFirstToken(includeZeroWidth: true); + return firstToken != default + && BeginsWithAutoGeneratedComment(firstToken.LeadingTrivia); } - /// - /// Attempts to collect an explicit MSDI/Autofac registration or an - /// attributed RegisterDi expansion from . - /// Returns when one or more entries were added. - /// - public bool TryCollect(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + private static bool IsGeneratedFilePath(string? filePath) { - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + if (string.IsNullOrEmpty(filePath)) { return false; } - var methodName = memberAccess.Name.Identifier.ValueText; - var beforeExplicit = _explicitEntries.Count; - var beforeAttributed = _attributedEntries.Count; - var beforeFactoryDelegates = _factoryDelegates.Count; - - if (methodName is "AddSingleton" or "AddScoped" or "AddTransient") + var fileName = Path.GetFileName(filePath); + if (fileName.StartsWith("TemporaryGeneratedFile_", StringComparison.OrdinalIgnoreCase)) { - CollectDirectRegistration(invocation, methodName, semanticModel); - } - else if (methodName == "TryAdd") - { - CollectTryAddRegistration(invocation, semanticModel); - } - else if (methodName == "RegisterType") - { - CollectAutofacRegisterType(invocation, semanticModel); + return true; } - else if (methodName == "Register") + + return fileName.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase); + } + + private static bool BeginsWithAutoGeneratedComment(SyntaxTriviaList triviaList) + { + foreach (var trivia in triviaList) { - CollectAutofacRegisterDelegate(invocation, semanticModel); + if (trivia.Kind() is SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia) + { + continue; + } + + if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) + || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia)) + { + var text = trivia.ToString(); + return text.IndexOf("= 0 + || text.IndexOf("= 0; + } + + // First non-whitespace trivia is not an auto-generated comment. + return false; } - else if (methodName == "RegisterDi") + + return false; + } + + private static DiRegistrationMap FromEntries( + IEnumerable entries, + IEnumerable? factoryDelegates = null) + { + var list = entries.ToImmutableArray(); + var lifetimes = new Dictionary(SymbolEqualityComparer.Default); + foreach (var entry in list) { - CollectRegisterDiRegistration(invocation, semanticModel); + lifetimes[entry.ImplementationType] = entry.Lifetime; } - return _explicitEntries.Count > beforeExplicit - || _attributedEntries.Count > beforeAttributed - || _factoryDelegates.Count > beforeFactoryDelegates; + var delegates = factoryDelegates?.ToImmutableArray() + ?? ImmutableArray.Empty; + return new DiRegistrationMap(list, lifetimes, delegates); } /// - /// Builds the map with attributed RegisterDi entries after explicit - /// ones so RegisterDi lifetimes overlay explicit registrations for the same - /// type (behaviour freeze vs the pre-extraction Captive Dependency path). - /// - public DiRegistrationMap Build() => - DiRegistrationMap.FromEntries( - _explicitEntries.Concat(_attributedEntries), - _factoryDelegates); - - /// - /// Collects registrations from generated RegisterDi calls. - /// Extracts the implementation lifetime and applies it to the types - /// bearing the DesignPatterns registration attribute that matches - /// the RegisterDi holder's pattern (Strategy/Factory/EventHandler/Decorator/Composite). + /// Private collector used only by . /// - private void CollectRegisterDiRegistration( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel) + private sealed class Builder { - var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; - if (methodSymbol is null || !methodSymbol.IsStatic || methodSymbol.Parameters.Length == 0) - { - return; - } + private readonly List _explicitEntries = new(); + private readonly List _attributedEntries = new(); + private readonly List _factoryDelegates = new(); + private readonly IReadOnlyDictionary> _attributedTypesByCategory; - // The first parameter must be IServiceCollection. - var firstParamType = methodSymbol.Parameters[0].Type; - if (firstParamType.ToDisplayString() != "Microsoft.Extensions.DependencyInjection.IServiceCollection") + public Builder( + IReadOnlyDictionary> attributedTypesByCategory) { - return; + _attributedTypesByCategory = attributedTypesByCategory; } - // Find the implementation lifetime parameter by name. - // Two-lifetime overload: implementationLifetime + registryLifetime - // Single-lifetime overload: lifetime (StateTransition, Composite, Decorator, EventHandler) - var implLifetimeParam = methodSymbol.Parameters.FirstOrDefault( - p => p.Name == "implementationLifetime"); - if (implLifetimeParam is null) + /// + /// Attempts to collect an explicit MSDI/Autofac registration or an + /// attributed RegisterDi expansion from . + /// Returns when one or more entries were added. + /// + public bool TryCollect(InvocationExpressionSyntax invocation, SemanticModel semanticModel) { - implLifetimeParam = methodSymbol.Parameters.FirstOrDefault( - p => p.Name == "lifetime"); - } + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + return false; + } - if (implLifetimeParam is null) - { - return; - } + var methodName = memberAccess.Name.Identifier.ValueText; + var beforeExplicit = _explicitEntries.Count; + var beforeAttributed = _attributedEntries.Count; + var beforeFactoryDelegates = _factoryDelegates.Count; - // Resolve the lifetime value from the call arguments. - var lifetime = LifetimeResolution.TryResolveArgument( - invocation, implLifetimeParam, semanticModel); - var containingTypeName = methodSymbol.ContainingType?.Name ?? ""; - if (lifetime is null) - { - // Use default: Factory → Transient, others → Singleton. - lifetime = AttributedRegistration.DefaultLifetimeForHolder(containingTypeName); - } + 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); + } + else if (methodName == "RegisterDi") + { + CollectRegisterDiRegistration(invocation, semanticModel); + } - // Match the RegisterDi holder type name to the correct attribute category. - // This prevents cross-pattern contamination (e.g. a Strategy RegisterDi call - // should not apply its lifetime to Factory implementation types). - var category = AttributedRegistration.MatchCategoryByHolderName(containingTypeName); - if (category is null) - { - return; - } + return _explicitEntries.Count > beforeExplicit + || _attributedEntries.Count > beforeAttributed + || _factoryDelegates.Count > beforeFactoryDelegates; + } + + /// + /// Builds the map with attributed RegisterDi entries after explicit + /// ones so RegisterDi lifetimes overlay explicit registrations for the same + /// type (behaviour freeze vs the pre-extraction Captive Dependency path). + /// + public DiRegistrationMap Build() => + FromEntries( + _explicitEntries.Concat(_attributedEntries), + _factoryDelegates); + + /// + /// Collects registrations from generated RegisterDi calls. + /// Extracts the implementation lifetime and applies it to the types + /// bearing the DesignPatterns registration attribute that matches + /// the RegisterDi holder's pattern (Strategy/Factory/EventHandler/Decorator/Composite). + /// + private void CollectRegisterDiRegistration( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel) + { + var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (methodSymbol is null || !methodSymbol.IsStatic || methodSymbol.Parameters.Length == 0) + { + return; + } - if (!_attributedTypesByCategory.TryGetValue(category.Value, out var typesForCategory)) - { - return; - } + // The first parameter must be IServiceCollection. + var firstParamType = methodSymbol.Parameters[0].Type; + if (firstParamType.ToDisplayString() != "Microsoft.Extensions.DependencyInjection.IServiceCollection") + { + return; + } - // Add only the implementation types of the matched category. - foreach (var implType in typesForCategory) - { - if (AutofacRegistration.IsOpenGeneric(implType)) + // Find the implementation lifetime parameter by name. + // Two-lifetime overload: implementationLifetime + registryLifetime + // Single-lifetime overload: lifetime (StateTransition, Composite, Decorator, EventHandler) + var implLifetimeParam = methodSymbol.Parameters.FirstOrDefault( + p => p.Name == "implementationLifetime"); + if (implLifetimeParam is null) { - continue; + implLifetimeParam = methodSymbol.Parameters.FirstOrDefault( + p => p.Name == "lifetime"); } - _attributedEntries.Add(new DiRegistration(implType, lifetime.Value, invocation)); - } - } + if (implLifetimeParam is null) + { + return; + } - 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; - } + // Resolve the lifetime value from the call arguments. + var lifetime = LifetimeResolution.TryResolveArgument( + invocation, implLifetimeParam, semanticModel); + var containingTypeName = methodSymbol.ContainingType?.Name ?? ""; + if (lifetime is null) + { + // Use default: Factory → Transient, others → Singleton. + lifetime = AttributedRegistration.DefaultLifetimeForHolder(containingTypeName); + } - INamedTypeSymbol? implType = null; - var args = invocation.ArgumentList?.Arguments ?? default; + // Match the RegisterDi holder type name to the correct attribute category. + // This prevents cross-pattern contamination (e.g. a Strategy RegisterDi call + // should not apply its lifetime to Factory implementation types). + var category = AttributedRegistration.MatchCategoryByHolderName(containingTypeName); + if (category is null) + { + return; + } - 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 (!_attributedTypesByCategory.TryGetValue(category.Value, out var typesForCategory)) + { + return; + } + + // Add only the implementation types of the matched category. + foreach (var implType in typesForCategory) { - if (AutofacRegistration.IsOpenGeneric(singleGeneric)) + if (AutofacRegistration.IsOpenGeneric(implType)) { - return; + continue; } - _explicitEntries.Add(new DiRegistration( - singleGeneric, - lifetime, - invocation, - skipConstructorAnalysis: true)); + _attributedEntries.Add(new DiRegistration(implType, lifetime.Value, invocation)); + } + } - // DP066 only analyzes Singleton factory lambdas (not instance args). - if (lifetime == Lifetime.Singleton && - args[0].Expression is AnonymousFunctionExpressionSyntax lambda) + 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)) { - _factoryDelegates.Add(new FactoryDelegateRegistration( + if (AutofacRegistration.IsOpenGeneric(singleGeneric)) + { + return; + } + + _explicitEntries.Add(new DiRegistration( singleGeneric, - lambda, - semanticModel)); + lifetime, + invocation, + skipConstructorAnalysis: true)); + + // DP066 only analyzes Singleton factory lambdas (not instance args). + if (lifetime == Lifetime.Singleton && + args[0].Expression is AnonymousFunctionExpressionSyntax lambda) + { + _factoryDelegates.Add(new FactoryDelegateRegistration( + singleGeneric, + lambda, + semanticModel)); + } + + return; } + implType = singleGeneric; + } + + if (implType is null || AutofacRegistration.IsOpenGeneric(implType)) + { return; } - implType = singleGeneric; + _explicitEntries.Add(new DiRegistration(implType, lifetime, invocation)); } - if (implType is null || AutofacRegistration.IsOpenGeneric(implType)) + private void CollectTryAddRegistration( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel) { - return; - } + var argList = invocation.ArgumentList; + if (argList is null || argList.Arguments.Count == 0) + { + return; + } - _explicitEntries.Add(new DiRegistration(implType, lifetime, invocation)); - } + var firstArg = argList.Arguments[0].Expression; + if (firstArg is not ObjectCreationExpressionSyntax objectCreation) + { + return; + } - private void CollectTryAddRegistration( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel) - { - var argList = invocation.ArgumentList; - if (argList is null || argList.Arguments.Count == 0) - { - return; - } + var descriptorArgList = objectCreation.ArgumentList; + if (descriptorArgList is null || descriptorArgList.Arguments.Count < 3) + { + return; + } - var firstArg = argList.Arguments[0].Expression; - if (firstArg is not ObjectCreationExpressionSyntax objectCreation) - { - return; - } + var descriptorArgs = descriptorArgList.Arguments; + var implType = ResolveTypeofArg(descriptorArgs[1].Expression, semanticModel); + if (implType is null) + { + return; + } - var descriptorArgList = objectCreation.ArgumentList; - if (descriptorArgList is null || descriptorArgList.Arguments.Count < 3) - { - return; - } + var lifetime = LifetimeResolution.TryResolve(descriptorArgs[2].Expression, semanticModel); + if (lifetime is null) + { + return; + } - var descriptorArgs = descriptorArgList.Arguments; - var implType = ResolveTypeofArg(descriptorArgs[1].Expression, semanticModel); - if (implType is null) - { - return; + _explicitEntries.Add(new DiRegistration(implType, lifetime.Value, invocation)); } - var lifetime = LifetimeResolution.TryResolve(descriptorArgs[2].Expression, semanticModel); - if (lifetime is null) + private void CollectAutofacRegisterType( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel) { - return; - } + var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (!AutofacRegistration.IsAutofacMethod(methodSymbol)) + { + return; + } - _explicitEntries.Add(new DiRegistration(implType, lifetime.Value, invocation)); - } + 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); + } + } - private void CollectAutofacRegisterType( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel) - { - var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; - if (!AutofacRegistration.IsAutofacMethod(methodSymbol)) - { - return; - } + if (implType is null || AutofacRegistration.IsOpenGeneric(implType)) + { + return; + } - INamedTypeSymbol? implType = null; - if (methodSymbol!.TypeArguments.Length == 1 && - methodSymbol.TypeArguments[0] is INamedTypeSymbol generic) - { - implType = generic; + _explicitEntries.Add(new DiRegistration( + implType, + AutofacRegistration.ResolveChainLifetime(invocation), + invocation)); } - else + + private void CollectAutofacRegisterDelegate( + InvocationExpressionSyntax invocation, + SemanticModel semanticModel) { - var args = invocation.ArgumentList?.Arguments ?? default; - if (args.Count == 1) + var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (!AutofacRegistration.IsAutofacMethod(methodSymbol)) { - implType = ResolveTypeofArg(args[0].Expression, semanticModel); + return; } - } - if (implType is null || AutofacRegistration.IsOpenGeneric(implType)) - { - return; - } + if (methodSymbol!.TypeArguments.Length != 1 || + methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType || + AutofacRegistration.IsOpenGeneric(serviceType)) + { + return; + } - _explicitEntries.Add(new DiRegistration( - implType, - AutofacRegistration.ResolveChainLifetime(invocation), - invocation)); - } + var args = invocation.ArgumentList?.Arguments ?? default; + if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax lambda) + { + return; + } - private void CollectAutofacRegisterDelegate( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel) - { - var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; - if (!AutofacRegistration.IsAutofacMethod(methodSymbol)) - { - return; - } + var lifetime = AutofacRegistration.ResolveChainLifetime(invocation); + _explicitEntries.Add(new DiRegistration( + serviceType, + lifetime, + invocation, + skipConstructorAnalysis: true)); - if (methodSymbol!.TypeArguments.Length != 1 || - methodSymbol.TypeArguments[0] is not INamedTypeSymbol serviceType || - AutofacRegistration.IsOpenGeneric(serviceType)) - { - return; + // DP066 only analyzes Singleton factory delegates. + if (lifetime == Lifetime.Singleton) + { + _factoryDelegates.Add(new FactoryDelegateRegistration( + serviceType, + lambda, + semanticModel)); + } } - var args = invocation.ArgumentList?.Arguments ?? default; - if (args.Count == 0 || args[0].Expression is not AnonymousFunctionExpressionSyntax lambda) + private static INamedTypeSymbol? ResolveTypeofArg( + ExpressionSyntax expr, + SemanticModel semanticModel) { - return; - } - - var lifetime = AutofacRegistration.ResolveChainLifetime(invocation); - _explicitEntries.Add(new DiRegistration( - serviceType, - lifetime, - invocation, - skipConstructorAnalysis: true)); + if (expr is not TypeOfExpressionSyntax typeofExpr) + { + return null; + } - // DP066 only analyzes Singleton factory delegates. - if (lifetime == Lifetime.Singleton) - { - _factoryDelegates.Add(new FactoryDelegateRegistration( - serviceType, - lambda, - semanticModel)); + var typeInfo = semanticModel.GetTypeInfo(typeofExpr.Type); + return typeInfo.Type as INamedTypeSymbol; } - } - private static INamedTypeSymbol? ResolveTypeofArg( - ExpressionSyntax expr, - SemanticModel semanticModel) - { - if (expr is not TypeOfExpressionSyntax typeofExpr) + private static bool IsFactoryOrInstanceArg( + ArgumentSyntax arg, + SemanticModel semanticModel) { - return null; - } - - var typeInfo = semanticModel.GetTypeInfo(typeofExpr.Type); - return typeInfo.Type as INamedTypeSymbol; - } + var expr = arg.Expression; - private static bool IsFactoryOrInstanceArg( - ArgumentSyntax arg, - SemanticModel semanticModel) - { - var expr = arg.Expression; + if (expr is SimpleLambdaExpressionSyntax or ParenthesizedLambdaExpressionSyntax) + { + return true; + } - 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; + } - 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; + } - if (expr is ObjectCreationExpressionSyntax or InvocationExpressionSyntax) - { - return true; + return false; } - - return false; } } diff --git a/DesignPatterns.Analyzers/SingletonLifecycleAnalyzer.cs b/DesignPatterns.Analyzers/SingletonLifecycleAnalyzer.cs index 21079ce..57dc39f 100644 --- a/DesignPatterns.Analyzers/SingletonLifecycleAnalyzer.cs +++ b/DesignPatterns.Analyzers/SingletonLifecycleAnalyzer.cs @@ -5,8 +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; namespace DesignPatterns.Analyzers; @@ -35,47 +33,12 @@ 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 => Analyze(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 Analyze( - CompilationAnalysisContext context, - DiRegistrationMapBuilder mapBuilder) - { - var map = mapBuilder.Build(); + var map = DiRegistrationMap.Build(context.Compilation); var singletonRegistrations = new HashSet(SymbolEqualityComparer.Default); foreach (var pair in map.Lifetimes) { diff --git a/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs b/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs index ddff817..568e968 100644 --- a/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs +++ b/tests/DesignPatterns.Analyzers.Tests/DiRegistrationMapTests.cs @@ -559,16 +559,100 @@ public static void Configure(IServiceCollection services) Assert.Equal(2, map.Entries.Count); } + [Fact] + public void Build_skips_registrations_in_generated_file_path_trees() + { + const string userSource = """ + using Microsoft.Extensions.DependencyInjection; + + class UserScopedService { } + + static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddScoped(); + } + } + """; + + const string generatedSource = """ + using Microsoft.Extensions.DependencyInjection; + + class GeneratedSingletonService { } + + static class GeneratedStartup + { + public static void Configure(IServiceCollection services) + { + services.AddSingleton(); + } + } + """; + + var map = DiRegistrationMap.Build(CreateCompilation( + ("Startup.cs", userSource), + ("Registry.g.cs", generatedSource))); + + Assert.Equal(Lifetime.Scoped, GetLifetime(map, "UserScopedService")); + Assert.DoesNotContain(map.Lifetimes.Keys, t => t.Name == "GeneratedSingletonService"); + } + + [Fact] + public void Build_skips_registrations_in_trees_with_auto_generated_header() + { + const string userSource = """ + using Microsoft.Extensions.DependencyInjection; + + class UserTransientService { } + + static class Startup + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + } + } + """; + + const string generatedSource = """ + // + using Microsoft.Extensions.DependencyInjection; + + class HeaderMarkedSingleton { } + + static class HeaderMarkedStartup + { + public static void Configure(IServiceCollection services) + { + services.AddSingleton(); + } + } + """; + + var map = DiRegistrationMap.Build(CreateCompilation( + ("Startup.cs", userSource), + ("Something.cs", generatedSource))); + + Assert.Equal(Lifetime.Transient, GetLifetime(map, "UserTransientService")); + Assert.DoesNotContain(map.Lifetimes.Keys, t => t.Name == "HeaderMarkedSingleton"); + } + 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) + private static CSharpCompilation CreateCompilation(string source) => + CreateCompilation(("Test.cs", source)); + + private static CSharpCompilation CreateCompilation(params (string Path, string Source)[] sources) { var parseOptions = new CSharpParseOptions(LanguageVersion.Latest); - var tree = CSharpSyntaxTree.ParseText(source, parseOptions, path: "Test.cs"); + var trees = sources + .Select(s => CSharpSyntaxTree.ParseText(s.Source, parseOptions, path: s.Path)) + .ToArray(); var references = new List { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), @@ -595,7 +679,7 @@ private static CSharpCompilation CreateCompilation(string source) return CSharpCompilation.Create( "DiRegistrationMapTests", - new[] { tree }, + trees, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); }