File: AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\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.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.LanguageService;
 
namespace Microsoft.CodeAnalysis.RemoveUnnecessaryImports
{
    internal abstract class AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer<TSyntaxNode> :
        AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
        where TSyntaxNode : SyntaxNode
    {
        // 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, "", "", isConfigurable: false);
 
        private readonly DiagnosticDescriptor _classificationIdDescriptor;
        private readonly DiagnosticDescriptor _generatedCodeClassificationIdDescriptor;
 
        protected AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer(LocalizableString titleAndMessage)
            : base(GetDescriptors(titleAndMessage, out var classificationIdDescriptor, out var generatedCodeClassificationIdDescriptor), FadingOptions.FadeOutUnusedImports)
        {
            _classificationIdDescriptor = classificationIdDescriptor;
            _generatedCodeClassificationIdDescriptor = generatedCodeClassificationIdDescriptor;
        }
 
        private static ImmutableArray<DiagnosticDescriptor> GetDescriptors(LocalizableString titleAndMessage, out DiagnosticDescriptor classificationIdDescriptor, out DiagnosticDescriptor generatedCodeClassificationIdDescriptor)
        {
            classificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId, EnforceOnBuildValues.RemoveUnnecessaryImports, titleAndMessage, isUnnecessary: true);
            generatedCodeClassificationIdDescriptor = CreateDescriptorWithId(IDEDiagnosticIds.RemoveUnnecessaryImportsDiagnosticId + "_gen", EnforceOnBuild.Never, titleAndMessage, isUnnecessary: true, isConfigurable: false);
            return ImmutableArray.Create(s_fixableIdDescriptor, 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 override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics;
 
        protected abstract SyntaxToken? TryGetLastToken(SyntaxNode node);
 
        protected override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterSemanticModelAction(AnalyzeSemanticModel);
        }
 
        private void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
        {
            var tree = context.SemanticModel.SyntaxTree;
            var cancellationToken = context.CancellationToken;
            var language = context.SemanticModel.Language;
 
            var unnecessaryImports = UnnecessaryImportsProvider.GetUnnecessaryImports(context.SemanticModel, 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 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;
    }
}