File: Rename\RenameUtilities.cs
Web Access
Project: src\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;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Rename;
 
internal static class RenameUtilities
{
    internal static SyntaxToken UpdateAliasAnnotation(SyntaxToken token, ISymbol aliasSymbol, string replacementText)
    {
        // If the below Single() assert fails then it means the token has gone through a rename session where
        // it obtained an AliasSyntaxAnnotation and it is going through another rename session. Make sure the token
        // has only one annotation pertaining to the current session or try to extract only the current session annotation
        var originalAliasAnnotation = token.GetAnnotations(AliasAnnotation.Kind).Single();
        var originalAliasName = AliasAnnotation.GetAliasName(originalAliasAnnotation);
 
        if (originalAliasName == aliasSymbol.Name)
        {
            token = token.WithoutAnnotations(originalAliasAnnotation);
            var replacementAliasAnnotation = AliasAnnotation.Create(replacementText);
            token = token.WithAdditionalAnnotations(replacementAliasAnnotation);
        }
 
        return token;
    }
 
    internal static ImmutableArray<ISymbol> GetSymbolsTouchingPosition(
        int position, SemanticModel semanticModel, SolutionServices services, CancellationToken cancellationToken)
    {
        var bindableToken = semanticModel.SyntaxTree.GetRoot(cancellationToken).FindToken(position, findInsideTrivia: true);
        var semanticInfo = semanticModel.GetSemanticInfo(bindableToken, services, cancellationToken);
        var symbols = semanticInfo.DeclaredSymbol != null
            ? [semanticInfo.DeclaredSymbol]
            : semanticInfo.GetSymbols(includeType: false);
 
        // if there are more than one symbol, then remove the alias symbols.
        // When using (not declaring) an alias, the alias symbol and the target symbol are returned
        // by GetSymbols
        if (symbols.Length > 1)
        {
            symbols = symbols.WhereAsArray(s => s.Kind != SymbolKind.Alias);
        }
 
        if (symbols.Length == 0)
        {
            var info = semanticModel.GetSymbolInfo(bindableToken, cancellationToken);
            if (info.CandidateReason == CandidateReason.MemberGroup)
            {
                return info.CandidateSymbols;
            }
        }
 
        return symbols;
    }
 
    private static bool IsSymbolDefinedInsideMethod(ISymbol symbol)
    {
        return
            symbol.Kind is SymbolKind.Local or
            SymbolKind.Label or
            SymbolKind.RangeVariable or
            SymbolKind.Parameter;
    }
 
    internal static IEnumerable<Document> GetDocumentsAffectedByRename(ISymbol symbol, Solution solution, IEnumerable<RenameLocation> renameLocations)
    {
        if (IsSymbolDefinedInsideMethod(symbol))
        {
            // if the symbol was declared inside of a method, don't check for conflicts in non-renamed documents.
            return renameLocations.Select(l => solution.GetRequiredDocument(l.DocumentId));
        }
        else
        {
            var documentsOfRenameSymbolDeclaration = symbol.Locations.Where(l => l.IsInSource).Select(l => solution.GetRequiredDocument(l.SourceTree!));
            var projectIdsOfRenameSymbolDeclaration =
                documentsOfRenameSymbolDeclaration.SelectMany(d => d.GetLinkedDocumentIds())
                .Concat(documentsOfRenameSymbolDeclaration.First().Id)
                .Select(d => d.ProjectId).Distinct();
 
            // perf optimization: only look in declaring project when possible
            if (ShouldRenameOnlyAffectDeclaringProject(symbol))
            {
                var isSubset = renameLocations.Select(l => l.DocumentId.ProjectId).Distinct().Except(projectIdsOfRenameSymbolDeclaration).IsEmpty();
                Contract.ThrowIfFalse(isSubset);
                return projectIdsOfRenameSymbolDeclaration.SelectMany(p => solution.GetRequiredProject(p).Documents);
            }
            else
            {
                // We are trying to figure out the projects that directly depend on the project that contains the declaration for
                // the rename symbol.  Other projects should not be affected by the rename.
                var relevantProjects = projectIdsOfRenameSymbolDeclaration.Concat(projectIdsOfRenameSymbolDeclaration.SelectMany(p =>
                   solution.GetProjectDependencyGraph().GetProjectsThatDirectlyDependOnThisProject(p))).Distinct();
                return relevantProjects.SelectMany(p => solution.GetRequiredProject(p).Documents);
            }
        }
    }
 
    /// <summary>
    /// Renaming a private symbol typically confines the set of references and potential
    /// conflicts to that symbols declaring project. However, rename may cascade to
    /// non-public symbols which may then require other projects be considered.
    /// </summary>
    private static bool ShouldRenameOnlyAffectDeclaringProject(ISymbol symbol)
    {
        if (symbol.DeclaredAccessibility != Accessibility.Private)
        {
            // non-private members can influence other projects.
            return false;
        }
 
        if (symbol.ExplicitInterfaceImplementations().Any())
        {
            // Explicit interface implementations can cascade to other projects
            return false;
        }
 
        if (symbol.IsOverride)
        {
            // private-overrides aren't actually legal.  But if we see one, we tolerate it and search other projects in case
            // they override it.
            // https://github.com/dotnet/roslyn/issues/25682
            return false;
        }
 
        return true;
    }
 
