// 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); } } }