// 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.Immutable; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines { using static MicrosoftCodeQualityAnalyzersResources; /// <summary> /// CA1034: <inheritdoc cref="NestedTypesShouldNotBeVisibleTitle"/> /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class NestedTypesShouldNotBeVisibleAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA1034"; private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(NestedTypesShouldNotBeVisibleTitle)); private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(NestedTypesShouldNotBeVisibleDescription)); internal static readonly DiagnosticDescriptor DefaultRule = DiagnosticDescriptorHelper.Create( RuleId, s_localizableTitle, CreateLocalizableResourceString(nameof(NestedTypesShouldNotBeVisibleMessageDefault)), DiagnosticCategory.Design, RuleLevel.CandidateForRemoval, // Need to validate cases where people want it. description: s_localizableDescription, isPortedFxCopRule: true, isDataflowRule: false); internal static readonly DiagnosticDescriptor VisualBasicModuleRule = DiagnosticDescriptorHelper.Create( RuleId, s_localizableTitle, CreateLocalizableResourceString(nameof(NestedTypesShouldNotBeVisibleMessageVisualBasicModule)), DiagnosticCategory.Design, RuleLevel.CandidateForRemoval, // Need to validate cases where people want it. description: s_localizableDescription, isPortedFxCopRule: true, isDataflowRule: false); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DefaultRule, VisualBasicModuleRule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction( compilationStartContext => { var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(compilationStartContext.Compilation); INamedTypeSymbol? enumeratorType = wellKnownTypeProvider.Compilation.GetSpecialType(SpecialType.System_Collections_IEnumerator); INamedTypeSymbol? dataSetType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDataDataSet); INamedTypeSymbol? dataTableType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDataDataTable); INamedTypeSymbol? dataRowType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDataDataRow); compilationStartContext.RegisterSymbolAction( symbolAnalysisContext => { var nestedType = (INamedTypeSymbol)symbolAnalysisContext.Symbol; INamedTypeSymbol containingType = nestedType.ContainingType; if (containingType == null) { return; } // Do not report diagnostic for compiler generated nested types for delegate declaration if (nestedType.TypeKind == TypeKind.Delegate) { return; } // Do not report diagnostic for extension members if (nestedType.TypeKind == ITypeSymbolExtensions.ExtensionTypeKind) { return; } // The Framework Design Guidelines (see 4.9 Nested Types) say that it is okay // to expose nested types for advanced customization and subclassing scenarios, // so, following FxCop's implementation of this rule, we allow protected and // internal nested types. if (nestedType.DeclaredAccessibility != Accessibility.Public) { return; } // Even if the nested type is declared public, don't complain if it's within // a type that's not visible outside the assembly. if (!containingType.IsExternallyVisible()) { return; } // By the design guidelines, nested enumerators are exempt. if (nestedType.AllInterfaces.Contains(enumeratorType)) { return; } // FxCop allowed public nested enums to accommodate .NET types such as // Environment.SpecialFolders. if (nestedType.TypeKind == TypeKind.Enum) { return; } // Allow builder pattern if (nestedType.Name.EndsWith("Builder", StringComparison.Ordinal) && containingType.ContainingType == null && !containingType.InstanceConstructors.Any(ctor => ctor.IsExternallyVisible())) { return; } if (IsDataSetSpecialCase(containingType, nestedType, dataSetType, dataTableType, dataRowType)) { return; } DiagnosticDescriptor descriptor = containingType.TypeKind == TypeKind.Module ? VisualBasicModuleRule : DefaultRule; symbolAnalysisContext.ReportDiagnostic(nestedType.CreateDiagnostic(descriptor, nestedType.Name)); }, SymbolKind.NamedType); }); } // When you use the Visual Studio Dataset Designer to add a DataTable to a DataSet, the // designer generates two public nested types within the DataSet: a DataTable and a DataRow. // Since these are generated code, we don't want to fire on them. private static bool IsDataSetSpecialCase( INamedTypeSymbol containingType, INamedTypeSymbol nestedType, INamedTypeSymbol? dataSetType, INamedTypeSymbol? dataTableType, INamedTypeSymbol? dataRowType) { if (!containingType.GetBaseTypes().Contains(dataSetType)) { return false; } var nestedTypeBases = nestedType.GetBaseTypes().ToList(); return dataTableType != null && nestedTypeBases.Contains(dataTableType) || dataRowType != null && nestedTypeBases.Contains(dataRowType); } } }