File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Extensions\DiagnosticDescriptorExtensions.cs
Web Access
Project: src\src\Tools\BuildActionTelemetryTable\BuildActionTelemetryTable.csproj (BuildActionTelemetryTable)
// 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.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Shared.Extensions;
 
internal static class DiagnosticDescriptorExtensions
{
    private const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic";
    private const string DotnetDiagnosticPrefix = "dotnet_diagnostic";
    private const string CategoryPrefix = "category";
    private const string SeveritySuffix = "severity";
 
    private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix;
 
    public static ImmutableArray<string> ImmutableCustomTags(this DiagnosticDescriptor descriptor)
    {
        Debug.Assert(descriptor.CustomTags is ImmutableArray<string>);
        return (ImmutableArray<string>)descriptor.CustomTags;
    }
 
    /// <summary>
    /// Gets project-level effective severity of the given <paramref name="descriptor"/> accounting for severity configurations from both the following sources:
    /// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc.
    /// 2. Analyzer config documents at the project root directory or in ancestor directories.
    /// </summary>
    public static ReportDiagnostic GetEffectiveSeverity(
        this DiagnosticDescriptor descriptor,
        CompilationOptions compilationOptions,
        AnalyzerConfigOptions? analyzerOptions,
        ImmutableDictionary<string, ReportDiagnostic>? treeOptions)
    {
        var effectiveSeverity = descriptor.GetEffectiveSeverity(compilationOptions);
 
        // Apply analyzer config options, unless configured with a non-default value in compilation options.
        // Note that compilation options (/nowarn, /warnaserror) override analyzer config options.
        if (treeOptions != null && analyzerOptions != null &&
            (!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) ||
             reportDiagnostic == ReportDiagnostic.Default))
        {
            if (treeOptions.TryGetValue(descriptor.Id, out reportDiagnostic) && reportDiagnostic != ReportDiagnostic.Default ||
                TryGetSeverityFromBulkConfiguration(descriptor, analyzerOptions, out reportDiagnostic))
            {
                Debug.Assert(reportDiagnostic != ReportDiagnostic.Default);
                effectiveSeverity = reportDiagnostic;
            }
        }
 