    internal static TokenRenameInfo GetTokenRenameInfo(
        ISemanticFactsService semanticFacts,
        SemanticModel semanticModel,
        SyntaxToken token,
        CancellationToken cancellationToken)
    {
        var symbol = semanticFacts.GetDeclaredSymbol(semanticModel, token, cancellationToken);
        if (symbol != null)
        {
            return TokenRenameInfo.CreateSingleSymbolTokenInfo(symbol);
        }
 
        var symbolInfo = semanticModel.GetSymbolInfo(token, cancellationToken);
        if (symbolInfo.Symbol != null)
        {
            if (symbolInfo.Symbol.IsTupleType())
            {
                return TokenRenameInfo.NoSymbolsTokenInfo;
            }
 
            return TokenRenameInfo.CreateSingleSymbolTokenInfo(symbolInfo.Symbol);
        }
 
        if (symbolInfo.CandidateReason == CandidateReason.MemberGroup && symbolInfo.CandidateSymbols.Any())
        {
            // This is a reference from a nameof expression. Allow the rename but set the RenameOverloads option
            return TokenRenameInfo.CreateMemberGroupTokenInfo(symbolInfo.CandidateSymbols);
        }
 
        // If we have overload resolution issues at the callsite, we generally don't want to rename (as it's unclear
        // which overload the user is actually calling).  However, if there is just a single overload, there's no real
        // issue since it's clear which one the user wants to rename in that case.
        if (symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure && symbolInfo.CandidateSymbols.Length == 1)
            return TokenRenameInfo.CreateMemberGroupTokenInfo(symbolInfo.CandidateSymbols);
 
        if (RenameLocation.ShouldRename(symbolInfo.CandidateReason) &&
            symbolInfo.CandidateSymbols.Length == 1)
        {
            // TODO(cyrusn): We're allowing rename here, but we likely should let the user
            // know that there is an error in the code and that rename results might be
            // inaccurate.
            return TokenRenameInfo.CreateSingleSymbolTokenInfo(symbolInfo.CandidateSymbols[0]);
        }
 
        return TokenRenameInfo.NoSymbolsTokenInfo;
    }
 
    public static IEnumerable<ISymbol> GetOverloadedSymbols(ISymbol symbol)
    {
        if (symbol is IMethodSymbol)
        {
            var containingType = symbol.ContainingType;
            if (containingType.Kind == SymbolKind.NamedType)
            {
                foreach (var member in containingType.GetMembers())
                {
                    if (string.Equals(member.MetadataName, symbol.MetadataName, StringComparison.Ordinal) && member is IMethodSymbol && !member.Equals(symbol))
                    {
                        yield return member;
                    }
                }
            }
        }
    }
 
    public static async Task<ISymbol?> TryGetPropertyFromAccessorOrAnOverrideAsync(
        ISymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        if (symbol.IsPropertyAccessor())
        {
            return ((IMethodSymbol)symbol).AssociatedSymbol;
        }
 
        if (symbol.IsOverride && symbol.GetOverriddenMember() != null)
        {
            var originalSourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(
                symbol.GetOverriddenMember(), solution, cancellationToken).ConfigureAwait(false);
 
            if (originalSourceSymbol != null)
            {
                return await TryGetPropertyFromAccessorOrAnOverrideAsync(originalSourceSymbol, solution, cancellationToken).ConfigureAwait(false);
            }
        }
 
        if (symbol.Kind == SymbolKind.Method &&
            symbol.ContainingType.TypeKind == TypeKind.Interface)
        {
            var methodImplementors = await SymbolFinder.FindImplementationsAsync(
                symbol, solution, cancellationToken: cancellationToken).ConfigureAwait(false);
 
            foreach (var methodImplementor in methodImplementors)
            {
                var propertyAccessorOrAnOverride = await TryGetPropertyFromAccessorOrAnOverrideAsync(methodImplementor, solution, cancellationToken).ConfigureAwait(false);
                if (propertyAccessorOrAnOverride != null)
                {
                    return propertyAccessorOrAnOverride;
                }
            }
        }
 
        return null;
    }
 
