File: Microsoft.CodeQuality.Analyzers\Maintainability\AvoidUninstantiatedInternalClasses.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.Lightup;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeQuality.Analyzers.Maintainability
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA1812: <inheritdoc cref="AvoidUninstantiatedInternalClassesTitle"/>
    /// </summary>
    public abstract class AvoidUninstantiatedInternalClassesAnalyzer : DiagnosticAnalyzer
    {
        private static readonly char[] s_commaArray = new[] { ',' };

        internal const string RuleId = "CA1812";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(AvoidUninstantiatedInternalClassesTitle)),
            CreateLocalizableResourceString(nameof(AvoidUninstantiatedInternalClassesMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.Disabled,    // Code coverage tools provide superior results when done correctly.
            description: CreateLocalizableResourceString(nameof(AvoidUninstantiatedInternalClassesDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false,
            isReportedAtCompilationEnd: true);

        public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public abstract void RegisterLanguageSpecificChecks(CompilationStartAnalysisContext context, ConcurrentDictionary<INamedTypeSymbol, object?> instantiatedTypes);

        public sealed override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);

            context.RegisterCompilationStartAction(startContext =>
            {
                ConcurrentDictionary<INamedTypeSymbol, object?> instantiatedTypes = new ConcurrentDictionary<INamedTypeSymbol, object?>();
                var internalTypes = new ConcurrentDictionary<INamedTypeSymbol, object?>();

                var compilation = startContext.Compilation;
                var entryPointContainingType = compilation.GetEntryPoint(startContext.CancellationToken)?.ContainingType;
                var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(compilation);
                var hasInternalsVisibleTo = startContext.Compilation.Assembly.HasAnyAttribute(
                    startContext.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesInternalsVisibleToAttribute));

                var systemAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttribute);
                var iConfigurationSectionHandlerSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemConfigurationIConfigurationSectionHandler);
                var configurationSectionSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemConfigurationConfigurationSection);
                var safeHandleSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesSafeHandle);
                var traceListenerSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsTraceListener);
                var mef1ExportAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemComponentModelCompositionExportAttribute);
                var mef2ExportAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCompositionExportAttribute);

                var coClassAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesCoClassAttribute);
                var designerAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemComponentModelDesignerAttribute);
                var debuggerTypeProxyAttributeSymbol = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsDebuggerTypeProxyAttribute);

                var instantiatingAttributeChecker = new List<(Func<INamedTypeSymbol, bool> isAttributeTarget, Func<AttributeData, Compilation, INamedTypeSymbol?> findTypeOrDefault)>
                {
                    (type => CanBeCoClassAttributeContext(type), (attribute, _) => FindTypeIfCoClassAttribute(attribute)),
                    (type => CanBeDesignerAttributeContext(type), (attribute, compilation) => FindTypeIfDesignerAttribute(attribute, compilation)),
                    (type => CanBeDebuggerTypeProxyAttributeContext(type), (attribute, compilation) => FindTypeIfDebuggerTypeProxyAttribute(attribute, compilation)),
                };

                RegisterLanguageSpecificChecks(startContext, instantiatedTypes);

                startContext.RegisterOperationAction(context =>
                {
                    if (context.Operation.Type is INamedTypeSymbol namedType && namedType.ContainingAssembly.Equals(context.ContainingSymbol.ContainingAssembly))
                    {
                        instantiatedTypes.TryAdd(namedType, null);
                    }
                }, OperationKind.ObjectCreation, OperationKindEx.CollectionExpression);

                startContext.RegisterSymbolAction(context =>
                {
                    var type = (INamedTypeSymbol)context.Symbol;
                    if (!type.IsExternallyVisible() &&
                        !IsOkToBeUninstantiated(type,
                            entryPointContainingType,
                            systemAttributeSymbol,
                            iConfigurationSectionHandlerSymbol,
                            configurationSectionSymbol,
                            safeHandleSymbol,
                            traceListenerSymbol,
                            mef1ExportAttributeSymbol,
                            mef2ExportAttributeSymbol))
                    {
                        internalTypes.TryAdd(type, null);
                    }

                    // Instantiation from the subtype constructor initializer.
                    // We are only interested in base types that are from the same compilation, so we check sure they are in the same assembly.
                    // Adding all base types is unnecessarily expensive.
                    if (type.BaseType is { } baseType && baseType.ContainingAssembly.Equals(type.ContainingAssembly))
                    {
                        instantiatedTypes.TryAdd(baseType, null);
                    }

                    var typeAttributes = type.GetAttributes();
                    if (!typeAttributes.IsEmpty)
                    {
                        // Some attributes are known to behave as type activator so we want to check them
                        var applicableAttributes = instantiatingAttributeChecker.Where(tuple => tuple.isAttributeTarget(type)).ToArray();
                        foreach (var attribute in typeAttributes)
                        {
                            foreach (var (_, findTypeOrDefault) in applicableAttributes)
                            {
                                if (findTypeOrDefault(attribute, context.Compilation) is INamedTypeSymbol namedType && namedType.ContainingAssembly.Equals(type.ContainingAssembly))
                                {
                                    instantiatedTypes.TryAdd(namedType, null);
                                    break;
                                }
                            }
                        }
                    }
                }, SymbolKind.NamedType);

                startContext.RegisterOperationAction(context =>
                {
                    var expr = (IObjectCreationOperation)context.Operation;
                    var constructedClass = (INamedTypeSymbol?)expr.Type;

                    if (constructedClass == null || !constructedClass.IsGenericType || constructedClass.IsUnboundGenericType)
                    {
                        return;
                    }

                    var generics = constructedClass.TypeParameters.Zip(constructedClass.TypeArguments, (parameter, argument) => (parameter, argument));
                    ProcessGenericTypes(generics, instantiatedTypes);
                }, OperationKind.ObjectCreation);

                startContext.RegisterOperationAction(context =>
                {
                    var expr = (IInvocationOperation)context.Operation;
                    var methodType = expr.TargetMethod;

                    if (!methodType.IsGenericMethod)
                    {
                        return;
                    }

                    var generics = methodType.TypeParameters.Zip(methodType.TypeArguments, (parameter, argument) => (parameter, argument));
                    ProcessGenericTypes(generics, instantiatedTypes);
                }, OperationKind.Invocation);

                startContext.RegisterCompilationEndAction(context =>
                {
                    var uninstantiatedInternalTypes = internalTypes
                        .Select(it => it.Key.OriginalDefinition)
                        .Except(instantiatedTypes.Select(it => it.Key.OriginalDefinition))
                        .Where(type => !HasInstantiatedNestedType(type, instantiatedTypes.Keys));

                    foreach (var type in uninstantiatedInternalTypes)
                    {
                        if (!hasInternalsVisibleTo || context.Options.GetBoolOptionValue(EditorConfigOptionNames.IgnoreInternalsVisibleTo, Rule, type, context.Compilation, defaultValue: false))
                        {
                            context.ReportDiagnostic(type.CreateDiagnostic(Rule, type.FormatMemberName()));
                        }
                    }
                });

                return;

                // Local functions

                bool CanBeCoClassAttributeContext(INamedTypeSymbol type)
                     => coClassAttributeSymbol != null && type.TypeKind == TypeKind.Interface;

                INamedTypeSymbol? FindTypeIfCoClassAttribute(AttributeData attribute)
                {
                    RoslynDebug.Assert(coClassAttributeSymbol != null);

                    if (attribute.AttributeClass != null &&
                        attribute.AttributeClass.Equals(coClassAttributeSymbol) &&
                        attribute.ConstructorArguments.Length == 1 &&
                        attribute.ConstructorArguments[0].Kind == TypedConstantKind.Type &&
                        attribute.ConstructorArguments[0].Value is INamedTypeSymbol typeSymbol &&
                        typeSymbol.TypeKind == TypeKind.Class)
                    {
                        return typeSymbol;
                    }

                    return null;
                }

                bool CanBeDesignerAttributeContext(INamedTypeSymbol type)
                    => designerAttributeSymbol != null && (type.TypeKind == TypeKind.Interface || type.TypeKind == TypeKind.Class);

                INamedTypeSymbol? FindTypeIfDesignerAttribute(AttributeData attribute, Compilation compilation)
                {
                    RoslynDebug.Assert(designerAttributeSymbol != null);

                    if (attribute.ConstructorArguments.Length is not (1 or 2) ||
                        attribute.AttributeClass == null ||
                        !attribute.AttributeClass.Equals(designerAttributeSymbol))
                    {
                        return null;
                    }

                    switch (attribute.ConstructorArguments[0].Value)
                    {
                        case string designerTypeName:
                            {
                                if (IsTypeInCurrentAssembly(designerTypeName, compilation, out var namedType))
                                {
                                    return namedType;
                                }

                                break;
                            }

                        case INamedTypeSymbol namedType:
                            return namedType;
                    }

                    return null;
                }

                bool CanBeDebuggerTypeProxyAttributeContext(INamedTypeSymbol type)
                    => debuggerTypeProxyAttributeSymbol != null && (type.TypeKind == TypeKind.Struct || type.TypeKind == TypeKind.Class);

                INamedTypeSymbol? FindTypeIfDebuggerTypeProxyAttribute(AttributeData attribute, Compilation compilation)
                {
                    RoslynDebug.Assert(debuggerTypeProxyAttributeSymbol != null);

                    if (attribute.AttributeClass == null ||
                        !attribute.AttributeClass.Equals(debuggerTypeProxyAttributeSymbol))
                    {
                        return null;
                    }

                    switch (attribute.ConstructorArguments[0].Value)
                    {
                        case string typeName:
                            {
                                if (IsTypeInCurrentAssembly(typeName, compilation, out var namedType))
                                {
                                    return namedType;
                                }

                                break;
                            }

                        case INamedTypeSymbol namedType:
                            return namedType;
                    }

                    return null;
                }
            });
        }

        private bool HasInstantiatedNestedType(INamedTypeSymbol type, IEnumerable<INamedTypeSymbol> instantiatedTypes)
        {
            var nestedTypes = type.GetTypeMembers();

            foreach (var nestedType in nestedTypes)
            {
                // We don't care whether a private nested type is instantiated, because if it
                // is, it can only have happened within the type itself.
                if (nestedType.DeclaredAccessibility == Accessibility.Private)
                {
                    continue;
                }

                if (instantiatedTypes.Contains(nestedType))
                {
                    return true;
                }

                if (HasInstantiatedNestedType(nestedType, instantiatedTypes))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsOkToBeUninstantiated(
            INamedTypeSymbol type,
            INamedTypeSymbol? entryPointContainingType,
            INamedTypeSymbol? systemAttributeSymbol,
            INamedTypeSymbol? iConfigurationSectionHandlerSymbol,
            INamedTypeSymbol? configurationSectionSymbol,
            INamedTypeSymbol? safeHandleSymbol,
            INamedTypeSymbol? traceListenerSymbol,
            INamedTypeSymbol? mef1ExportAttributeSymbol,
            INamedTypeSymbol? mef2ExportAttributeSymbol)
        {
            if (type.TypeKind != TypeKind.Class || type.IsAbstract || type.IsStatic)
            {
                return true;
            }

            // Attributes are not instantiated in IL but are created by reflection.
            if (type.Inherits(systemAttributeSymbol))
            {
                return true;
            }

            // The type containing the assembly's entry point is OK.
            if (SymbolEqualityComparer.Default.Equals(entryPointContainingType, type))
            {
                return true;
            }

            // MEF exported classes are instantiated by MEF, by reflection.
            if (IsMefExported(type, mef1ExportAttributeSymbol, mef2ExportAttributeSymbol))
            {
                return true;
            }

            // Types implementing the (deprecated) IConfigurationSectionHandler interface
            // are OK because they are instantiated by the configuration system.
            if (type.Inherits(iConfigurationSectionHandlerSymbol))
            {
                return true;
            }

            // Likewise for types derived from ConfigurationSection.
            if (type.Inherits(configurationSectionSymbol))
            {
                return true;
            }

            // SafeHandles can be created from within the type itself by native code.
            if (type.Inherits(safeHandleSymbol))
            {
                return true;
            }

            if (type.Inherits(traceListenerSymbol))
            {
                return true;
            }

            if (type.IsStaticHolderType())
            {
                return true;
            }

            return false;
        }

        public static bool IsMefExported(
            INamedTypeSymbol type,
            INamedTypeSymbol? mef1ExportAttributeSymbol,
            INamedTypeSymbol? mef2ExportAttributeSymbol)
        {
            return (mef1ExportAttributeSymbol != null && type.HasAnyAttribute(mef1ExportAttributeSymbol))
                || (mef2ExportAttributeSymbol != null && type.HasAnyAttribute(mef2ExportAttributeSymbol));
        }

        /// <summary>
        /// If a type is passed a generic argument to another type or a method that specifies that the type must have a constructor,
        /// we presume that the method will be constructing the type, and add it to the list of instantiated types.
        /// </summary>
        protected void ProcessGenericTypes(IEnumerable<(ITypeParameterSymbol param, ITypeSymbol arg)> generics, ConcurrentDictionary<INamedTypeSymbol, object?> instantiatedTypes)
        {
            foreach (var (typeParam, typeArg) in generics)
            {
                if (typeParam.HasConstructorConstraint)
                {
                    void ProcessNamedTypeParamConstraint(INamedTypeSymbol namedTypeArg)
                    {
                        if (!instantiatedTypes.TryAdd(namedTypeArg, null))
                        {
                            // Already processed.
                            return;
                        }

                        // We need to handle if this type param also has type params that have a generic constraint. Take the following example:
                        // new Factory1<Factory2<InstantiatedType>>();
                        // In this example, Factory1 and Factory2 have type params with constructor constraints. Therefore, we need to add all 3
                        // types to the list of types that have actually been instantiated. However, in the following example:
                        // new List<Factory<InstantiatedType>>();
                        // List does not have a constructor constraint, so we can't reasonably infer anything about its type parameters.
                        if (namedTypeArg.IsGenericType)
                        {
                            var newGenerics = namedTypeArg.TypeParameters.Zip(namedTypeArg.TypeArguments, (parameter, argument) => (parameter, argument));
                            ProcessGenericTypes(newGenerics, instantiatedTypes);
                        }
                    }

                    if (typeArg is INamedTypeSymbol namedType)
                    {
                        ProcessNamedTypeParamConstraint(namedType);
                    }
                    else if (typeArg is ITypeParameterSymbol typeParameterArg && !typeParameterArg.ConstraintTypes.IsEmpty)
                    {
                        static IEnumerable<INamedTypeSymbol> GetAllNamedTypeConstraints(ITypeParameterSymbol t)
                        {
                            var directConstraints = t.ConstraintTypes.OfType<INamedTypeSymbol>();
                            var inheritedConstraints = t.ConstraintTypes.OfType<ITypeParameterSymbol>()
                                .SelectMany(constraintT => GetAllNamedTypeConstraints(constraintT));
                            return directConstraints.Concat(inheritedConstraints);
                        }

                        var constraints = GetAllNamedTypeConstraints(typeParameterArg);
                        foreach (INamedTypeSymbol constraint in constraints)
                        {
                            ProcessNamedTypeParamConstraint(constraint);
                        }
                    }
                }
            }
        }

        private static bool IsTypeInCurrentAssembly(string typeName, Compilation compilation, out INamedTypeSymbol? namedType)
        {
            namedType = null;
            var nameParts = typeName.Split(s_commaArray, StringSplitOptions.RemoveEmptyEntries);

            return nameParts.Length >= 2 &&
                nameParts[1].Trim().Equals(compilation.AssemblyName, StringComparison.Ordinal) &&
                compilation.TryGetOrCreateTypeByMetadataName(nameParts[0].Trim(), out namedType) &&
                namedType.ContainingAssembly.Equals(compilation.Assembly);
        }
    }
}