File: src\Analyzers\Core\Analyzers\RemoveUnnecessaryImports\AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
 
internal abstract class AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer<TSyntaxNode> :
    AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
    where TSyntaxNode : SyntaxNode
{
    // NOTE: This is a special helper diagnostic ID which is reported when the remove unnecesssary diagnostic ID (IDE0005) is
    // ecalated to a warning or an error, but 'GenerateDocumentationFile' is false, which leads to IDE0005 not being reported
    // on command line builds. See https://github.com/dotnet/roslyn/issues/41640 for more details.
    internal const string EnableGenerateDocumentationFileId = "EnableGenerateDocumentationFile";
 
    // The NotConfigurable custom tag ensures that user can't turn this diagnostic into a warning / error via
    // ruleset editor or solution explorer. Setting messageFormat to empty string ensures that we won't display
    // this diagnostic in the preview pane header.
    private static readonly DiagnosticDescriptor s_fixableIdDescriptor = CreateDescriptorWithId(
        RemoveUnnecessaryImportsConstants.DiagnosticFixableId, EnforceOnBuild.Never, hasAnyCodeStyleOption: false, "", "", isConfigurable: false);
 
#pragma warning disable RS0030 // Do not used banned APIs - Special diagnostic with 'Warning' default severity.
    private static readonly DiagnosticDescriptor s_enableGenerateDocumentationFileIdDescriptor = new(
        EnableGenerateDocumentationFileId,
        title: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true,
        messageFormat: AnalyzersResources.Set_MSBuild_Property_GenerateDocumentationFile_to_true_in_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build,
        category: DiagnosticCategory.Style,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        helpLinkUri: "https://github.com/dotnet/roslyn/issues/41640",
        description: AnalyzersResources.Add_the_following_PropertyGroup_to_your_MSBuild_project_file_to_enable_IDE0005_Remove_unnecessary_usings_imports_on_build,
        customTags: [.. DiagnosticCustomTags.Microsoft, EnforceOnBuild.Never.ToCustomTag()]);
#pragma warning restore RS0030 // Do not used banned APIs
 
    private readonly DiagnosticDescriptor _classificationIdDescriptor;
    private readonly DiagnosticDescriptor _generatedCodeClassificationIdDescriptor;
 
    protected AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer(LocalizableString titleAndMessage)
        : base(GetDescriptors(titleAndMessage, out var classificationIdDescriptor, out var generatedCodeClassificationIdDescriptor))
    {
        _classificationIdDescriptor = classificationIdDescriptor;
        _generatedCodeClassificationIdDescriptor = generatedCodeClassificationIdDescriptor;
    }
 
    private static ImmutableArray<DiagnosticDescriptor> GetDescriptors(LocalizableString titleAndMessage, out DiagnosticDescriptor classificationIdDescriptor, out DiagnosticDescriptor generatedCodeClassificationIdDescriptor)
    {
        classificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, EnforceOnBuildValues.RemoveUnnecessaryImports, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true);
        generatedCodeClassificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsGeneratedCodeDiagnosticId, EnforceOnBuild.Never, hasAnyCodeStyleOption: false, titleAndMessage, isUnnecessary: true, isConfigurable: false);
        return
        [
            s_fixableIdDescriptor,
            s_enableGenerateDocumentationFileIdDescriptor,
            classificationIdDescriptor,
            generatedCodeClassificationIdDescriptor,
        ];
    }
 
    protected abstract ISyntaxFacts SyntaxFacts { get; }
    protected abstract ImmutableArray<SyntaxNode> MergeImports(ImmutableArray<TSyntaxNode> unnecessaryImports);
    protected abstract bool IsRegularCommentOrDocComment(SyntaxTrivia trivia);
    protected abstract IUnnecessaryImportsProvider<TSyntaxNode> UnnecessaryImportsProvider { get; }
 
    protected abstract SyntaxToken? TryGetLastToken(SyntaxNode node);
 
    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterSemanticModelAction(AnalyzeSemanticModel);
        context.RegisterCompilationAction(AnalyzeCompilation);
    }
 
    private void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
    {
        if (ShouldSkipAnalysis(context, notification: null))
            return;
 
        var tree = context.SemanticModel.SyntaxTree;
        var cancellationToken = context.CancellationToken;
 
        var unnecessaryImports = UnnecessaryImportsProvider.GetUnnecessaryImports(context.SemanticModel, context.FilterSpan, cancellationToken);
        if (unnecessaryImports.Any())
        {
            // The IUnnecessaryImportsService will return individual import pieces that
            // need to be removed.  For example, it will return individual import-clauses
            // from VB.  However, we want to mark the entire import statement if we are
            // going to remove all the clause.  Defer to our subclass to stitch this up
            // for us appropriately.
            var mergedImports = MergeImports(unnecessaryImports);
 
            var descriptor = GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, cancellationToken)
                ? _generatedCodeClassificationIdDescriptor
                : _classificationIdDescriptor;
            var contiguousSpans = GetContiguousSpans(mergedImports);
            var diagnostics =
                CreateClassificationDiagnostics(contiguousSpans, tree, descriptor, cancellationToken).Concat(
                CreateFixableDiagnostics(mergedImports, tree, cancellationToken));
 
            foreach (var diagnostic in diagnostics)
            {
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
 
    private void AnalyzeCompilation(CompilationAnalysisContext context)
    {
        // Due to https://github.com/dotnet/roslyn/issues/41640, enabling this analyzer (IDE0005) on build requires users
        // to enable generation of XML documentation comments. We detect if generation of XML documentation comments
        // is disabled for this tree and IDE0005 diagnostics are being reported with effective severity "Warning" or "Error".
        // If so, we report a special diagnostic that recommends the users to set "GenerateDocumentationFile" to "true"
        // in their project file to enable IDE0005 on build.
 
        var compilation = context.Compilation;
        if (!IsAnalysisLevelGreaterThanOrEquals(8, context.Options))
            return;
 
        var tree = compilation.SyntaxTrees.FirstOrDefault(tree => !GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, context.CancellationToken));
        if (tree is null || tree.Options.DocumentationMode != DocumentationMode.None)
            return;
 
        if (ShouldSkipAnalysis(tree, context.Options, compilation.Options, notification: null, context.CancellationToken))
            return;
 
        var effectiveSeverity = _classificationIdDescriptor.GetEffectiveSeverity(compilation.Options, tree, context.Options);
        if (effectiveSeverity is ReportDiagnostic.Warn or ReportDiagnostic.Error)
        {
            context.ReportDiagnostic(Diagnostic.Create(s_enableGenerateDocumentationFileIdDescriptor, Location.None));
        }
    }
 
    private IEnumerable<TextSpan> GetContiguousSpans(ImmutableArray<SyntaxNode> nodes)
    {
        var syntaxFacts = this.SyntaxFacts;
        (SyntaxNode node, TextSpan textSpan)? previous = null;
 
        // Sort the nodes in source location order.
        foreach (var node in nodes.OrderBy(n => n.SpanStart))
        {
            TextSpan textSpan;
            var nodeEnd = GetEnd(node);
            if (previous == null)
            {
                textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd);
            }
            else
            {
                var lastToken = TryGetLastToken(previous.Value.node) ?? previous.Value.node.GetLastToken();
                if (lastToken.GetNextToken(includeDirectives: true) == node.GetFirstToken())
                {
                    // Expand the span
                    textSpan = TextSpan.FromBounds(previous.Value.textSpan.Start, nodeEnd);
                }
                else
                {
                    // Return the last span, and start a new one
                    yield return previous.Value.textSpan;
                    textSpan = TextSpan.FromBounds(node.Span.Start, nodeEnd);
                }
            }
 
            previous = (node, textSpan);
        }
 
        if (previous.HasValue)
            yield return previous.Value.textSpan;
 
        yield break;
 
        int GetEnd(SyntaxNode node)
        {
            var end = node.Span.End;
            foreach (var trivia in node.GetTrailingTrivia())
            {
                if (syntaxFacts.IsRegularComment(trivia))
                    end = trivia.Span.End;
            }
 
            return end;
        }
    }
 
    // Create one diagnostic for each unnecessary span that will be classified as Unnecessary
    private static IEnumerable<Diagnostic> CreateClassificationDiagnostics(
        IEnumerable<TextSpan> contiguousSpans, SyntaxTree tree,
        DiagnosticDescriptor descriptor, CancellationToken cancellationToken)
    {
        foreach (var span in contiguousSpans)
        {
            if (tree.OverlapsHiddenPosition(span, cancellationToken))
            {
                continue;
            }
 
            yield return Diagnostic.Create(descriptor, tree.GetLocation(span));
        }
    }
 
    protected abstract IEnumerable<TextSpan> GetFixableDiagnosticSpans(
        IEnumerable<SyntaxNode> nodes, SyntaxTree tree, CancellationToken cancellationToken);
 
    private IEnumerable<Diagnostic> CreateFixableDiagnostics(
        IEnumerable<SyntaxNode> nodes, SyntaxTree tree, CancellationToken cancellationToken)
    {
        var spans = GetFixableDiagnosticSpans(nodes, tree, cancellationToken);
 
        foreach (var span in spans)
            yield return Diagnostic.Create(s_fixableIdDescriptor, tree.GetLocation(span));
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;
}