| | | 1 | | using System.Collections.Immutable; |
| | | 2 | | |
| | | 3 | | using Microsoft.CodeAnalysis; |
| | | 4 | | using Microsoft.CodeAnalysis.Diagnostics; |
| | | 5 | | |
| | | 6 | | namespace NexusLabs.Needlr.Analyzers; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Analyzer that detects injectable types in the global namespace that won't be |
| | | 10 | | /// discovered when IncludeNamespacePrefixes is set without an empty string. |
| | | 11 | | /// </summary> |
| | | 12 | | [DiagnosticAnalyzer(LanguageNames.CSharp)] |
| | | 13 | | public sealed class GlobalNamespaceTypeAnalyzer : DiagnosticAnalyzer |
| | | 14 | | { |
| | | 15 | | private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute" |
| | | 16 | | private const string SingletonAttributeName = "NexusLabs.Needlr.SingletonAttribute"; |
| | | 17 | | private const string ScopedAttributeName = "NexusLabs.Needlr.ScopedAttribute"; |
| | | 18 | | private const string TransientAttributeName = "NexusLabs.Needlr.TransientAttribute"; |
| | | 19 | | private const string DoNotInjectAttributeName = "NexusLabs.Needlr.DoNotInjectAttribute"; |
| | | 20 | | private const string DoNotAutoRegisterAttributeName = "NexusLabs.Needlr.DoNotAutoRegisterAttribute"; |
| | | 21 | | |
| | | 22 | | public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => |
| | 187 | 23 | | ImmutableArray.Create(DiagnosticDescriptors.GlobalNamespaceTypeNotDiscovered); |
| | | 24 | | |
| | | 25 | | public override void Initialize(AnalysisContext context) |
| | | 26 | | { |
| | 25 | 27 | | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); |
| | 25 | 28 | | context.EnableConcurrentExecution(); |
| | | 29 | | |
| | 25 | 30 | | context.RegisterCompilationStartAction(compilationContext => |
| | 25 | 31 | | { |
| | 25 | 32 | | // Check if the assembly has [GenerateTypeRegistry] with IncludeNamespacePrefixes set |
| | 14 | 33 | | var (hasAttribute, prefixes, includesEmptyString) = GetGenerateTypeRegistryInfo(compilationContext.Compilati |
| | 25 | 34 | | |
| | 14 | 35 | | if (!hasAttribute) |
| | 1 | 36 | | return; |
| | 25 | 37 | | |
| | 25 | 38 | | // If no prefixes are set, all types are included (no warning needed) |
| | 13 | 39 | | if (prefixes == null || prefixes.Length == 0) |
| | 1 | 40 | | return; |
| | 25 | 41 | | |
| | 25 | 42 | | // If empty string is in prefixes, global namespace types are included |
| | 12 | 43 | | if (includesEmptyString) |
| | 1 | 44 | | return; |
| | 25 | 45 | | |
| | 25 | 46 | | // Register to check all named types |
| | 11 | 47 | | compilationContext.RegisterSymbolAction(symbolContext => |
| | 11 | 48 | | { |
| | 92 | 49 | | var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol; |
| | 11 | 50 | | |
| | 11 | 51 | | // Only check types in the global namespace |
| | 92 | 52 | | if (typeSymbol.ContainingNamespace?.IsGlobalNamespace != true) |
| | 80 | 53 | | return; |
| | 11 | 54 | | |
| | 11 | 55 | | // Skip types that are excluded from injection |
| | 12 | 56 | | if (HasAttribute(typeSymbol, DoNotInjectAttributeName) || |
| | 12 | 57 | | HasAttribute(typeSymbol, DoNotAutoRegisterAttributeName)) |
| | 2 | 58 | | return; |
| | 11 | 59 | | |
| | 11 | 60 | | // Check if this type looks injectable |
| | 10 | 61 | | if (!IsLikelyInjectableType(typeSymbol)) |
| | 4 | 62 | | return; |
| | 11 | 63 | | |
| | 11 | 64 | | // Report diagnostic |
| | 6 | 65 | | var diagnostic = Diagnostic.Create( |
| | 6 | 66 | | DiagnosticDescriptors.GlobalNamespaceTypeNotDiscovered, |
| | 6 | 67 | | typeSymbol.Locations.FirstOrDefault(), |
| | 6 | 68 | | typeSymbol.Name); |
| | 11 | 69 | | |
| | 6 | 70 | | symbolContext.ReportDiagnostic(diagnostic); |
| | 11 | 71 | | |
| | 17 | 72 | | }, SymbolKind.NamedType); |
| | 36 | 73 | | }); |
| | 25 | 74 | | } |
| | | 75 | | |
| | | 76 | | private static (bool hasAttribute, string[]? prefixes, bool includesEmptyString) GetGenerateTypeRegistryInfo(IAssemb |
| | | 77 | | { |
| | 41 | 78 | | foreach (var attribute in assembly.GetAttributes()) |
| | | 79 | | { |
| | 13 | 80 | | var attrClass = attribute.AttributeClass; |
| | 13 | 81 | | if (attrClass?.ToDisplayString() != GenerateTypeRegistryAttributeName) |
| | | 82 | | continue; |
| | | 83 | | |
| | 13 | 84 | | string[]? prefixes = null; |
| | 13 | 85 | | var includesEmptyString = false; |
| | | 86 | | |
| | 50 | 87 | | foreach (var namedArg in attribute.NamedArguments) |
| | | 88 | | { |
| | 12 | 89 | | if (namedArg.Key == "IncludeNamespacePrefixes" && |
| | 12 | 90 | | !namedArg.Value.IsNull && |
| | 12 | 91 | | namedArg.Value.Values.Length > 0) |
| | | 92 | | { |
| | 12 | 93 | | prefixes = namedArg.Value.Values |
| | 13 | 94 | | .Where(v => v.Value is string) |
| | 13 | 95 | | .Select(v => (string)v.Value!) |
| | 12 | 96 | | .ToArray(); |
| | | 97 | | |
| | 25 | 98 | | includesEmptyString = prefixes.Any(p => string.IsNullOrEmpty(p)); |
| | | 99 | | } |
| | | 100 | | } |
| | | 101 | | |
| | 13 | 102 | | return (true, prefixes, includesEmptyString); |
| | | 103 | | } |
| | | 104 | | |
| | 1 | 105 | | return (false, null, false); |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | private static bool HasAttribute(INamedTypeSymbol typeSymbol, string attributeName) |
| | | 109 | | { |
| | 88 | 110 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 111 | | { |
| | 9 | 112 | | if (attribute.AttributeClass?.ToDisplayString() == attributeName) |
| | 4 | 113 | | return true; |
| | | 114 | | } |
| | 33 | 115 | | return false; |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | private static bool IsLikelyInjectableType(INamedTypeSymbol typeSymbol) |
| | | 119 | | { |
| | | 120 | | // Skip abstract types, interfaces, and static classes |
| | 10 | 121 | | if (typeSymbol.IsAbstract || typeSymbol.TypeKind == TypeKind.Interface || typeSymbol.IsStatic) |
| | 4 | 122 | | return false; |
| | | 123 | | |
| | | 124 | | // Skip generic type definitions (open generics) |
| | 6 | 125 | | if (typeSymbol.IsGenericType && typeSymbol.TypeParameters.Length > 0 && typeSymbol.TypeArguments.Length == 0) |
| | 0 | 126 | | return false; |
| | | 127 | | |
| | | 128 | | // Check if it has lifetime attributes |
| | 6 | 129 | | if (HasAttribute(typeSymbol, SingletonAttributeName) || |
| | 6 | 130 | | HasAttribute(typeSymbol, ScopedAttributeName) || |
| | 6 | 131 | | HasAttribute(typeSymbol, TransientAttributeName)) |
| | 2 | 132 | | return true; |
| | | 133 | | |
| | | 134 | | // Check if it implements any interfaces (common for DI services) |
| | 4 | 135 | | if (typeSymbol.AllInterfaces.Length > 0) |
| | 2 | 136 | | return true; |
| | | 137 | | |
| | | 138 | | // Check if it has a constructor with interface/class parameters (likely DI dependencies) |
| | 6 | 139 | | foreach (var constructor in typeSymbol.Constructors) |
| | | 140 | | { |
| | 2 | 141 | | if (constructor.IsStatic) |
| | | 142 | | continue; |
| | | 143 | | |
| | 6 | 144 | | foreach (var parameter in constructor.Parameters) |
| | | 145 | | { |
| | 2 | 146 | | var paramType = parameter.Type; |
| | 2 | 147 | | if (paramType.TypeKind == TypeKind.Interface || |
| | 2 | 148 | | (paramType.TypeKind == TypeKind.Class && paramType.SpecialType == SpecialType.None)) |
| | | 149 | | { |
| | 2 | 150 | | return true; |
| | | 151 | | } |
| | | 152 | | } |
| | | 153 | | } |
| | | 154 | | |
| | 0 | 155 | | return false; |
| | | 156 | | } |
| | | 157 | | } |