< Summary

Information
Class: NexusLabs.Needlr.Generators.GeneratedConstructorAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Generators/GeneratedConstructorAnalyzer.cs
Line coverage
94%
Covered lines: 264
Uncovered lines: 14
Coverable lines: 278
Total lines: 440
Line coverage: 94.9%
Branch coverage
94%
Covered branches: 91
Total branches: 96
Branch coverage: 94.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_SupportedDiagnostics()100%11100%
Initialize(...)100%11100%
AnalyzeTypeDeclaration(...)83.33%121293.1%
AnalyzeTypeShape(...)100%2020100%
AnalyzeGenerateConstructorEnumArgument(...)100%88100%
CollectFieldGuardOccurrences(...)100%1616100%
ReportFieldOccurrenceDiagnostics(...)100%1212100%
AnalyzeGuardDefinitionTarget(...)89.28%322882.6%

File(s)

/_work/nick-alienware-needlr-1-1784953859-3e8997/needlr/needlr/src/NexusLabs.Needlr.Generators/GeneratedConstructorAnalyzer.cs

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Collections.Immutable;
 3using System.Linq;
 4
 5using Microsoft.CodeAnalysis;
 6using Microsoft.CodeAnalysis.CSharp;
 7using Microsoft.CodeAnalysis.CSharp.Syntax;
 8using Microsoft.CodeAnalysis.Diagnostics;
 9
 10using NexusLabs.Needlr.Generators.Models;
 11using NexusLabs.Needlr.Roslyn.Shared;
 12
 13namespace NexusLabs.Needlr.Generators;
 14
 15/// <summary>
 16/// Validates generated-constructor type shapes, field participation, built-in and
 17/// custom guards, and custom guard alias definitions.
 18/// </summary>
 19[DiagnosticAnalyzer(LanguageNames.CSharp)]
 20public sealed class GeneratedConstructorAnalyzer : DiagnosticAnalyzer
 21{
 22    private const string GenerateConstructorAttributeName = "GenerateConstructorAttribute";
 23    private const string ConstructorGuardAttributeName = "ConstructorGuardAttribute";
 24    private const string ConstructorIgnoreAttributeName = "ConstructorIgnoreAttribute";
 25    private const string ConstructorGuardDefinitionAttributeName = "ConstructorGuardDefinitionAttribute";
 26    private const string DefaultGuardMethodName = "Validate";
 27
 28    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 249829        ImmutableArray.Create(
 249830            DiagnosticDescriptors.GeneratedConstructorRequiresPartialType,
 249831            DiagnosticDescriptors.GeneratedConstructorUnsupportedTypeShape,
 249832            DiagnosticDescriptors.GeneratedConstructorConflictsWithExplicitConstructor,
 249833            DiagnosticDescriptors.GeneratedConstructorBaseTypeRequiresParameterlessConstructor,
 249834            DiagnosticDescriptors.GeneratedConstructorNoEligibleFields,
 249835            DiagnosticDescriptors.GeneratedConstructorParameterNameCollision,
 249836            DiagnosticDescriptors.ConstructorGuardAttributeHasNoEffect,
 249837            DiagnosticDescriptors.ConstructorGuardAttributeOnIneligibleField,
 249838            DiagnosticDescriptors.InvalidConstructorGuardEnumValue,
 249839            DiagnosticDescriptors.ConstructorGuardIncompatibleWithFieldType,
 249840            DiagnosticDescriptors.ConstructorGuardTypeInvalid,
 249841            DiagnosticDescriptors.ConstructorGuardMethodNameInvalid,
 249842            DiagnosticDescriptors.ConstructorGuardMethodInvalid,
 249843            DiagnosticDescriptors.ConstructorGuardMethodAmbiguous,
 249844            DiagnosticDescriptors.ConstructorGuardDefinitionTargetInvalid,
 249845            DiagnosticDescriptors.ConstructorGuardDefinitionUnresolvedGuard,
 249846            DiagnosticDescriptors.ConstructorGuardAliasUsageArgumentUnsupported,
 249847            DiagnosticDescriptors.ConstructorGuardForwardedArgumentIncompatible);
 48
 49    public override void Initialize(AnalysisContext context)
 50    {
 18651        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 18652        context.EnableConcurrentExecution();
 18653        context.RegisterSyntaxNodeAction(
 18654            AnalyzeTypeDeclaration,
 18655            SyntaxKind.ClassDeclaration,
 18656            SyntaxKind.RecordDeclaration);
 18657    }
 58
 59    private static void AnalyzeTypeDeclaration(SyntaxNodeAnalysisContext context)
 60    {
 210261        var typeDeclaration = (TypeDeclarationSyntax)context.Node;
 210262        if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not
 210263            INamedTypeSymbol typeSymbol)
 64        {
 065            return;
 66        }
 67
 210268        var occurrences = CollectFieldGuardOccurrences(
 210269            context,
 210270            typeDeclaration);
 210271        var classHasTrigger =
 210272            GeneratedConstructorEligibility.HasGenerateConstructorAttribute(typeSymbol) ||
 210273            GeneratedConstructorEligibility.HasPositiveFieldGuardTrigger(typeSymbol);
 74
 210275        ReportFieldOccurrenceDiagnostics(
 210276            context,
 210277            typeSymbol,
 210278            occurrences,
 210279            classHasTrigger);
 80
 210281        if (!GeneratedConstructorEligibility.IsCanonicalDeclaration(
 210282            typeSymbol,
 210283            typeDeclaration))
 84        {
 485            return;
 86        }
 87
 209888        AnalyzeGuardDefinitionTarget(context, typeSymbol);
 89
 209890        if (!classHasTrigger)
 200791            return;
 92
 9193        if (typeSymbol.IsRecord &&
 9194            RecordConstructorOverloadDiscoveryHelper
 9195                .GetMarkedProperties(typeSymbol).Count > 0)
 96        {
 097            return;
 98        }
 99
 91100        AnalyzeGenerateConstructorEnumArgument(context, typeSymbol);
 91101        AnalyzeTypeShape(context, typeDeclaration, typeSymbol);
 91102    }
 103
 104    private static void AnalyzeTypeShape(
 105        SyntaxNodeAnalysisContext context,
 106        TypeDeclarationSyntax typeDeclaration,
 107        INamedTypeSymbol typeSymbol)
 108    {
 91109        var location = typeDeclaration.Identifier.GetLocation();
 110
 91111        if (typeSymbol.IsRecord)
 112        {
 2113            context.ReportDiagnostic(Diagnostic.Create(
 2114                DiagnosticDescriptors.GeneratedConstructorUnsupportedTypeShape,
 2115                location,
 2116                typeSymbol.Name,
 2117                "a record type"));
 118        }
 89119        else if (typeSymbol.ContainingType is not null)
 120        {
 2121            context.ReportDiagnostic(Diagnostic.Create(
 2122                DiagnosticDescriptors.GeneratedConstructorUnsupportedTypeShape,
 2123                location,
 2124                typeSymbol.Name,
 2125                "a nested type"));
 126        }
 127
 91128        if (!GeneratedConstructorEligibility.IsDeclaredPartial(typeSymbol))
 129        {
 2130            context.ReportDiagnostic(Diagnostic.Create(
 2131                DiagnosticDescriptors.GeneratedConstructorRequiresPartialType,
 2132                location,
 2133                typeSymbol.Name));
 134        }
 135
 91136        if (GeneratedConstructorEligibility.HasExplicitInstanceConstructor(typeSymbol))
 137        {
 6138            context.ReportDiagnostic(Diagnostic.Create(
 6139                DiagnosticDescriptors.GeneratedConstructorConflictsWithExplicitConstructor,
 6140                location,
 6141                typeSymbol.Name));
 142        }
 143
 91144        if (typeSymbol.BaseType is not null &&
 91145            typeSymbol.BaseType.SpecialType != SpecialType.System_Object &&
 91146            !GeneratedConstructorEligibility.HasAccessibleParameterlessConstructor(
 91147                typeSymbol.BaseType))
 148        {
 2149            context.ReportDiagnostic(Diagnostic.Create(
 2150                DiagnosticDescriptors.GeneratedConstructorBaseTypeRequiresParameterlessConstructor,
 2151                location,
 2152                typeSymbol.Name,
 2153                typeSymbol.BaseType.Name));
 154        }
 155
 91156        var eligibleFields =
 91157            GeneratedConstructorEligibility.GetEligibleConstructorFields(typeSymbol);
 91158        if (eligibleFields.Count == 0)
 159        {
 2160            context.ReportDiagnostic(Diagnostic.Create(
 2161                DiagnosticDescriptors.GeneratedConstructorNoEligibleFields,
 2162                location,
 2163                typeSymbol.Name));
 2164            return;
 165        }
 166
 89167        var seenParameterNames = new HashSet<string>(
 89168            System.StringComparer.Ordinal);
 362169        foreach (var field in eligibleFields)
 170        {
 92171            var parameterName =
 92172                ConstructorGenerationDiscoveryHelper.GetParameterName(field.Name);
 92173            if (!seenParameterNames.Add(parameterName))
 174            {
 2175                context.ReportDiagnostic(Diagnostic.Create(
 2176                    DiagnosticDescriptors.GeneratedConstructorParameterNameCollision,
 2177                    location,
 2178                    typeSymbol.Name,
 2179                    parameterName));
 180            }
 181        }
 89182    }
 183
 184    private static void AnalyzeGenerateConstructorEnumArgument(
 185        SyntaxNodeAnalysisContext context,
 186        INamedTypeSymbol typeSymbol)
 187    {
 230188        foreach (var attribute in typeSymbol.GetAttributes())
 189        {
 24190            if (attribute.AttributeClass is not { } attrClass ||
 24191                !GeneratedConstructorEligibility.IsNeedlrGeneratorsAttribute(
 24192                    attrClass,
 24193                    GenerateConstructorAttributeName) ||
 24194                attribute.ConstructorArguments.Length == 0)
 195            {
 196                continue;
 197            }
 198
 2199            ConstructorGuardAnalysisHelper.TryReportUndefinedEnum(
 2200                context,
 2201                ConstructorGuardAnalysisHelper.GetAttributeLocation(
 2202                    context,
 2203                    attribute),
 2204                attribute.ConstructorArguments[0]);
 205        }
 91206    }
 207
 208    private static List<ConstructorGuardOccurrence> CollectFieldGuardOccurrences(
 209        SyntaxNodeAnalysisContext context,
 210        TypeDeclarationSyntax typeDeclaration)
 211    {
 2102212        var occurrences = new List<ConstructorGuardOccurrence>();
 213
 4672214        foreach (var fieldDeclaration in typeDeclaration.Members
 2102215            .OfType<FieldDeclarationSyntax>())
 216        {
 936217            foreach (var variable in fieldDeclaration.Declaration.Variables)
 218            {
 234219                if (context.SemanticModel.GetDeclaredSymbol(variable) is not
 234220                    IFieldSymbol fieldSymbol)
 221                {
 222                    continue;
 223                }
 224
 234225                var ineligibilityReason =
 234226                    GeneratedConstructorEligibility.GetFieldIneligibilityReason(
 234227                        fieldSymbol);
 228
 636229                foreach (var attribute in fieldSymbol.GetAttributes())
 230                {
 84231                    if (attribute.AttributeClass is not { } attrClass)
 232                        continue;
 233
 84234                    if (GeneratedConstructorEligibility.IsNeedlrGeneratorsAttribute(
 84235                        attrClass,
 84236                        ConstructorIgnoreAttributeName))
 237                    {
 3238                        occurrences.Add(new ConstructorGuardOccurrence(
 3239                            fieldSymbol,
 3240                            fieldSymbol.Type,
 3241                            "field",
 3242                            attribute,
 3243                            ConstructorGuardOccurrenceKind.Ignore,
 3244                            ineligibilityReason,
 3245                            null,
 3246                            null,
 3247                            false,
 3248                            false));
 3249                        continue;
 250                    }
 251
 81252                    if (GeneratedConstructorEligibility.IsNeedlrGeneratorsAttribute(
 81253                        attrClass,
 81254                        ConstructorGuardAttributeName))
 255                    {
 52256                        occurrences.Add(
 52257                            ConstructorGuardAnalysisHelper.BuildDirectGuardOccurrence(
 52258                                fieldSymbol,
 52259                                fieldSymbol.Type,
 52260                                "field",
 52261                                attribute,
 52262                                ineligibilityReason));
 52263                        continue;
 264                    }
 265
 29266                    if (!ConstructorGuardAnalysisHelper.TryGetGuardDefinition(
 29267                        attrClass,
 29268                        out var guardType,
 29269                        out var methodName,
 29270                        out var methodNameExplicit))
 271                    {
 272                        continue;
 273                    }
 274
 29275                    occurrences.Add(
 29276                        ConstructorGuardAnalysisHelper.BuildAliasOccurrence(
 29277                            fieldSymbol,
 29278                            fieldSymbol.Type,
 29279                            "field",
 29280                            attribute,
 29281                            ineligibilityReason,
 29282                            guardType,
 29283                            methodName,
 29284                            methodNameExplicit,
 29285                            attrClass.DeclaringSyntaxReferences.Length > 0));
 286                }
 287            }
 288        }
 289
 2102290        return occurrences;
 291    }
 292
 293    private static void ReportFieldOccurrenceDiagnostics(
 294        SyntaxNodeAnalysisContext context,
 295        INamedTypeSymbol typeSymbol,
 296        IReadOnlyList<ConstructorGuardOccurrence> occurrences,
 297        bool classHasTrigger)
 298    {
 4372299        foreach (var occurrence in occurrences)
 300        {
 84301            var location =
 84302                ConstructorGuardAnalysisHelper.GetAttributeLocation(
 84303                    context,
 84304                    occurrence.Attribute);
 305
 84306            if (occurrence.IneligibilityReason is { } reason)
 307            {
 12308                context.ReportDiagnostic(Diagnostic.Create(
 12309                    DiagnosticDescriptors.ConstructorGuardAttributeOnIneligibleField,
 12310                    location,
 12311                    occurrence.Member.Name,
 12312                    reason));
 12313                continue;
 314            }
 315
 72316            if (occurrence.Kind == ConstructorGuardOccurrenceKind.Ignore &&
 72317                !classHasTrigger)
 318            {
 2319                context.ReportDiagnostic(Diagnostic.Create(
 2320                    DiagnosticDescriptors.ConstructorGuardAttributeHasNoEffect,
 2321                    location,
 2322                    occurrence.Member.Name,
 2323                    "[ConstructorIgnore]"));
 2324                continue;
 325            }
 326
 70327            if (occurrence.Kind ==
 70328                    ConstructorGuardOccurrenceKind.BuiltInNone &&
 70329                !classHasTrigger)
 330            {
 2331                context.ReportDiagnostic(Diagnostic.Create(
 2332                    DiagnosticDescriptors.ConstructorGuardAttributeHasNoEffect,
 2333                    location,
 2334                    occurrence.Member.Name,
 2335                    "[ConstructorGuard(ConstructorGuardKind.None)]"));
 2336                continue;
 337            }
 338
 68339            ConstructorGuardAnalysisHelper.AnalyzePositiveGuardOccurrence(
 68340                context,
 68341                typeSymbol,
 68342                occurrence,
 68343                location);
 344        }
 2102345    }
 346
 347    private static void AnalyzeGuardDefinitionTarget(
 348        SyntaxNodeAnalysisContext context,
 349        INamedTypeSymbol typeSymbol)
 350    {
 7698351        foreach (var attribute in typeSymbol.GetAttributes())
 352        {
 1751353            if (attribute.AttributeClass is not { } attrClass ||
 1751354                !GeneratedConstructorEligibility.IsNeedlrGeneratorsAttribute(
 1751355                    attrClass,
 1751356                    ConstructorGuardDefinitionAttributeName))
 357            {
 358                continue;
 359            }
 360
 38361            var location =
 38362                ConstructorGuardAnalysisHelper.GetAttributeLocation(
 38363                    context,
 38364                    attribute);
 38365            var targetInvalidReason =
 38366                ConstructorGuardAnalysisHelper.GetGuardDefinitionTargetInvalidReason(
 38367                    typeSymbol);
 38368            if (targetInvalidReason is not null)
 369            {
 4370                context.ReportDiagnostic(Diagnostic.Create(
 4371                    DiagnosticDescriptors.ConstructorGuardDefinitionTargetInvalid,
 4372                    location,
 4373                    typeSymbol.Name,
 4374                    targetInvalidReason));
 4375                continue;
 376            }
 377
 34378            if (attribute.ConstructorArguments.Length == 0 ||
 34379                attribute.ConstructorArguments[0].Value is not ITypeSymbol guardType ||
 34380                guardType.TypeKind == TypeKind.Error)
 381            {
 2382                context.ReportDiagnostic(Diagnostic.Create(
 2383                    DiagnosticDescriptors.ConstructorGuardDefinitionUnresolvedGuard,
 2384                    location,
 2385                    typeSymbol.Name,
 2386                    "the guard type could not be resolved"));
 2387                continue;
 388            }
 389
 32390            if (!context.Compilation.IsSymbolAccessibleWithin(
 32391                guardType,
 32392                typeSymbol))
 393            {
 0394                context.ReportDiagnostic(Diagnostic.Create(
 0395                    DiagnosticDescriptors.ConstructorGuardDefinitionUnresolvedGuard,
 0396                    location,
 0397                    typeSymbol.Name,
 0398                    $"'{guardType.ToDisplayString()}' is not accessible from '{typeSymbol.ToDisplayString()}'"));
 0399                continue;
 400            }
 401
 32402            var methodNameExplicit =
 32403                attribute.ConstructorArguments.Length > 1 &&
 32404                attribute.ConstructorArguments[1].Value is string;
 32405            var methodName = methodNameExplicit
 32406                ? (string)attribute.ConstructorArguments[1].Value!
 32407                : DefaultGuardMethodName;
 32408            if (methodNameExplicit && string.IsNullOrWhiteSpace(methodName))
 409            {
 0410                context.ReportDiagnostic(Diagnostic.Create(
 0411                    DiagnosticDescriptors.ConstructorGuardDefinitionUnresolvedGuard,
 0412                    location,
 0413                    typeSymbol.Name,
 0414                    "the guard method name must not be empty or consist only of white space"));
 0415                continue;
 416            }
 417
 32418            var resolution =
 32419                ConstructorGuardAnalysisHelper.TryResolveGuardMethod(
 32420                    context.Compilation,
 32421                    typeSymbol,
 32422                    guardType,
 32423                    methodName,
 32424                    null,
 32425                    "member",
 32426                    ImmutableArray<ITypeSymbol>.Empty,
 32427                    out _,
 32428                    out var reason,
 32429                    out _);
 32430            if (resolution == GuardMethodResolution.NotFound)
 431            {
 2432                context.ReportDiagnostic(Diagnostic.Create(
 2433                    DiagnosticDescriptors.ConstructorGuardDefinitionUnresolvedGuard,
 2434                    location,
 2435                    typeSymbol.Name,
 2436                    reason ?? "no compatible guard method was found"));
 437            }
 438        }
 2098439    }
 440}