    public static string ReplaceMatchingSubStrings(
        string replaceInsideString,
        string matchText,
        string replacementText,
        ImmutableSortedSet<TextSpan>? subSpansToReplace = null)
    {
        if (subSpansToReplace == null)
        {
            // We do not have already computed sub-spans to replace inside the string.
            // Get regex for matches within the string and replace all matches with replacementText.
            var regex = GetRegexForMatch(matchText);
            return regex.Replace(replaceInsideString, replacementText);
        }
        else
        {
            // We are provided specific matches to replace inside the string.
            // Process the input string from start to end, replacing matchText with replacementText
            // at the provided sub-spans within the string for these matches.
            var stringBuilder = new StringBuilder();
            var startOffset = 0;
            foreach (var subSpan in subSpansToReplace)
            {
                Debug.Assert(subSpan.Start <= replaceInsideString.Length);
                Debug.Assert(subSpan.End <= replaceInsideString.Length);
 
                // Verify that provided sub-span has a match with matchText.
                if (replaceInsideString.Substring(subSpan.Start, subSpan.Length) != matchText)
                    continue;
 
                // Append the sub-string from last match till the next match
                var offset = subSpan.Start - startOffset;
                stringBuilder.Append(replaceInsideString.Substring(startOffset, offset));
 
                // Append the replacementText
                stringBuilder.Append(replacementText);
 
                // Update startOffset to process the next match.
                startOffset += offset + subSpan.Length;
            }
 
            // Append the remaining of the sub-string within replaceInsideString after the last match. 
            stringBuilder.Append(replaceInsideString[startOffset..]);
 
            return stringBuilder.ToString();
        }
    }
 
    public static Regex GetRegexForMatch(string matchText)
    {
        var matchString = string.Format(@"\b{0}\b", matchText);
        return new Regex(matchString, RegexOptions.CultureInvariant);
    }
 
    /// <summary>
    /// Given a symbol in a document, returns the "right" symbol that should be renamed in
    /// the case the name binds to things like aliases _and_ the underlying type at once.
    /// </summary>
    public static async Task<ISymbol?> TryGetRenamableSymbolAsync(
        Document document, int position, CancellationToken cancellationToken)
    {
        var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false);
        if (symbol == null)
            return null;
 
        var definitionSymbol = await FindDefinitionSymbolAsync(symbol, document.Project.Solution, cancellationToken).ConfigureAwait(false);
        Contract.ThrowIfNull(definitionSymbol);
 
        return definitionSymbol;
    }
 
    /// <summary>
    /// Given a symbol, finds the symbol that actually defines the name that we're using.
    /// </summary>
    public static async Task<ISymbol> FindDefinitionSymbolAsync(
        ISymbol symbol, Solution solution, CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(symbol);
        Contract.ThrowIfNull(solution);
 
        // Make sure we're on the original source definition if we can be
        var foundSymbol = await SymbolFinder.FindSourceDefinitionAsync(
            symbol, solution, cancellationToken).ConfigureAwait(false);
 
        var bestSymbol = foundSymbol ?? symbol;
        symbol = bestSymbol;
 
        // If we're renaming a property, it might be a synthesized property for a method
        // backing field.
        if (symbol.Kind == SymbolKind.Parameter)
        {
            if (symbol.ContainingSymbol.Kind == SymbolKind.Method)
            {
                var containingMethod = (IMethodSymbol)symbol.ContainingSymbol;
                if (containingMethod.AssociatedSymbol is IPropertySymbol)
                {
                    var associatedPropertyOrEvent = (IPropertySymbol)containingMethod.AssociatedSymbol;
                    var ordinal = containingMethod.Parameters.IndexOf((IParameterSymbol)symbol);
                    if (ordinal < associatedPropertyOrEvent.Parameters.Length)
                    {
                        return associatedPropertyOrEvent.Parameters[ordinal];
                    }
                }
            }
        }
 
        // if we are renaming a compiler generated delegate for an event, cascade to the event
        if (symbol.Kind == SymbolKind.NamedType)
        {
            var typeSymbol = (INamedTypeSymbol)symbol;
            if (typeSymbol.IsImplicitlyDeclared && typeSymbol.IsDelegateType() && typeSymbol.AssociatedSymbol != null)
            {
                return typeSymbol.AssociatedSymbol;
            }
        }
 
        // If we are renaming a constructor or destructor, we wish to rename the whole type
        if (symbol.Kind == SymbolKind.Method)
        {
            var methodSymbol = (IMethodSymbol)symbol;
            if (methodSymbol.MethodKind is MethodKind.Constructor or
                MethodKind.StaticConstructor or
                MethodKind.Destructor)
            {
                return methodSymbol.ContainingType;
            }
        }
 
        // If we are renaming a backing field for a property, cascade to the property
        if (symbol.Kind == SymbolKind.Field)
        {
            var fieldSymbol = (IFieldSymbol)symbol;
            if (fieldSymbol.IsImplicitlyDeclared &&
                fieldSymbol.AssociatedSymbol.IsKind(SymbolKind.Property))
            {
                return fieldSymbol.AssociatedSymbol;
            }
        }
 
        // in case this is e.g. an overridden property accessor, we'll treat the property itself as the definition symbol
        var property = await RenameUtilities.TryGetPropertyFromAccessorOrAnOverrideAsync(bestSymbol, solution, cancellationToken).ConfigureAwait(false);
 
        return property ?? bestSymbol;
    }
}