< Summary

Information
Class: AsiBackbone.Analyzers.LocalDevelopmentSigningProductionAnalyzer
Assembly: AsiBackbone.Analyzers
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Analyzers/LocalDevelopmentSigningProductionAnalyzer.cs
Line coverage
82%
Covered lines: 117
Uncovered lines: 25
Coverable lines: 142
Total lines: 337
Line coverage: 82.3%
Branch coverage
64%
Covered branches: 88
Total branches: 136
Branch coverage: 64.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Analyzers/LocalDevelopmentSigningProductionAnalyzer.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using Microsoft.CodeAnalysis;
 3using Microsoft.CodeAnalysis.Diagnostics;
 4using Microsoft.CodeAnalysis.Operations;
 5
 6namespace AsiBackbone.Analyzers;
 7
 8[DiagnosticAnalyzer(LanguageNames.CSharp)]
 9public sealed class LocalDevelopmentSigningProductionAnalyzer : DiagnosticAnalyzer
 10{
 11    public const string DiagnosticId = "ASIB002";
 12
 13    /// <summary>
 14    /// Reports local-development signing wired with no environment guard at all.
 15    /// </summary>
 16    public const string UnguardedDiagnosticId = "ASIB003";
 17
 18    private const string LocalDevelopmentNamespace = "AsiBackbone.Signing.LocalDevelopment";
 19
 120    private static readonly DiagnosticDescriptor Rule = new(
 121        DiagnosticId,
 122        "Do not wire local-development signing in production branches",
 123        "Local-development signing type '{0}' is used inside a production environment branch; use a host-owned productio
 124        "AsiBackbone.ProductionSafety",
 125        DiagnosticSeverity.Warning,
 126        isEnabledByDefault: true,
 127        description: "LocalDevelopment signing providers generate in-process keys for tests, samples, and local proof pa
 28
 29    /// <remarks>
 30    /// <see cref="Rule" /> only fires on a call the analyzer can see inside a production branch, so an unconditional
 31    /// registration — the shape that actually reaches production — was never reported. The runtime guard in
 32    /// <c>UseLocalDevelopmentSigning</c> throws in Production; this rule surfaces the same problem at build time.
 33    /// </remarks>
 134    private static readonly DiagnosticDescriptor UnguardedRule = new(
 135        UnguardedDiagnosticId,
 136        "Guard local-development signing by environment",
 137        "Local-development signing type '{0}' is wired without an environment guard; it will be registered in whichever 
 138        "AsiBackbone.ProductionSafety",
 139        DiagnosticSeverity.Warning,
 140        isEnabledByDefault: true,
 141        description: "LocalDevelopment signing providers generate an in-process key that is never persisted, so artifact
 42
 943    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule, UnguardedRule];
 44
 45    public override void Initialize(AnalysisContext context)
 46    {
 947        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 948        context.EnableConcurrentExecution();
 949        context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
 950        context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
 951    }
 52
 53    private static void AnalyzeInvocation(OperationAnalysisContext context)
 54    {
 1555        var invocation = (IInvocationOperation)context.Operation;
 56
 1557        if (IsSuppressedByHostMarker(context.ContainingSymbol)
 1558            || IsNestedInsideInvocationThatAlreadyReferencesLocalDevelopment(invocation))
 59        {
 460            return;
 61        }
 62
 1163        bool insideProductionBranch = IsInsideProductionBranch(invocation);
 64
 1165        if (!insideProductionBranch && IsInsideEnvironmentConditional(invocation))
 66        {
 467            return;
 68        }
 69
 770        ITypeSymbol? localDevelopmentType = FindReferencedLocalDevelopmentType(invocation);
 771        if (localDevelopmentType is null)
 72        {
 373            return;
 74        }
 75
 476        context.ReportDiagnostic(
 477            Diagnostic.Create(
 478                insideProductionBranch ? Rule : UnguardedRule,
 479                invocation.Syntax.GetLocation(),
 480                localDevelopmentType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
 481    }
 82
 83    private static void AnalyzeObjectCreation(OperationAnalysisContext context)
 84    {
 2185        var objectCreation = (IObjectCreationOperation)context.Operation;
 86
 2187        if (IsSuppressedByHostMarker(context.ContainingSymbol) || !IsInsideProductionBranch(objectCreation))
 88        {
 2089            return;
 90        }
 91
 192        ITypeSymbol? localDevelopmentType = FindLocalDevelopmentType(objectCreation.Type);
 193        if (localDevelopmentType is null)
 94        {
 095            return;
 96        }
 97
 198        context.ReportDiagnostic(
 199            Diagnostic.Create(
 1100                Rule,
 1101                objectCreation.Syntax.GetLocation(),
 1102                localDevelopmentType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
 1103    }
 104
 105    private static bool IsNestedInsideInvocationThatAlreadyReferencesLocalDevelopment(IInvocationOperation invocation)
 106    {
 12107        return invocation.Parent is IArgumentOperation { Parent: IInvocationOperation parentInvocation }
 12108            && FindReferencedLocalDevelopmentType(parentInvocation) is not null;
 109    }
 110
 111    private static ITypeSymbol? FindReferencedLocalDevelopmentType(IInvocationOperation invocation)
 112    {
 8113        ITypeSymbol? containingType = FindLocalDevelopmentType(invocation.TargetMethod.ContainingType);
 8114        if (containingType is not null)
 115        {
 0116            return containingType;
 117        }
 118
 21119        foreach (ITypeSymbol typeArgument in invocation.TargetMethod.TypeArguments)
 120        {
 5121            ITypeSymbol? localDevelopmentType = FindLocalDevelopmentType(typeArgument);
 5122            if (localDevelopmentType is not null)
 123            {
 5124                return localDevelopmentType;
 125            }
 126        }
 127
 12128        foreach (IArgumentOperation argument in invocation.Arguments)
 129        {
 3130            ITypeSymbol? localDevelopmentType = FindLocalDevelopmentType(argument.Value.Type);
 3131            if (localDevelopmentType is not null)
 132            {
 0133                return localDevelopmentType;
 134            }
 135        }
 136
 3137        return FindLocalDevelopmentType(invocation.Type);
 138    }
 139
 140    private static ITypeSymbol? FindLocalDevelopmentType(ITypeSymbol? type)
 141    {
 20142        if (type is null)
 143        {
 0144            return null;
 145        }
 146
 20147        if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
 148        {
 0149            foreach (ITypeSymbol typeArgument in namedType.TypeArguments)
 150            {
 0151                ITypeSymbol? localDevelopmentType = FindLocalDevelopmentType(typeArgument);
 0152                if (localDevelopmentType is not null)
 153                {
 0154                    return localDevelopmentType;
 155                }
 156            }
 157        }
 158
 20159        string namespaceName = type.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) ?
 20160        return namespaceName.Equals(LocalDevelopmentNamespace, StringComparison.Ordinal)
 20161            ? type
 20162            : null;
 163    }
 164
 165    private static bool IsInsideProductionBranch(IOperation operation)
 166    {
 206167        for (IOperation? current = operation.Parent; current is not null; current = current.Parent)
 168        {
 80169            if (current is IConditionalOperation conditionalOperation
 80170                && IsProductionLikeCondition(conditionalOperation.Condition))
 171            {
 7172                return true;
 173            }
 174        }
 175
 23176        return false;
 177    }
 178
 179    /// <summary>
 180    /// Determines whether the operation sits inside any conditional that tests the host environment.
 181    /// </summary>
 182    /// <remarks>
 183    /// A registration guarded by an environment check — for either the production or the non-production side — is a
 184    /// deliberate choice the host has already made. Only registrations with no environment check at all are reported by
 185    /// <see cref="UnguardedRule" />.
 186    /// </remarks>
 187    private static bool IsInsideEnvironmentConditional(IOperation operation)
 188    {
 24189        for (IOperation? current = operation.Parent; current is not null; current = current.Parent)
 190        {
 11191            if (current is IConditionalOperation conditionalOperation
 11192                && ConditionReferencesEnvironment(conditionalOperation.Condition))
 193            {
 4194                return true;
 195            }
 196        }
 197
 1198        return false;
 199    }
 200
 201    private static bool ConditionReferencesEnvironment(IOperation operation)
 202    {
 4203        operation = Unwrap(operation);
 204
 4205        if (IsEnvironmentNameReference(operation))
 206        {
 0207            return true;
 208        }
 209
 210        switch (operation)
 211        {
 212            case IInvocationOperation invocationOperation:
 4213                IMethodSymbol method = invocationOperation.TargetMethod.ReducedFrom ?? invocationOperation.TargetMethod;
 4214                string namespaceName = method.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessag
 215
 4216                return namespaceName.Equals("Microsoft.Extensions.Hosting", StringComparison.Ordinal)
 4217                    || InvocationComparesEnvironmentNameToProduction(invocationOperation)
 4218                    || IsEnvironmentNameReference(invocationOperation.Instance);
 219
 220            case IBinaryOperation binaryOperation:
 0221                return ConditionReferencesEnvironment(binaryOperation.LeftOperand)
 0222                    || ConditionReferencesEnvironment(binaryOperation.RightOperand);
 223
 224            case IUnaryOperation unaryOperation:
 0225                return ConditionReferencesEnvironment(unaryOperation.Operand);
 226
 227            case IPropertyReferenceOperation propertyReference:
 0228                return propertyReference.Property.Name.Contains("Environment", StringComparison.Ordinal);
 229
 230            default:
 0231                return false;
 232        }
 233    }
 234
 235    private static bool IsProductionLikeCondition(IOperation operation)
 236    {
 11237        operation = Unwrap(operation);
 238
 11239        return operation switch
 11240        {
 10241            IInvocationOperation invocationOperation => IsProductionInvocation(invocationOperation)
 10242                || InvocationComparesEnvironmentNameToProduction(invocationOperation),
 1243            IBinaryOperation binaryOperation => BinaryComparesEnvironmentNameToProduction(binaryOperation),
 0244            _ => false
 11245        };
 246    }
 247
 248    private static bool IsProductionInvocation(IInvocationOperation invocation)
 249    {
 10250        IMethodSymbol method = invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod;
 10251        string namespaceName = method.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
 252
 10253        return method.Name.Equals("IsProduction", StringComparison.Ordinal)
 10254            && namespaceName.Equals("Microsoft.Extensions.Hosting", StringComparison.Ordinal);
 255    }
 256
 257    private static bool InvocationComparesEnvironmentNameToProduction(IInvocationOperation invocation)
 258    {
 4259        if (!invocation.TargetMethod.Name.Equals("Equals", StringComparison.Ordinal))
 260        {
 4261            return false;
 262        }
 263
 0264        bool hasEnvironmentName = IsEnvironmentNameReference(invocation.Instance);
 0265        bool hasProductionConstant = false;
 266
 0267        foreach (IArgumentOperation argument in invocation.Arguments)
 268        {
 0269            IOperation value = Unwrap(argument.Value);
 0270            hasEnvironmentName = hasEnvironmentName || IsEnvironmentNameReference(value);
 0271            hasProductionConstant = hasProductionConstant || IsProductionConstant(value);
 272        }
 273
 0274        return hasEnvironmentName && hasProductionConstant;
 275    }
 276
 277    private static bool BinaryComparesEnvironmentNameToProduction(IBinaryOperation binaryOperation)
 278    {
 1279        if (binaryOperation.OperatorKind != BinaryOperatorKind.Equals)
 280        {
 0281            return false;
 282        }
 283
 1284        IOperation left = Unwrap(binaryOperation.LeftOperand);
 1285        IOperation right = Unwrap(binaryOperation.RightOperand);
 286
 1287        return (IsEnvironmentNameReference(left) && IsProductionConstant(right))
 1288            || (IsEnvironmentNameReference(right) && IsProductionConstant(left));
 289    }
 290
 291    private static bool IsEnvironmentNameReference(IOperation? operation)
 292    {
 5293        operation = operation is null ? null : Unwrap(operation);
 294
 5295        return operation is IPropertyReferenceOperation propertyReference
 5296            && propertyReference.Property.Name.Equals("EnvironmentName", StringComparison.Ordinal);
 297    }
 298
 299    private static bool IsProductionConstant(IOperation operation)
 300    {
 1301        return operation.ConstantValue.HasValue
 1302            && operation.ConstantValue.Value is string value
 1303            && value.Equals("Production", StringComparison.OrdinalIgnoreCase);
 304    }
 305
 306    private static IOperation Unwrap(IOperation operation)
 307    {
 22308        while (operation is IConversionOperation conversionOperation)
 309        {
 0310            operation = conversionOperation.Operand;
 0311        }
 312
 22313        return operation;
 314    }
 315
 316    private static bool IsSuppressedByHostMarker(ISymbol? symbol)
 317    {
 116318        for (ISymbol? current = symbol; current is not null; current = current.ContainingSymbol)
 319        {
 139320            foreach (AttributeData attribute in current.GetAttributes())
 321            {
 14322                string? attributeName = attribute.AttributeClass?.Name;
 14323                if (attributeName is "AsiBackboneProductionConfigurationReviewed" or "AsiBackboneProductionConfiguration
 324                {
 5325                    return true;
 326                }
 327            }
 328
 53329            if (current is INamedTypeSymbol)
 330            {
 331                break;
 332            }
 333        }
 334
 31335        return false;
 336    }
 337}