        return effectiveSeverity;
    }
 
    /// <summary>
    /// Gets document-level effective severity of the given <paramref name="descriptor"/> accounting for severity configurations from both the following sources:
    /// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc.
    /// 2. Analyzer config documents at the document root directory or in ancestor directories.
    /// </summary>
    public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions compilationOptions, SyntaxTree tree, AnalyzerOptions analyzerOptions)
    {
        var effectiveSeverity = descriptor.GetEffectiveSeverity(compilationOptions);
 
        // Apply analyzer config options, unless configured with a non-default value in compilation options.
        // Note that compilation options (/nowarn, /warnaserror) override analyzer config options.
        if (!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) ||
            reportDiagnostic == ReportDiagnostic.Default)
        {
            // First check for tree-level analyzer config options.
            var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree);
            var providerAndTree = compilationOptions.SyntaxTreeOptionsProvider != null
                ? (compilationOptions.SyntaxTreeOptionsProvider, tree)
                : default;
            var severityInEditorConfig = descriptor.GetEffectiveSeverity(analyzerConfigOptions, providerAndTree);
            if (severityInEditorConfig != ReportDiagnostic.Default)
            {
                effectiveSeverity = severityInEditorConfig;
            }
            else
            {
                // If not found, check for global analyzer config options.
                var severityInGlobalConfig = descriptor.GetEffectiveSeverity(analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions, providerAndTree);
                if (severityInGlobalConfig != ReportDiagnostic.Default)
                {
                    effectiveSeverity = severityInGlobalConfig;
                }
            }
        }
 
        return effectiveSeverity;
    }
 
    public static bool IsDefinedInEditorConfig(this DiagnosticDescriptor descriptor, AnalyzerConfigOptions analyzerConfigOptions)
    {
        // Check if the option is defined explicitly in the editorconfig
        var diagnosticKey = $"{DotnetDiagnosticPrefix}.{descriptor.Id}.{SeveritySuffix}";
        if (analyzerConfigOptions.TryGetValue(diagnosticKey, out var value) &&
            EditorConfigSeverityStrings.TryParse(value, out var severity))
        {
            return true;
        }
 
        // Check if the option is defined as part of a bulk configuration
        // Analyzer bulk configuration does not apply to:
        //  1. Disabled by default diagnostics
        //  2. Compiler diagnostics
        //  3. Non-configurable diagnostics
        if (!descriptor.IsEnabledByDefault ||
            descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
        {
            return false;
        }
 
        // If user has explicitly configured default severity for the diagnostic category, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
        var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
        if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return true;
        }
 
        // Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.severity = error'
        if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return true;
        }
 
        // option not defined in editorconfig, assumed to be the default
        return false;
    }
 
    /// <summary>
    /// Gets the effective diagnostic severity for the diagnostic ID corresponding to the
    /// given <paramref name="descriptor"/> by looking up the severity settings in the options.
    /// If the provided options are specific to a particular tree, provide a non-null value
    /// for <paramref name="providerAndTree"/> to look up tree specific severity options.
    /// </summary>
    public static ReportDiagnostic GetEffectiveSeverity(
        this DiagnosticDescriptor descriptor,
        AnalyzerConfigOptions analyzerConfigOptions,
        (SyntaxTreeOptionsProvider provider, SyntaxTree tree)? providerAndTree = null)
    {
        ReportDiagnostic severity;
        string? value;
 
        // Check if the option is defined explicitly in the editorconfig
        if (providerAndTree.HasValue)
        {
            var provider = providerAndTree.Value.provider;
            var tree = providerAndTree.Value.tree;
            if (provider.TryGetDiagnosticValue(tree, descriptor.Id, CancellationToken.None, out severity) ||
                provider.TryGetGlobalDiagnosticValue(descriptor.Id, CancellationToken.None, out severity))
            {
                return severity;
            }
        }
        else
        {
            var diagnosticKey = $"{DotnetDiagnosticPrefix}.{descriptor.Id}.{SeveritySuffix}";
            if (analyzerConfigOptions.TryGetValue(diagnosticKey, out value) &&
                EditorConfigSeverityStrings.TryParse(value, out severity))
            {
                return severity;
            }
        }
 
        // Check if the option is defined as part of a bulk configuration
        // Analyzer bulk configuration does not apply to:
        //  1. Disabled by default diagnostics
        //  2. Compiler diagnostics
        //  3. Non-configurable diagnostics
        if (!descriptor.IsEnabledByDefault ||
            descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
        {
            return ReportDiagnostic.Default;
        }
 
        // If user has explicitly configured default severity for the diagnostic category, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
        var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
        if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return severity;
        }
 
        // Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.severity = error'
        if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return severity;
        }
 
        // option not defined in editorconfig, assumed to be the default
        return ReportDiagnostic.Default;
    }
 
    /// <summary>
    /// Tries to get configured severity for the given <paramref name="descriptor"/>
    /// from bulk configuration analyzer config options, i.e.
    ///     'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%'
    ///         or
    ///     'dotnet_analyzer_diagnostic.severity = %severity%'
    /// Docs: https://docs.microsoft.com/visualstudio/code-quality/use-roslyn-analyzers?view=vs-2019#set-rule-severity-of-multiple-analyzer-rules-at-once-in-an-editorconfig-file for details
    /// </summary>
    private static bool TryGetSeverityFromBulkConfiguration(
        DiagnosticDescriptor descriptor,
        AnalyzerConfigOptions analyzerOptions,
        out ReportDiagnostic severity)
    {
        // Analyzer bulk configuration does not apply to:
        //  1. Disabled by default diagnostics
        //  2. Compiler diagnostics
        //  3. Non-configurable diagnostics
        if (!descriptor.IsEnabledByDefault ||
            descriptor.ImmutableCustomTags().Any(static tag => tag is WellKnownDiagnosticTags.Compiler or WellKnownDiagnosticTags.NotConfigurable))
        {
            severity = default;
            return false;
        }
 
        // If user has explicitly configured default severity for the diagnostic category, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
        var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}";
        if (analyzerOptions.TryGetValue(categoryBasedKey, out var value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return true;
        }
 
        // Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
        // For example, 'dotnet_analyzer_diagnostic.severity = error'
        if (analyzerOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
            EditorConfigSeverityStrings.TryParse(value, out severity))
        {
            return true;
        }
 
        severity = default;
        return false;
    }
 
    public static bool IsCompilationEnd(this DiagnosticDescriptor descriptor)
        => descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.CompilationEnd);
 
    // TODO: the value stored in descriptor should already be valid URI (https://github.com/dotnet/roslyn/issues/59205)
    internal static Uri? GetValidHelpLinkUri(this DiagnosticDescriptor descriptor)
       => Uri.TryCreate(descriptor.HelpLinkUri, UriKind.Absolute, out var uri) &&
          (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) ? uri : null;
 
    public static DiagnosticDescriptor WithMessageFormat(this DiagnosticDescriptor descriptor, LocalizableString messageFormat)
    {
#pragma warning disable RS0030 // Do not used banned APIs - DiagnosticDescriptor .ctor is banned in this project, but fine to use here.
        return new DiagnosticDescriptor(descriptor.Id, descriptor.Title, messageFormat,
            descriptor.Category, descriptor.DefaultSeverity, descriptor.IsEnabledByDefault,
            descriptor.Description, descriptor.HelpLinkUri, descriptor.CustomTags.ToArray());
#pragma warning restore RS0030 // Do not used banned APIs
    }
}