File: MetaAnalyzers\DiagnosticAnalyzerAPIUsageAnalyzer.cs
Web Access
Project: src\src\RoslynAnalyzers\Microsoft.CodeAnalysis.Analyzers\Core\Microsoft.CodeAnalysis.Analyzers.csproj (Microsoft.CodeAnalysis.Analyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers.Helpers;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
namespace Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers
{
    using static CodeAnalysisDiagnosticsResources;
 
    /// <summary>
    /// RS1022: <inheritdoc cref="DoNotUseTypesFromAssemblyRuleTitle"/>
    /// </summary>
    public abstract class DiagnosticAnalyzerApiUsageAnalyzer<TTypeSyntax> : DiagnosticAnalyzer
        where TTypeSyntax : SyntaxNode
    {
        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(DoNotUseTypesFromAssemblyRuleTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(DoNotUseTypesFromAssemblyRuleDescription), nameof(AnalysisContext), DiagnosticWellKnownNames.RegisterCompilationStartActionName);
        private const string CodeActionMetadataName = "Microsoft.CodeAnalysis.CodeActions.CodeAction";
        private const string HelpLinkUri = "https://github.com/dotnet/roslyn/blob/main/docs/roslyn-analyzers/rules/RS1022.md";
        private static readonly ImmutableArray<string> s_WorkspaceAssemblyNames = ImmutableArray.Create(
            "Microsoft.CodeAnalysis.Workspaces",
            "Microsoft.CodeAnalysis.CSharp.Workspaces",
            "Microsoft.CodeAnalysis.VisualBasic.Workspaces");
 
        public static readonly DiagnosticDescriptor DoNotUseTypesFromAssemblyDirectRule = new(
            DiagnosticIds.DoNotUseTypesFromAssemblyRuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(DoNotUseTypesFromAssemblyRuleDirectMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescription,
            helpLinkUri: HelpLinkUri,
            customTags: WellKnownDiagnosticTagsExtensions.CompilationEndAndTelemetry);
 
        public static readonly DiagnosticDescriptor DoNotUseTypesFromAssemblyIndirectRule = new(
            DiagnosticIds.DoNotUseTypesFromAssemblyRuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(DoNotUseTypesFromAssemblyRuleIndirectMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescription,
            helpLinkUri: HelpLinkUri,
            customTags: WellKnownDiagnosticTagsExtensions.CompilationEndAndTelemetry);
 
        protected abstract bool IsNamedTypeDeclarationBlock(SyntaxNode syntax);
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DoNotUseTypesFromAssemblyDirectRule, DoNotUseTypesFromAssemblyIndirectRule);
 
        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
 
            context.RegisterCompilationStartAction(compilationStartContext =>
            {
                // This analyzer is disabled by default via a configuration option that also applies to RS1038. It only
                // needs to proceed if .globalconfig contains the following line to enable it:
                //
                // roslyn_correctness.assembly_reference_validation = relaxed
                if (CompilerExtensionStrictApiAnalyzer.IsStrictAnalysisEnabled(compilationStartContext.Options))
                {
                    // RS1038 is being applied instead of RS1022
                    return;
                }
 
                if (compilationStartContext.Compilation.GetOrCreateTypeByMetadataName(CodeActionMetadataName) == null)
                {
                    // No reference to core Workspaces assembly.
                    return;
                }
 
                INamedTypeSymbol? diagnosticAnalyzer = compilationStartContext.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsDiagnosticAnalyzer);
                if (diagnosticAnalyzer == null)
                {
                    // Does not contain any diagnostic analyzers.
                    return;
                }
 
                var hasAccessToTypeFromWorkspaceAssemblies = false;
                var namedTypesToAccessedTypesMap = new ConcurrentDictionary<INamedTypeSymbol, ImmutableHashSet<INamedTypeSymbol>>();
                var diagnosticAnalyzerTypes = new ConcurrentBag<INamedTypeSymbol>();
                compilationStartContext.RegisterSymbolAction(symbolContext =>
                {
                    var namedType = (INamedTypeSymbol)symbolContext.Symbol;
                    if (namedType.DerivesFrom(diagnosticAnalyzer, baseTypesOnly: true))
                    {
                        diagnosticAnalyzerTypes.Add(namedType);
                    }
 
                    var usedTypes = GetUsedNamedTypes(namedType, symbolContext.Compilation, symbolContext.CancellationToken, ref hasAccessToTypeFromWorkspaceAssemblies);
                    var added = namedTypesToAccessedTypesMap.TryAdd(namedType, usedTypes);
                    Debug.Assert(added);
                }, SymbolKind.NamedType);
 
                compilationStartContext.RegisterCompilationEndAction(compilationEndContext =>
                {
                    if (diagnosticAnalyzerTypes.IsEmpty || !hasAccessToTypeFromWorkspaceAssemblies)
                    {
                        return;
                    }
 
                    var typesToProcess = new Queue<INamedTypeSymbol>();
                    var processedTypes = new HashSet<INamedTypeSymbol>();
                    var violatingTypeNamesBuilder = new SortedSet<string>();
                    var violatingUsedTypeNamesBuilder = new SortedSet<string>();
                    foreach (INamedTypeSymbol declaredType in namedTypesToAccessedTypesMap.Keys)
                    {
                        if (!diagnosticAnalyzerTypes.Contains(declaredType))
                        {
                            continue;
                        }
 
                        typesToProcess.Clear();
                        processedTypes.Clear();
                        violatingTypeNamesBuilder.Clear();
                        violatingUsedTypeNamesBuilder.Clear();
                        typesToProcess.Enqueue(declaredType);
                        do
                        {
                            var typeToProcess = typesToProcess.Dequeue();
                            Debug.Assert(SymbolEqualityComparer.Default.Equals(typeToProcess.ContainingAssembly, declaredType.ContainingAssembly));
                            Debug.Assert(namedTypesToAccessedTypesMap.ContainsKey(typeToProcess));
 
                            foreach (INamedTypeSymbol usedType in namedTypesToAccessedTypesMap[typeToProcess])
                            {
                                if (usedType.ContainingAssembly != null &&
                                    s_WorkspaceAssemblyNames.Contains(usedType.ContainingAssembly.Name))
                                {
                                    violatingTypeNamesBuilder.Add(usedType.ToDisplayString());
                                    violatingUsedTypeNamesBuilder.Add(typeToProcess.ToDisplayString());
                                }
 
                                if (!processedTypes.Contains(usedType) && namedTypesToAccessedTypesMap.ContainsKey(usedType))
                                {
                                    typesToProcess.Enqueue(usedType);
                                }
                            }
 
                            processedTypes.Add(typeToProcess);
                        } while (typesToProcess.Count != 0);
 
                        if (violatingTypeNamesBuilder.Count > 0)
                        {
                            string[] args;
                            DiagnosticDescriptor rule;
                            if (violatingUsedTypeNamesBuilder.Count == 1 && violatingUsedTypeNamesBuilder.Single() == declaredType.ToDisplayString())
                            {
                                // Change diagnostic analyzer type '{0}' to remove all direct accesses to type(s) '{1}'
                                rule = DoNotUseTypesFromAssemblyDirectRule;
                                args = new[]
                                {
                                    declaredType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
                                    string.Join(", ", violatingTypeNamesBuilder)
                                };
                            }
                            else
                            {
                                // Change diagnostic analyzer type '{0}' to remove all direct and/or indirect accesses to type(s) '{1}', which access type(s) '{2}'
                                rule = DoNotUseTypesFromAssemblyIndirectRule;
                                args = new[]
                                {
                                    declaredType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
                                    string.Join(", ", violatingUsedTypeNamesBuilder),
                                    string.Join(", ", violatingTypeNamesBuilder)
                                };
                            }
 
                            Diagnostic diagnostic = declaredType.CreateDiagnostic(rule, args);
                            compilationEndContext.ReportDiagnostic(diagnostic);
                        }
                    }
                });
            });
        }
 
        private ImmutableHashSet<INamedTypeSymbol> GetUsedNamedTypes(INamedTypeSymbol namedType, Compilation compilation, CancellationToken cancellationToken, ref bool hasAccessToTypeFromWorkspaceAssemblies)
        {
            var builder = PooledHashSet<INamedTypeSymbol>.GetInstance();
            foreach (var decl in namedType.DeclaringSyntaxReferences)
            {
                var syntax = decl.GetSyntax(cancellationToken);
 
                // GetSyntax for VB returns the StatementSyntax instead of BlockSyntax node.
                syntax = syntax.FirstAncestorOrSelf<SyntaxNode>(IsNamedTypeDeclarationBlock, ascendOutOfTrivia: false) ?? syntax;
 
#pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
                var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
#pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
                var nodesToProcess = new Queue<(SyntaxNode node, bool inExecutableCode)>();
                nodesToProcess.Enqueue((node: syntax, inExecutableCode: false));
 
                do
                {
                    (SyntaxNode node, bool inExecutableCode) nodeToProcess = nodesToProcess.Dequeue();
                    var node = nodeToProcess.node;
                    var inExecutableCode = nodeToProcess.inExecutableCode;
                    if (!inExecutableCode && !ReferenceEquals(node, syntax) && IsNamedTypeDeclarationBlock(node))
                    {
                        // Skip type member declarations.
                        continue;
                    }
 
                    if (node is TTypeSyntax typeSyntax)
                    {
                        var typeInfo = semanticModel.GetTypeInfo(typeSyntax, cancellationToken);
                        AddUsedNamedTypeCore(typeInfo.Type, builder, ref hasAccessToTypeFromWorkspaceAssemblies);
                    }
 
                    if (!inExecutableCode)
                    {
                        var operationBlock = semanticModel.GetOperation(node, cancellationToken);
                        if (operationBlock != null)
                        {
                            // Add used types within executable code in the operation tree.
                            inExecutableCode = true;
                            foreach (var operation in operationBlock.DescendantsAndSelf())
                            {
                                AddUsedNamedTypeCore(operation.Type, builder, ref hasAccessToTypeFromWorkspaceAssemblies);
 
                                // Handle static member accesses specially as there is no operation for static type off which the member is accessed.
                                if (operation is IMemberReferenceOperation memberReference &&
                                    memberReference.Member.IsStatic)
                                {
                                    AddUsedNamedTypeCore(memberReference.Member.ContainingType, builder, ref hasAccessToTypeFromWorkspaceAssemblies);
                                }
                                else if (operation is IInvocationOperation invocation &&
                                    (invocation.TargetMethod.IsStatic || invocation.TargetMethod.IsExtensionMethod))
                                {
                                    AddUsedNamedTypeCore(invocation.TargetMethod.ContainingType, builder, ref hasAccessToTypeFromWorkspaceAssemblies);
                                }
                            }
                        }
                    }
 
                    foreach (var child in node.ChildNodes())
                    {
                        nodesToProcess.Enqueue((child, inExecutableCode));
                    }
                } while (nodesToProcess.Count != 0);
            }
 
            return builder.ToImmutableAndFree();
        }
 
        private static void AddUsedNamedTypeCore(ITypeSymbol? type, PooledHashSet<INamedTypeSymbol> builder, ref bool hasAccessToTypeFromWorkspaceAssemblies)
        {
            if (type is INamedTypeSymbol usedType &&
                usedType.TypeKind != TypeKind.Error)
            {
                builder.Add(usedType);
 
                if (!hasAccessToTypeFromWorkspaceAssemblies &&
                    usedType.ContainingAssembly != null &&
                    s_WorkspaceAssemblyNames.Contains(usedType.ContainingAssembly.Name))
                {
                    hasAccessToTypeFromWorkspaceAssemblies = true;
                }
 
                if (usedType.IsGenericType)
                {
                    foreach (var typeArgument in usedType.TypeArguments)
                    {
                        AddUsedNamedTypeCore(typeArgument, builder, ref hasAccessToTypeFromWorkspaceAssemblies);
                    }
                }
            }
        }
    }
}