File: Rename\SymbolicRenameLocations.ReferenceProcessing.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Rename
{
    /// <summary>
    /// A helper class that contains some of the methods and filters that must be used when
    /// processing the raw results from the FindReferences API.
    /// </summary>
    internal sealed partial class SymbolicRenameLocations
    {
        internal static class ReferenceProcessing
        {
            private static async Task<bool> ShouldIncludeSymbolAsync(
                ISymbol referencedSymbol, ISymbol originalSymbol, Solution solution, bool considerSymbolReferences, CancellationToken cancellationToken)
            {
                if (referencedSymbol.IsPropertyAccessor())
                {
                    return considerSymbolReferences;
                }
 
                if (referencedSymbol.Equals(originalSymbol))
                {
                    return true;
                }
 
                // Parameters of properties and methods can cascade to each other in
                // indexer scenarios.
                if (originalSymbol.Kind == SymbolKind.Parameter && referencedSymbol.Kind == SymbolKind.Parameter)
                {
                    return true;
                }
 
                // If the original symbol is a property, cascade to the backing field
                if (referencedSymbol.Kind == SymbolKind.Field && originalSymbol.Equals(((IFieldSymbol)referencedSymbol).AssociatedSymbol))
                {
                    return true;
                }
 
                // If the symbol doesn't actually exist in source, we never want to rename it
                if (referencedSymbol.IsImplicitlyDeclared)
                {
                    return considerSymbolReferences;
                }
 
                // We can cascade from members to other members only if the names match. The example
                // where the names might be different is explicit interface implementations in
                // Visual Basic and VB's identifiers are case insensitive. 
                // Do not cascade to symbols that are defined only in metadata.
                if (referencedSymbol.Kind == originalSymbol.Kind &&
                    string.Compare(TrimNameToAfterLastDot(referencedSymbol.Name), TrimNameToAfterLastDot(originalSymbol.Name), StringComparison.OrdinalIgnoreCase) == 0 &&
                    referencedSymbol.Locations.Any(static loc => loc.IsInSource))
                {
                    return true;
                }
 
                // If the original symbol is an alias, then the referenced symbol will be where we
                // actually see references.
                if (originalSymbol.Kind == SymbolKind.Alias)
                {
                    var target = ((IAliasSymbol)originalSymbol).Target;
 
                    switch (target)
                    {
                        case INamedTypeSymbol nt:
                            return nt.ConstructedFrom.Equals(referencedSymbol)
                                || IsConstructorForType(possibleConstructor: referencedSymbol, possibleType: nt);
 
                        case INamespaceOrTypeSymbol s:
                            return s.Equals(referencedSymbol);
 
                        default: return false;
                    }
                }
 
                // cascade from property accessor to property (someone in C# renames base.get_X, or the accessor override)
                if (await IsPropertyAccessorOrAnOverrideAsync(referencedSymbol, solution, cancellationToken).ConfigureAwait(false) ||
                    await IsPropertyAccessorOrAnOverrideAsync(originalSymbol, solution, cancellationToken).ConfigureAwait(false))
                {
                    return true;
                }
 
                // cascade from constructor to named type
                if (IsConstructorForType(possibleConstructor: referencedSymbol, possibleType: originalSymbol))
                {
                    return true;
                }
 
                if (referencedSymbol.ContainingSymbol != null &&
                    referencedSymbol.ContainingSymbol.Kind == SymbolKind.NamedType &&
                    ((INamedTypeSymbol)referencedSymbol.ContainingSymbol).TypeKind == TypeKind.Interface &&
                    !originalSymbol.ExplicitInterfaceImplementations().Any(static (s, referencedSymbol) => s.Equals(referencedSymbol), referencedSymbol))
                {
                    return true;
                }
 
                return false;
 
                // Local functions
                static bool IsConstructorForType(ISymbol possibleConstructor, ISymbol possibleType)
                {
                    return possibleConstructor.IsConstructor()
                        && possibleType is INamedTypeSymbol namedType
                        && Equals(possibleConstructor.ContainingType.ConstructedFrom, namedType.ConstructedFrom);
                }
            }
 
            private static async Task<bool> IsPropertyAccessorOrAnOverrideAsync(
                ISymbol symbol, Solution solution, CancellationToken cancellationToken)
            {
                var result = await RenameUtilities.TryGetPropertyFromAccessorOrAnOverrideAsync(
                    symbol, solution, cancellationToken).ConfigureAwait(false);
                return result != null;
            }
 
            private static string TrimNameToAfterLastDot(string name)
            {
                var position = name.LastIndexOf('.');
 
                if (position == -1)
                {
                    return name;
                }
                else
                {
                    return name[(position + 1)..];
                }
            }
 
            /// <summary>
            /// Given a ISymbol, returns the renameable locations for a given symbol.
            /// </summary>
            public static async Task<ImmutableArray<RenameLocation>> GetRenamableDefinitionLocationsAsync(
                ISymbol referencedSymbol, ISymbol originalSymbol, Solution solution, CancellationToken cancellationToken)
            {
                var shouldIncludeSymbol = await ShouldIncludeSymbolAsync(referencedSymbol, originalSymbol, solution, false, cancellationToken).ConfigureAwait(false);
                if (!shouldIncludeSymbol)
                {
                    return ImmutableArray<RenameLocation>.Empty;
                }
 
                // Namespaces are definitions and references all in one. Since every definition
                // location is also a reference, we'll ignore it's definitions.
                if (referencedSymbol.Kind == SymbolKind.Namespace)
                {
                    return ImmutableArray<RenameLocation>.Empty;
                }
 
                var results = ArrayBuilder<RenameLocation>.GetInstance();
 
                // If the original symbol was an alias, then the definitions will just be the
                // location of the alias, always
                if (originalSymbol.Kind == SymbolKind.Alias)
                {
                    var location = originalSymbol.Locations.Single();
                    AddRenameLocationIfNotGenerated(location);
                    return results.ToImmutableAndFree();
                }
 
                var isRenamableAccessor = await IsPropertyAccessorOrAnOverrideAsync(referencedSymbol, solution, cancellationToken).ConfigureAwait(false);
                foreach (var location in referencedSymbol.Locations)
                {
                    if (location.IsInSource)
                    {
                        AddRenameLocationIfNotGenerated(location, isRenamableAccessor);
                    }
                }
 
                // If we're renaming a named type, we'll also have to find constructors and
                // destructors declarations that match the name
                if (referencedSymbol.Kind == SymbolKind.NamedType && referencedSymbol.Locations.All(l => l.IsInSource))
                {
                    var firstLocation = referencedSymbol.Locations[0];
                    var syntaxFacts = solution.GetRequiredDocument(firstLocation.SourceTree!)
                                              .GetRequiredLanguageService<ISyntaxFactsService>();
 
                    var namedType = (INamedTypeSymbol)referencedSymbol;
                    foreach (var method in namedType.GetMembers().OfType<IMethodSymbol>())
                    {
                        if (!method.IsImplicitlyDeclared && (method.MethodKind == MethodKind.Constructor ||
                                                      method.MethodKind == MethodKind.StaticConstructor ||
                                                      method.MethodKind == MethodKind.Destructor))
                        {
                            foreach (var location in method.Locations)
                            {
                                if (location.IsInSource)
                                {
                                    var token = location.FindToken(cancellationToken);
                                    if (!syntaxFacts.IsReservedOrContextualKeyword(token) &&
                                        token.ValueText == referencedSymbol.Name)
                                    {
                                        AddRenameLocationIfNotGenerated(location);
                                    }
                                }
                            }
                        }
                    }
                }
 
                return results.ToImmutableAndFree();
 
                void AddRenameLocationIfNotGenerated(Location location, bool isRenamableAccessor = false)
                {
                    RoslynDebug.Assert(location.IsInSource);
                    var document = solution.GetRequiredDocument(location.SourceTree);
 
                    // If the location is in a source generated file, we won't rename it. Our assumption in this case is we
                    // have cascaded to this symbol from our original source symbol, and the generator will update this file
                    // based on the renamed symbol.
                    if (document is not SourceGeneratedDocument)
                        results.Add(new RenameLocation(location, document.Id, isRenamableAccessor: isRenamableAccessor));
                }
            }
 
            internal static async Task<IEnumerable<RenameLocation>> GetRenamableReferenceLocationsAsync(ISymbol referencedSymbol, ISymbol originalSymbol, ReferenceLocation location, Solution solution, CancellationToken cancellationToken)
            {
                // We won't try to update references in source generated files; we'll assume the generator will rerun
                // and produce an updated document with the new name.
                if (location.Document is SourceGeneratedDocument)
                {
                    return SpecializedCollections.EmptyEnumerable<RenameLocation>();
                }
 
                var shouldIncludeSymbol = await ShouldIncludeSymbolAsync(referencedSymbol, originalSymbol, solution, true, cancellationToken).ConfigureAwait(false);
                if (!shouldIncludeSymbol)
                {
                    return SpecializedCollections.EmptyEnumerable<RenameLocation>();
                }
 
                // Implicit references are things like a foreach referencing GetEnumerator. We don't
                // want to consider those as part of the set
                if (location.IsImplicit)
                {
                    return SpecializedCollections.EmptyEnumerable<RenameLocation>();
                }
 
                var results = new List<RenameLocation>();
 
                // If we were originally naming an alias, then we'll only use the location if was
                // also bound through the alias
                if (originalSymbol.Kind == SymbolKind.Alias)
                {
                    if (originalSymbol.Equals(location.Alias))
                    {
                        results.Add(new RenameLocation(location, location.Document.Id));
 
                        // We also need to add the location of the alias
                        // itself
                        var aliasLocation = location.Alias.Locations.Single();
                        Contract.ThrowIfNull(aliasLocation.SourceTree);
                        results.Add(new RenameLocation(aliasLocation, solution.GetRequiredDocument(aliasLocation.SourceTree).Id));
                    }
                }
                else
                {
                    // If we bound through an alias, we'll only rename if the alias's name matches
                    // the name of symbol it points to. We do this because it's common to see things
                    // like "using Goo = System.Goo" where people want to import a single type
                    // rather than a whole namespace of stuff.
                    if (location.Alias != null)
                    {
                        if (location.Alias.Name == referencedSymbol.Name)
                        {
                            results.Add(new RenameLocation(location.Location, location.Document.Id,
                                candidateReason: location.CandidateReason, isRenamableAliasUsage: true, isWrittenTo: location.IsWrittenTo));
 
                            // We also need to add the location of the alias itself
                            var aliasLocation = location.Alias.Locations.Single();
                            Contract.ThrowIfNull(aliasLocation.SourceTree);
                            results.Add(new RenameLocation(aliasLocation, solution.GetRequiredDocument(aliasLocation.SourceTree).Id));
                        }
                    }
                    else if (location.ContainingStringLocation != Location.None)
                    {
                        // Location within a string
                        results.Add(new RenameLocation(
                            location.Location,
                            location.Document.Id,
                            containingLocationForStringOrComment: location.ContainingStringLocation.SourceSpan));
                    }
                    else
                    {
                        // The simple case, so just the single location and we're done
                        results.Add(new RenameLocation(
                            location.Location,
                            location.Document.Id,
                            isWrittenTo: location.IsWrittenTo,
                            candidateReason: location.CandidateReason,
                            isRenamableAccessor: await IsPropertyAccessorOrAnOverrideAsync(referencedSymbol, solution, cancellationToken).ConfigureAwait(false)));
                    }
                }
 
                return results;
            }
 
            internal static async Task<(ImmutableArray<RenameLocation> strings, ImmutableArray<RenameLocation> comments)> GetRenamableLocationsInStringsAndCommentsAsync(
                ISymbol originalSymbol,
                Solution solution,
                ISet<RenameLocation> renameLocations,
                bool renameInStrings,
                bool renameInComments,
                CancellationToken cancellationToken)
            {
                if (!renameInStrings && !renameInComments)
                    return default;
 
                var renameText = originalSymbol.Name;
 
                using var _1 = ArrayBuilder<RenameLocation>.GetInstance(out var stringLocations);
                using var _2 = ArrayBuilder<RenameLocation>.GetInstance(out var commentLocations);
 
                foreach (var documentsGroupedByLanguage in RenameUtilities.GetDocumentsAffectedByRename(originalSymbol, solution, renameLocations).GroupBy(d => d.Project.Language))
                {
                    var syntaxFactsLanguageService = solution.Services.GetLanguageServices(documentsGroupedByLanguage.Key).GetService<ISyntaxFactsService>();
 
                    if (syntaxFactsLanguageService != null)
                    {
                        foreach (var document in documentsGroupedByLanguage)
                        {
                            if (renameInStrings)
                            {
                                await AddLocationsToRenameInStringsAsync(
                                    document, renameText, syntaxFactsLanguageService,
                                    stringLocations, cancellationToken).ConfigureAwait(false);
                            }
 
                            if (renameInComments)
                            {
                                await AddLocationsToRenameInCommentsAsync(document, renameText, commentLocations, cancellationToken).ConfigureAwait(false);
                            }
                        }
                    }
                }
 
                return (renameInStrings ? stringLocations.ToImmutable() : default,
                        renameInComments ? commentLocations.ToImmutable() : default);
            }
 
            private static async Task AddLocationsToRenameInStringsAsync(
                Document document, string renameText, ISyntaxFactsService syntaxFactsService,
                ArrayBuilder<RenameLocation> renameLocations, CancellationToken cancellationToken)
            {
                var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var renameTextLength = renameText.Length;
 
                var renameStringsAndPositions = root
                    .DescendantTokens()
                    .Where(t => syntaxFactsService.IsStringLiteralOrInterpolatedStringLiteral(t) && t.Span.Length >= renameTextLength)
                    .Select(t => Tuple.Create(t.ToString(), t.Span.Start, t.Span));
 
                if (renameStringsAndPositions.Any())
                {
                    AddLocationsToRenameInStringsAndComments(document, root.SyntaxTree, renameText,
                        renameStringsAndPositions, renameLocations);
                }
            }
 
            private static async Task AddLocationsToRenameInCommentsAsync(
                Document document, string renameText, ArrayBuilder<RenameLocation> renameLocations, CancellationToken cancellationToken)
            {
                var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var renameTextLength = renameText.Length;
 
                var renameStringsAndPositions = root
                    .DescendantTrivia(descendIntoTrivia: true)
                    .Where(t => t.Span.Length >= renameTextLength)
                    .Select(t => Tuple.Create(t.ToString(), t.Span.Start, t.Token.Span));
 
                if (renameStringsAndPositions.Any())
                {
                    AddLocationsToRenameInStringsAndComments(document, root.SyntaxTree, renameText,
                        renameStringsAndPositions, renameLocations);
                }
            }
 
            private static void AddLocationsToRenameInStringsAndComments(
                Document document,
                SyntaxTree tree,
                string renameText,
                IEnumerable<Tuple<string, int, TextSpan>> renameStringsAndPositions,
                ArrayBuilder<RenameLocation> renameLocations)
            {
                var regex = RenameUtilities.GetRegexForMatch(renameText);
                foreach (var renameStringAndPosition in renameStringsAndPositions)
                {
                    var renameString = renameStringAndPosition.Item1;
                    var renameStringPosition = renameStringAndPosition.Item2;
                    var containingSpan = renameStringAndPosition.Item3;
 
                    var matches = regex.Matches(renameString);
 
                    foreach (Match? match in matches)
                    {
                        if (match == null)
                            continue;
 
                        var start = renameStringPosition + match.Index;
                        Debug.Assert(renameText.Length == match.Length);
                        var matchTextSpan = new TextSpan(start, renameText.Length);
                        var matchLocation = tree.GetLocation(matchTextSpan);
                        var renameLocation = new RenameLocation(matchLocation, document.Id, containingLocationForStringOrComment: containingSpan);
                        renameLocations.Add(renameLocation);
                    }
                }
            }
        }
    }
}