File: CSharpRemoveUnnecessaryImportsService.Rewriter.cs
Web Access
Project: ..\..\..\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.
 
#nullable disable
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Helpers.RemoveUnnecessaryImports;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports
{
    internal partial class CSharpRemoveUnnecessaryImportsService
    {
        private class Rewriter : CSharpSyntaxRewriter
        {
            private readonly ISet<UsingDirectiveSyntax> _unnecessaryUsingsDoNotAccessDirectly;
            private readonly CancellationToken _cancellationToken;
 
            public Rewriter(
                ISet<UsingDirectiveSyntax> unnecessaryUsings,
                CancellationToken cancellationToken)
                : base(visitIntoStructuredTrivia: true)
            {
                _unnecessaryUsingsDoNotAccessDirectly = unnecessaryUsings;
                _cancellationToken = cancellationToken;
            }
 
            public override SyntaxNode DefaultVisit(SyntaxNode node)
            {
                _cancellationToken.ThrowIfCancellationRequested();
                return base.DefaultVisit(node);
            }
 
            private static void ProcessUsings(
                SyntaxList<UsingDirectiveSyntax> usings,
                ISet<UsingDirectiveSyntax> usingsToRemove,
                out SyntaxList<UsingDirectiveSyntax> finalUsings,
                out SyntaxTriviaList finalTrivia)
            {
                var currentUsings = new List<UsingDirectiveSyntax>(usings);
                var firstUsingNotBeingRemoved = true;
                var passedLeadngTrivia = false;
 
                finalTrivia = default;
                for (var i = 0; i < usings.Count; i++)
                {
                    if (usingsToRemove.Contains(usings[i]))
                    {
                        var currentUsing = currentUsings[i];
                        currentUsings[i] = null;
 
                        var leadingTrivia = currentUsing.GetLeadingTrivia();
                        // We always preserve trivia on the first using in a file scoped namespace
                        if (ShouldPreserveTrivia(leadingTrivia) ||
                            (i == 0 && currentUsing.IsParentKind(SyntaxKind.FileScopedNamespaceDeclaration)))
                        {
                            // This using had trivia we want to preserve.  If we're the last
                            // directive, then copy this trivia out so that our caller can place
                            // it on the next token.  If there is any directive following us,
                            // then place it on that.
                            if (i < usings.Count - 1)
                            {
                                var nextIndex = i + 1;
                                var nextUsing = currentUsings[nextIndex];
 
                                if (ShouldPreserveTrivia(nextUsing.GetLeadingTrivia()))
                                {
                                    // If we need to preserve the next trivia too then, prepend
                                    // the two together.
                                    currentUsings[nextIndex] = nextUsing.WithPrependedLeadingTrivia(leadingTrivia);
                                }
                                else
                                {
                                    // Otherwise, replace the next trivia with this trivia that we
                                    // want to preserve.
                                    currentUsings[nextIndex] = nextUsing.WithLeadingTrivia(leadingTrivia);
                                }
 
                                passedLeadngTrivia = true;
                            }
                            else
                            {
                                finalTrivia = leadingTrivia;
                            }
                        }
                    }
                    else if (firstUsingNotBeingRemoved)
                    {
                        // 1) We only apply this logic for not first using, that is saved:
                        // ===================
                        // namespace N;
                        //
                        // using System; <- if we save this using, we don't need to cut leading lines
                        // ===================
                        // 2) If leading trivia was saved from the previous using, that was removed,
                        // we don't bother cutting blank lines as well:
                        // ===================
                        // namespace N;
                        //
                        // using System; <- need to delete this using
                        // using System.Collections.Generic; <- this using is saved, no need to eat the line,
                        // otherwise https://github.com/dotnet/roslyn/issues/58972 will happen
                        if (i > 0 && !passedLeadngTrivia)
                        {
                            var currentUsing = currentUsings[i];
                            var currentUsingLeadingTrivia = currentUsing.GetLeadingTrivia();
                            currentUsings[i] = currentUsing.WithLeadingTrivia(currentUsingLeadingTrivia.WithoutLeadingBlankLines());
                        }
 
                        firstUsingNotBeingRemoved = false;
                    }
                }
 
                finalUsings = currentUsings.WhereNotNull().ToSyntaxList();
            }
 
            private static bool ShouldPreserveTrivia(SyntaxTriviaList trivia)
                => trivia.Any(t => !t.IsWhitespaceOrEndOfLine());
 
            private ISet<UsingDirectiveSyntax> GetUsingsToRemove(
                SyntaxList<UsingDirectiveSyntax> oldUsings,
                SyntaxList<UsingDirectiveSyntax> newUsings)
            {
                Debug.Assert(oldUsings.Count == newUsings.Count);
 
                var result = new HashSet<UsingDirectiveSyntax>();
                for (var i = 0; i < oldUsings.Count; i++)
                {
                    if (_unnecessaryUsingsDoNotAccessDirectly.Contains(oldUsings[i]))
                    {
                        result.Add(newUsings[i]);
                    }
                }
 
                return result;
            }
 
            public override SyntaxNode VisitCompilationUnit(CompilationUnitSyntax node)
            {
                var compilationUnit = (CompilationUnitSyntax)base.VisitCompilationUnit(node);
 
                var usingsToRemove = GetUsingsToRemove(node.Usings, compilationUnit.Usings);
                if (usingsToRemove.Count == 0)
                {
                    return compilationUnit;
                }
 
                ProcessUsings(compilationUnit.Usings, usingsToRemove, out var finalUsings, out var finalTrivia);
 
                // If there was any left over trivia, then attach it to the next token that
                // follows the usings.
                if (finalTrivia.Count > 0)
                {
                    var nextToken = compilationUnit.Usings.Last().GetLastToken().GetNextTokenOrEndOfFile();
                    compilationUnit = compilationUnit.ReplaceToken(nextToken, nextToken.WithPrependedLeadingTrivia(finalTrivia));
                }
 
                var resultCompilationUnit = compilationUnit.WithUsings(finalUsings);
                if (finalUsings.Count == 0 &&
                    resultCompilationUnit.Externs.Count == 0 &&
                    resultCompilationUnit.Members.Count >= 1)
                {
                    // We've removed all the usings and now the first thing in the namespace is a
                    // type.  In this case, remove any newlines preceding the type.
                    var firstToken = resultCompilationUnit.GetFirstToken();
                    var newFirstToken = RemoveUnnecessaryImportsHelpers.StripNewLines(CSharpSyntaxFacts.Instance, firstToken);
                    resultCompilationUnit = resultCompilationUnit.ReplaceToken(firstToken, newFirstToken);
                }
 
                return resultCompilationUnit;
            }
 
            public override SyntaxNode VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax node)
                => VisitBaseNamespaceDeclaration(node, (BaseNamespaceDeclarationSyntax)base.VisitFileScopedNamespaceDeclaration(node));
 
            public override SyntaxNode VisitNamespaceDeclaration(NamespaceDeclarationSyntax node)
                => VisitBaseNamespaceDeclaration(node, (BaseNamespaceDeclarationSyntax)base.VisitNamespaceDeclaration(node));
 
            private SyntaxNode VisitBaseNamespaceDeclaration(
                BaseNamespaceDeclarationSyntax node,
                BaseNamespaceDeclarationSyntax namespaceDeclaration)
            {
                var usingsToRemove = GetUsingsToRemove(node.Usings, namespaceDeclaration.Usings);
                if (usingsToRemove.Count == 0)
                    return namespaceDeclaration;
 
                ProcessUsings(namespaceDeclaration.Usings, usingsToRemove, out var finalUsings, out var finalTrivia);
 
                // If there was any left over trivia, then attach it to the next token that
                // follows the usings.
                if (finalTrivia.Count > 0)
                {
                    var nextToken = namespaceDeclaration.Usings.Last().GetLastToken().GetNextToken();
                    namespaceDeclaration = namespaceDeclaration.ReplaceToken(nextToken, nextToken.WithPrependedLeadingTrivia(finalTrivia));
                }
 
                var resultNamespace = namespaceDeclaration.WithUsings(finalUsings);
                if (finalUsings.Count == 0 &&
                    resultNamespace.Externs.Count == 0 &&
                    resultNamespace.Members.Count >= 1)
                {
                    // We've removed all the usings and now the first thing in the namespace is a
                    // type.  In this case, remove any newlines preceding the type.
                    var firstToken = resultNamespace.Members.First().GetFirstToken();
                    var newFirstToken = RemoveUnnecessaryImportsHelpers.StripNewLines(CSharpSyntaxFacts.Instance, firstToken);
                    resultNamespace = resultNamespace.ReplaceToken(firstToken, newFirstToken);
                }
 
                return resultNamespace;
            }
        }
    }
}