File: Rename\ConflictEngine\ConflictResolver.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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Rename.ConflictEngine;
 
internal static partial class ConflictResolver
{
    private static readonly SymbolDisplayFormat s_metadataSymbolDisplayFormat = new(
        globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
        typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
        genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance,
        memberOptions: SymbolDisplayMemberOptions.IncludeContainingType | SymbolDisplayMemberOptions.IncludeModifiers | SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeType,
        delegateStyle: SymbolDisplayDelegateStyle.NameAndSignature,
        extensionMethodStyle: SymbolDisplayExtensionMethodStyle.StaticMethod,
        parameterOptions: SymbolDisplayParameterOptions.IncludeParamsRefOut | SymbolDisplayParameterOptions.IncludeType,
        propertyStyle: SymbolDisplayPropertyStyle.NameOnly,
        miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);
 
    private const string s_metadataNameSeparators = " .,:<`>()\r\n";
 
    /// <summary>
    /// Performs the renaming of the symbol in the solution, identifies renaming conflicts and automatically
    /// resolves them where possible.
    /// </summary>
    /// <param name="replacementText">The new name of the identifier</param>
    /// <param name="nonConflictSymbolKeys">Used after renaming references. References that now bind to any of these
    /// symbols are not considered to be in conflict. Useful for features that want to rename existing references to
    /// point at some existing symbol. Normally this would be a conflict, but this can be used to override that
    /// behavior.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A conflict resolution containing the new solution.</returns>
    internal static async Task<ConflictResolution> ResolveLightweightConflictsAsync(
        ISymbol symbol,
        LightweightRenameLocations lightweightRenameLocations,
        string replacementText,
        ImmutableArray<SymbolKey> nonConflictSymbolKeys,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        using (Logger.LogBlock(FunctionId.Renamer_ResolveConflictsAsync, cancellationToken))
        {
            var solution = lightweightRenameLocations.Solution;
            var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
            if (client != null)
            {
                var serializableSymbol = SerializableSymbolAndProjectId.Dehydrate(lightweightRenameLocations.Solution, symbol, cancellationToken);
                var serializableLocationSet = lightweightRenameLocations.Dehydrate();
 
                var result = await client.TryInvokeAsync<IRemoteRenamerService, SerializableConflictResolution?>(
                    solution,
                    (service, solutionInfo, cancellationToken) => service.ResolveConflictsAsync(solutionInfo, serializableSymbol, serializableLocationSet, replacementText, nonConflictSymbolKeys, cancellationToken),
                    cancellationToken).ConfigureAwait(false);
 
                if (result.HasValue && result.Value != null)
                    return await result.Value.RehydrateAsync(solution, cancellationToken).ConfigureAwait(false);
 
                // TODO: do not fall back to in-proc if client is available (https://github.com/dotnet/roslyn/issues/47557)
            }
        }
 
        var heavyweightLocations = await lightweightRenameLocations.ToSymbolicLocationsAsync(symbol, cancellationToken).ConfigureAwait(false);
        if (heavyweightLocations is null)
            return new ConflictResolution(WorkspacesResources.Failed_to_resolve_rename_conflicts);
 
        return await ResolveSymbolicLocationConflictsInCurrentProcessAsync(
            heavyweightLocations, replacementText, nonConflictSymbolKeys, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Finds any conflicts that would arise from using <paramref name="replacementText"/> as the new name for a
    /// symbol and returns how to resolve those conflicts.  Will not cross any process boundaries to do this.
    /// </summary>
    internal static async Task<ConflictResolution> ResolveSymbolicLocationConflictsInCurrentProcessAsync(
        SymbolicRenameLocations renameLocations,
        string replacementText,
        ImmutableArray<SymbolKey> nonConflictSymbolKeys,
        CancellationToken cancellationToken)
    {
        // when someone e.g. renames a symbol from metadata through the API (IDE blocks this), we need to return
        var renameSymbolDeclarationLocation = renameLocations.Symbol.Locations.Where(loc => loc.IsInSource).FirstOrDefault();
        if (renameSymbolDeclarationLocation == null)
        {
            // Symbol "{0}" is not from source.
            return new ConflictResolution(string.Format(WorkspacesResources.Symbol_0_is_not_from_source, renameLocations.Symbol.Name));
        }
 
        var resolution = await ResolveMutableConflictsAsync(
            renameLocations, renameSymbolDeclarationLocation, replacementText, nonConflictSymbolKeys, cancellationToken).ConfigureAwait(false);
 
        return resolution.ToConflictResolution();
    }
 
    private static Task<MutableConflictResolution> ResolveMutableConflictsAsync(
        SymbolicRenameLocations renameLocationSet,
        Location renameSymbolDeclarationLocation,
        string replacementText,
        ImmutableArray<SymbolKey> nonConflictSymbolKeys,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var session = new Session(
            renameLocationSet, renameSymbolDeclarationLocation, replacementText, nonConflictSymbolKeys, cancellationToken);
        return session.ResolveConflictsAsync();
    }
 
    /// <summary>
    /// Used to find the symbols associated with the Invocation Expression surrounding the Token
    /// </summary>
    private static ImmutableArray<ISymbol> SymbolsForEnclosingInvocationExpressionWorker(SyntaxNode invocationExpression, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        var symbolInfo = semanticModel.GetSymbolInfo(invocationExpression, cancellationToken);
        return symbolInfo.Symbol == null
            ? default
            : [symbolInfo.Symbol];
    }
 
    private static SyntaxNode? GetExpansionTargetForLocationPerLanguage(SyntaxToken tokenOrNode, Document document)
    {
        var renameRewriterService = document.GetRequiredLanguageService<IRenameRewriterLanguageService>();
        var complexifiedTarget = renameRewriterService.GetExpansionTargetForLocation(tokenOrNode);
        return complexifiedTarget;
    }
 
    private static bool LocalVariableConflictPerLanguage(SyntaxToken tokenOrNode, Document document, ImmutableArray<ISymbol> newReferencedSymbols)
    {
        var renameRewriterService = document.GetRequiredLanguageService<IRenameRewriterLanguageService>();
        var isConflict = renameRewriterService.LocalVariableConflict(tokenOrNode, newReferencedSymbols);
        return isConflict;
    }
 
    private static bool IsIdentifierValid_Worker(Solution solution, string replacementText, IEnumerable<ProjectId> projectIds)
    {
        foreach (var language in projectIds.Select(p => solution.GetRequiredProject(p).Language).Distinct())
        {
            var languageServices = solution.Services.GetLanguageServices(language);
            var renameRewriterLanguageService = languageServices.GetRequiredService<IRenameRewriterLanguageService>();
            var syntaxFactsLanguageService = languageServices.GetRequiredService<ISyntaxFactsService>();
            if (!renameRewriterLanguageService.IsIdentifierValid(replacementText, syntaxFactsLanguageService))
            {
                return false;
            }
        }
 
        return true;
    }
 
    private static bool IsRenameValid(MutableConflictResolution conflictResolution, ISymbol renamedSymbol)
    {
        // if we rename an identifier and it now binds to a symbol from metadata this should be treated as
        // an invalid rename.
        return conflictResolution.ReplacementTextValid && renamedSymbol != null && renamedSymbol.Locations.Any(static loc => loc.IsInSource);
    }
 
    private static async Task AddImplicitConflictsAsync(
        ISymbol renamedSymbol,
        ISymbol originalSymbol,
        IEnumerable<ReferenceLocation> implicitReferenceLocations,
        SemanticModel semanticModel,
        Location originalDeclarationLocation,
        int newDeclarationLocationStartingPosition,
        MutableConflictResolution conflictResolution,
        CancellationToken cancellationToken)
    {
        {
            var renameRewriterService =
                conflictResolution.CurrentSolution.Services.GetRequiredLanguageService<IRenameRewriterLanguageService>(renamedSymbol.Language);
            var implicitUsageConflicts = renameRewriterService.ComputePossibleImplicitUsageConflicts(renamedSymbol, semanticModel, originalDeclarationLocation, newDeclarationLocationStartingPosition, cancellationToken);
            foreach (var implicitUsageConflict in implicitUsageConflicts)
            {
                Contract.ThrowIfNull(implicitUsageConflict.SourceTree);
                conflictResolution.AddOrReplaceRelatedLocation(new RelatedLocation(
                    implicitUsageConflict.SourceSpan, conflictResolution.OldSolution.GetRequiredDocument(implicitUsageConflict.SourceTree).Id, RelatedLocationType.UnresolvableConflict));
            }
        }
 
        if (implicitReferenceLocations.IsEmpty())
        {
            return;
        }
 
        foreach (var implicitReferenceLocationsPerLanguage in implicitReferenceLocations.GroupBy(loc => loc.Document.Project.Language))
        {
            // the location of the implicit reference defines the language rules to check.
            // E.g. foreach in C# using a MoveNext in VB that is renamed to MOVENEXT (within VB)
            var renameRewriterService = implicitReferenceLocationsPerLanguage.First().Document.Project.Services.GetRequiredService<IRenameRewriterLanguageService>();
            var implicitConflicts = await renameRewriterService.ComputeImplicitReferenceConflictsAsync(
                originalSymbol,
                renamedSymbol,
                implicitReferenceLocationsPerLanguage,
                cancellationToken).ConfigureAwait(false);
 
            foreach (var implicitConflict in implicitConflicts)
            {
                Contract.ThrowIfNull(implicitConflict.SourceTree);
                conflictResolution.AddRelatedLocation(new RelatedLocation(
                    implicitConflict.SourceSpan, conflictResolution.OldSolution.GetRequiredDocument(implicitConflict.SourceTree).Id, RelatedLocationType.UnresolvableConflict));
            }
        }
    }
 
    /// <summary>
    /// Computes an adds conflicts relating to declarations, which are independent of
    /// location-based checks. Examples of these types of conflicts include renaming a member to
    /// the same name as another member of a type: binding doesn't change (at least from the
    /// perspective of find all references), but we still need to track it.
    /// </summary>
    private static async Task AddDeclarationConflictsAsync(
        ISymbol renamedSymbol,
        ISymbol renameSymbol,
        IEnumerable<ISymbol> referencedSymbols,
        MutableConflictResolution conflictResolution,
        IDictionary<Location, Location> reverseMappedLocations,
        CancellationToken cancellationToken)
    {
        try
        {
            var projectOpt = conflictResolution.CurrentSolution.GetProject(renamedSymbol.ContainingAssembly, cancellationToken);
            if (renamedSymbol.ContainingSymbol.IsKind(SymbolKind.NamedType))
            {
                Contract.ThrowIfNull(projectOpt);
                var otherThingsNamedTheSame = renamedSymbol.ContainingType.GetMembers(renamedSymbol.Name)
                                                       .Where(s => !s.Equals(renamedSymbol) &&
                                                                   string.Equals(s.MetadataName, renamedSymbol.MetadataName, StringComparison.Ordinal));
 
                IEnumerable<ISymbol> otherThingsNamedTheSameExcludeMethodAndParameterizedProperty;
 
                // Possibly overloaded symbols are excluded here and handled elsewhere
                var semanticFactsService = projectOpt.Services.GetRequiredService<ISemanticFactsService>();
                if (semanticFactsService.SupportsParameterizedProperties)
                {
                    otherThingsNamedTheSameExcludeMethodAndParameterizedProperty = otherThingsNamedTheSame
                        .Where(s => !s.MatchesKind(SymbolKind.Method, SymbolKind.Property) ||
                            !renamedSymbol.MatchesKind(SymbolKind.Method, SymbolKind.Property));
                }
                else
                {
                    otherThingsNamedTheSameExcludeMethodAndParameterizedProperty = otherThingsNamedTheSame
                        .Where(s => s.Kind != SymbolKind.Method || renamedSymbol.Kind != SymbolKind.Method);
                }
 
                AddConflictingSymbolLocations(otherThingsNamedTheSameExcludeMethodAndParameterizedProperty, conflictResolution, reverseMappedLocations);
            }
 
            if (renamedSymbol.IsKind(SymbolKind.Namespace) && renamedSymbol.ContainingSymbol.IsKind(SymbolKind.Namespace))
            {
                var otherThingsNamedTheSame = ((INamespaceSymbol)renamedSymbol.ContainingSymbol).GetMembers(renamedSymbol.Name)
                                                        .Where(s => !s.Equals(renamedSymbol) &&
                                                                    !s.IsKind(SymbolKind.Namespace) &&
                                                                    string.Equals(s.MetadataName, renamedSymbol.MetadataName, StringComparison.Ordinal));
 
                AddConflictingSymbolLocations(otherThingsNamedTheSame, conflictResolution, reverseMappedLocations);
            }
 
            if (renamedSymbol.IsKind(SymbolKind.NamedType) && renamedSymbol.ContainingSymbol is INamespaceOrTypeSymbol)
            {
                var otherThingsNamedTheSame = ((INamespaceOrTypeSymbol)renamedSymbol.ContainingSymbol).GetMembers(renamedSymbol.Name)
                                                        .Where(s => !s.Equals(renamedSymbol) &&
                                                                    string.Equals(s.MetadataName, renamedSymbol.MetadataName, StringComparison.Ordinal));
 
                var conflictingSymbolLocations = otherThingsNamedTheSame.Where(s => !s.IsKind(SymbolKind.Namespace));
                if (otherThingsNamedTheSame.Any(s => s.IsKind(SymbolKind.Namespace)))
                {
                    conflictingSymbolLocations = conflictingSymbolLocations.Concat(renamedSymbol);
                }
 
                AddConflictingSymbolLocations(conflictingSymbolLocations, conflictResolution, reverseMappedLocations);
            }
 
            // Some types of symbols (namespaces, cref stuff, etc) might not have ContainingAssemblies
            if (renamedSymbol.ContainingAssembly != null)
            {
                Contract.ThrowIfNull(projectOpt);
                // There also might be language specific rules we need to include
                var languageRenameService = projectOpt.Services.GetRequiredService<IRenameRewriterLanguageService>();
                var languageConflicts = await languageRenameService.ComputeDeclarationConflictsAsync(
                    conflictResolution.ReplacementText,
                    renamedSymbol,
                    renameSymbol,
                    referencedSymbols,
                    conflictResolution.OldSolution,
                    conflictResolution.CurrentSolution,
                    reverseMappedLocations,
                    cancellationToken).ConfigureAwait(false);
 
                foreach (var languageConflict in languageConflicts)
                {
                    Contract.ThrowIfNull(languageConflict.SourceTree);
                    conflictResolution.AddOrReplaceRelatedLocation(new RelatedLocation(
                        languageConflict.SourceSpan, conflictResolution.OldSolution.GetRequiredDocument(languageConflict.SourceTree).Id, RelatedLocationType.UnresolvableConflict));
                }
            }
        }
        catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
        {
            // A NullReferenceException is happening in this method, but the dumps do not
            // contain information about this stack frame because this method is async and
            // therefore the exception filter in IdentifyConflictsAsync is insufficient.
            // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=378642
 
            throw ExceptionUtilities.Unreachable();
        }
    }
 
    private static void AddConflictingSymbolLocations(IEnumerable<ISymbol> conflictingSymbols, MutableConflictResolution conflictResolution, IDictionary<Location, Location> reverseMappedLocations)
    {
        foreach (var newSymbol in conflictingSymbols)
        {
            foreach (var newLocation in newSymbol.Locations)
            {
                if (newLocation.IsInSource)
                {
                    if (reverseMappedLocations.TryGetValue(newLocation, out var oldLocation))
                    {
                        Contract.ThrowIfNull(oldLocation.SourceTree);
                        conflictResolution.AddOrReplaceRelatedLocation(new RelatedLocation(
                            oldLocation.SourceSpan, conflictResolution.OldSolution.GetRequiredDocument(oldLocation.SourceTree).Id, RelatedLocationType.UnresolvableConflict));
                    }
                }
            }
        }
    }
 
    public static async Task<RenameDeclarationLocationReference[]> CreateDeclarationLocationAnnotationsAsync(
        Solution solution,
        IEnumerable<ISymbol> symbols,
        CancellationToken cancellationToken)
    {
        var renameDeclarationLocations = new RenameDeclarationLocationReference[symbols.Count()];
 
        var symbolIndex = 0;
        foreach (var symbol in symbols)
        {
            var locations = symbol.Locations;
            var overriddenFromMetadata = false;
 
            if (symbol.IsOverride)
            {
                var overriddenSymbol = symbol.GetOverriddenMember();
 
                if (overriddenSymbol != null)
                {
                    overriddenSymbol = await SymbolFinder.FindSourceDefinitionAsync(overriddenSymbol, solution, cancellationToken).ConfigureAwait(false);
                    overriddenFromMetadata = overriddenSymbol == null || overriddenSymbol.Locations.All(loc => loc.IsInMetadata);
                }
            }
 
            var location = await GetSymbolLocationAsync(solution, symbol, cancellationToken).ConfigureAwait(false);
            if (location != null && location.IsInSource)
            {
                renameDeclarationLocations[symbolIndex] = new RenameDeclarationLocationReference(solution.GetDocumentId(location.SourceTree), location.SourceSpan, overriddenFromMetadata, locations.Length);
            }
            else
            {
                renameDeclarationLocations[symbolIndex] = new RenameDeclarationLocationReference(GetString(symbol), locations.Length);
            }
 
            symbolIndex++;
        }
 
        return renameDeclarationLocations;
    }
 
    private static string GetString(ISymbol symbol)
    {
        if (symbol.IsAnonymousType())
        {
            return symbol.ToDisplayParts(s_metadataSymbolDisplayFormat)
                .WhereAsArray(p => p.Kind is not SymbolDisplayPartKind.PropertyName and not SymbolDisplayPartKind.FieldName)
                .ToDisplayString();
        }
        else
        {
            return symbol.ToDisplayString(s_metadataSymbolDisplayFormat);
        }
    }
 
    /// <summary>
    /// Gives the First Location for a given Symbol by ordering the locations using DocumentId first and Location starting position second
    /// </summary>
    private static async Task<Location?> GetSymbolLocationAsync(Solution solution, ISymbol symbol, CancellationToken cancellationToken)
    {
        var locations = symbol.Locations;
 
        var originalsourcesymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, solution, cancellationToken).ConfigureAwait(false);
        if (originalsourcesymbol != null)
        {
            locations = originalsourcesymbol.Locations;
        }
 
        var orderedLocations = locations
            .OrderBy(l => l.IsInSource ? solution.GetDocumentId(l.SourceTree)!.Id : Guid.Empty)
            .ThenBy(l => l.IsInSource ? l.SourceSpan.Start : int.MaxValue);
 
        return orderedLocations.FirstOrDefault();
    }
 
    private static bool HeuristicMetadataNameEquivalenceCheck(
        string oldMetadataName,
        string newMetadataName,
        string originalText,
        string replacementText)
    {
        if (string.Equals(oldMetadataName, newMetadataName, StringComparison.Ordinal))
        {
            return true;
        }
 
        var index = newMetadataName.IndexOf(replacementText, 0);
        var newMetadataNameBuilder = new StringBuilder();
 
        // Every loop updates the newMetadataName to resemble the oldMetadataName
        while (index != -1 && index < oldMetadataName.Length)
        {
            // This check is to see if the part of string before the string match, matches
            if (!IsSubStringEqual(oldMetadataName, newMetadataName, index))
            {
                return false;
            }
 
            // Ok to replace
            if (IsWholeIdentifier(newMetadataName, replacementText, index))
            {
                newMetadataNameBuilder.Append(newMetadataName, 0, index);
                newMetadataNameBuilder.Append(originalText);
                newMetadataNameBuilder.Append(newMetadataName, index + replacementText.Length, newMetadataName.Length - (index + replacementText.Length));
                newMetadataName = newMetadataNameBuilder.ToString();
                newMetadataNameBuilder.Clear();
            }
 
            index = newMetadataName.IndexOf(replacementText, index + 1);
        }
 
        return string.Equals(newMetadataName, oldMetadataName, StringComparison.Ordinal);
    }
 
    private static bool IsSubStringEqual(
        string str1,
        string str2,
        int index)
    {
        Debug.Assert(index <= str1.Length && index <= str2.Length, "Index cannot be greater than the string");
        var currentIndex = 0;
        while (currentIndex < index)
        {
            if (str1[currentIndex] != str2[currentIndex])
            {
                return false;
            }
 
            currentIndex++;
        }
 
        return true;
    }
 
    private static bool IsWholeIdentifier(
        string metadataName,
        string searchText,
        int index)
    {
        if (index == -1)
        {
            return false;
        }
 
        // Check for the previous char
        if (index != 0)
        {
            var previousChar = metadataName[index - 1];
 
            if (!IsIdentifierSeparator(previousChar))
            {
                return false;
            }
        }
 
        // Check for the next char
        if (index + searchText.Length != metadataName.Length)
        {
            var nextChar = metadataName[index + searchText.Length];
 
            if (!IsIdentifierSeparator(nextChar))
            {
                return false;
            }
        }
 
        return true;
    }
 
    private static bool IsIdentifierSeparator(char element)
        => s_metadataNameSeparators.IndexOf(element) != -1;
}