File: MetaAnalyzers\DiagnosticAnalyzerAttributeAnalyzer.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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers
{
    using static CodeAnalysisDiagnosticsResources;
 
    /// <summary>
    /// RS1001: <inheritdoc cref="MissingDiagnosticAnalyzerAttributeTitle"/>
    /// RS1004: <inheritdoc cref="AddLanguageSupportToAnalyzerTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class DiagnosticAnalyzerAttributeAnalyzer : DiagnosticAnalyzerCorrectnessAnalyzer
    {
        public static readonly DiagnosticDescriptor MissingDiagnosticAnalyzerAttributeRule = new(
            DiagnosticIds.MissingDiagnosticAnalyzerAttributeRuleId,
            CreateLocalizableResourceString(nameof(MissingDiagnosticAnalyzerAttributeTitle)),
            CreateLocalizableResourceString(nameof(MissingAttributeMessage), WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsDiagnosticAnalyzerAttribute),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: CreateLocalizableResourceString(nameof(MissingDiagnosticAnalyzerAttributeDescription)),
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor AddLanguageSupportToAnalyzerRule = new(
           DiagnosticIds.AddLanguageSupportToAnalyzerRuleId,
           CreateLocalizableResourceString(nameof(AddLanguageSupportToAnalyzerTitle)),
           CreateLocalizableResourceString(nameof(AddLanguageSupportToAnalyzerMessage)),
           DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
           DiagnosticSeverity.Warning,
           isEnabledByDefault: true,
           description: CreateLocalizableResourceString(nameof(AddLanguageSupportToAnalyzerDescription)),
           customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(MissingDiagnosticAnalyzerAttributeRule, AddLanguageSupportToAnalyzerRule);
 
#pragma warning disable RS1025 // Configure generated code analysis
        public override void Initialize(AnalysisContext context)
#pragma warning restore RS1025 // Configure generated code analysis
        {
            context.EnableConcurrentExecution();
 
            base.Initialize(context);
        }
 
        [SuppressMessage("AnalyzerPerformance", "RS1012:Start action has no registered actions.", Justification = "Method returns an analyzer that is registered by the caller.")]
        protected override DiagnosticAnalyzerSymbolAnalyzer GetDiagnosticAnalyzerSymbolAnalyzer(CompilationStartAnalysisContext compilationContext, INamedTypeSymbol diagnosticAnalyzer, INamedTypeSymbol diagnosticAnalyzerAttribute)
        {
            var attributeUsageAttribute = compilationContext.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemAttributeUsageAttribute);
            return new AttributeAnalyzer(diagnosticAnalyzer, diagnosticAnalyzerAttribute, attributeUsageAttribute);
        }
 
        private sealed class AttributeAnalyzer : DiagnosticAnalyzerSymbolAnalyzer
        {
            private const string CSharpCompilationFullName = @"Microsoft.CodeAnalysis.CSharp.CSharpCompilation";
            private const string BasicCompilationFullName = @"Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation";
 
            private readonly INamedTypeSymbol? _attributeUsageAttribute;
 
            public AttributeAnalyzer(INamedTypeSymbol diagnosticAnalyzer, INamedTypeSymbol diagnosticAnalyzerAttribute, INamedTypeSymbol? attributeUsageAttribute)
                : base(diagnosticAnalyzer, diagnosticAnalyzerAttribute)
            {
                _attributeUsageAttribute = attributeUsageAttribute;
            }
 
            protected override void AnalyzeDiagnosticAnalyzer(SymbolAnalysisContext symbolContext)
            {
                var namedType = (INamedTypeSymbol)symbolContext.Symbol;
                if (namedType.IsAbstract)
                {
                    return;
                }
 
                // 1) MissingDiagnosticAnalyzerAttributeRule: DiagnosticAnalyzer has no DiagnosticAnalyzerAttribute.
                // 2) AddLanguageSupportToAnalyzerRule: For analyzer supporting only one of C# or VB languages, detect if it can support the other language.
 
                var hasAttribute = false;
                SyntaxNode? attributeSyntax = null;
                bool supportsCSharp = false;
                bool supportsVB = false;
 
                var namedTypeAttributes = namedType.GetApplicableAttributes(_attributeUsageAttribute);
 
                foreach (AttributeData attribute in namedTypeAttributes)
                {
                    if (attribute.AttributeClass.DerivesFrom(DiagnosticAnalyzerAttribute))
                    {
                        // Bail out for the case where analyzer type derives from a sub-type in different assembly, and the sub-type has the diagnostic analyzer attribute.
                        if (attribute.ApplicationSyntaxReference == null)
                        {
                            return;
                        }
 
                        hasAttribute = true;
 
                        // The attribute constructor's signature is "(string, params string[])",
                        // so process both string arguments and string[] arguments.
                        foreach (TypedConstant arg in attribute.ConstructorArguments)
                        {
                            CheckLanguage(arg, ref supportsCSharp, ref supportsVB);
 
                            if (arg.Kind == TypedConstantKind.Array)
                            {
                                foreach (TypedConstant element in arg.Values)
                                {
                                    CheckLanguage(element, ref supportsCSharp, ref supportsVB);
                                }
                            }
                        }
 
                        attributeSyntax = attribute.ApplicationSyntaxReference.GetSyntax(symbolContext.CancellationToken);
                    }
                }
 
                if (!hasAttribute)
                {
                    Diagnostic diagnostic = namedType.CreateDiagnostic(MissingDiagnosticAnalyzerAttributeRule);
                    symbolContext.ReportDiagnostic(diagnostic);
                }
                else if (supportsCSharp ^ supportsVB)
                {
                    RoslynDebug.Assert(attributeSyntax != null);
 
                    // If the analyzer assembly doesn't reference either C# or VB CodeAnalysis assemblies,
                    // then the analyzer is pretty likely a language-agnostic analyzer.
                    Compilation compilation = symbolContext.Compilation;
                    string compilationTypeNameToCheck = supportsCSharp ? CSharpCompilationFullName : BasicCompilationFullName;
                    INamedTypeSymbol? compilationType = compilation.GetOrCreateTypeByMetadataName(compilationTypeNameToCheck);
                    if (compilationType == null)
                    {
                        string missingLanguage = supportsCSharp ? LanguageNames.VisualBasic : LanguageNames.CSharp;
                        Diagnostic diagnostic = attributeSyntax.CreateDiagnostic(AddLanguageSupportToAnalyzerRule, namedType.Name, missingLanguage);
                        symbolContext.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
 
        private static void CheckLanguage(TypedConstant argument, ref bool supportsCSharp, ref bool supportsVB)
        {
            if (argument.Kind == TypedConstantKind.Primitive &&
                argument.Type != null &&
                argument.Type.SpecialType == SpecialType.System_String)
            {
                var supportedLanguage = (string?)argument.Value;
                if (supportedLanguage == LanguageNames.CSharp)
                {
                    supportsCSharp = true;
                }
                else if (supportedLanguage == LanguageNames.VisualBasic)
                {
                    supportsVB = true;
                }
            }
        }
    }
}