File: Rename\SymbolicRenameInfo.cs
Web Access
Project: src\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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
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 Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Rename;
 
internal sealed class SymbolicRenameInfo
{
    private const string AttributeSuffix = "Attribute";
 
    [MemberNotNullWhen(true, nameof(LocalizedErrorMessage))]
    [MemberNotNullWhen(false, nameof(Document))]
    [MemberNotNullWhen(false, nameof(TriggerText))]
    [MemberNotNullWhen(false, nameof(Symbol))]
    public bool IsError => LocalizedErrorMessage != null;
 
    public string? LocalizedErrorMessage { get; }
 
    public Document? Document { get; }
    public SyntaxToken TriggerToken { get; }
    public string? TriggerText { get; }
    public ISymbol? Symbol { get; }
    public bool ForceRenameOverloads { get; }
    public ImmutableArray<DocumentSpan> DocumentSpans { get; }
 
    public bool IsRenamingAttributePrefix { get; }
 
    private SymbolicRenameInfo(string localizedErrorMessage)
        => this.LocalizedErrorMessage = localizedErrorMessage;
 
    private SymbolicRenameInfo(
        Document document,
        SyntaxToken triggerToken,
        string triggerText,
        ISymbol symbol,
        bool forceRenameOverloads,
        ImmutableArray<DocumentSpan> documentSpans)
    {
        Document = document;
        TriggerToken = triggerToken;
        TriggerText = triggerText;
        Symbol = symbol;
        ForceRenameOverloads = forceRenameOverloads;
        DocumentSpans = documentSpans;
 
        this.IsRenamingAttributePrefix = CanRenameAttributePrefix(triggerText);
    }
 
    private bool CanRenameAttributePrefix(string triggerText)
    {
        Contract.ThrowIfTrue(this.IsError);
 
        // if this isn't an attribute, or it doesn't have the 'Attribute' suffix, then clearly
        // we can't rename just the attribute prefix.
        if (!IsRenamingAttributeTypeWithAttributeSuffix())
            return false;
 
        // Ok, the symbol is good.  Now, make sure that the trigger text starts with the prefix
        // of the attribute.  If it does, then we can rename just the attribute prefix (otherwise
        // we need to rename the entire attribute).
        var nameWithoutAttribute = GetWithoutAttributeSuffix(this.Symbol.Name);
        return triggerText.StartsWith(nameWithoutAttribute);
 
        bool IsRenamingAttributeTypeWithAttributeSuffix()
        {
            if (this.Symbol.IsAttribute() || (this.Symbol is IAliasSymbol alias && alias.Target.IsAttribute()))
            {
                if (HasAttributeSuffix(this.Symbol.Name))
                    return true;
            }
 
            return false;
        }
    }
 
    public string GetWithoutAttributeSuffix(string value)
    {
        Contract.ThrowIfTrue(this.IsError);
        var isCaseSensitive = this.Document.GetRequiredLanguageService<ISyntaxFactsService>().IsCaseSensitive;
        return value.GetWithoutAttributeSuffix(isCaseSensitive)!;
    }
 
    private bool HasAttributeSuffix(string value)
    {
        Contract.ThrowIfTrue(this.IsError);
 
        var isCaseSensitive = this.Document.GetRequiredLanguageService<ISyntaxFactsService>().IsCaseSensitive;
        return value.TryGetWithoutAttributeSuffix(isCaseSensitive, result: out var _);
    }
 
    public string GetFinalSymbolName(string replacementText)
    {
        if (this.IsRenamingAttributePrefix && !HasAttributeSuffix(replacementText))
        {
            return replacementText + AttributeSuffix;
        }
 
        return replacementText;
    }
 
    public static async Task<SymbolicRenameInfo> GetRenameInfoAsync(
        Document document, int position, CancellationToken cancellationToken)
    {
        var triggerToken = await GetTriggerTokenAsync(document, position, cancellationToken).ConfigureAwait(false);
        if (triggerToken == default)
            return new SymbolicRenameInfo(FeaturesResources.You_must_rename_an_identifier);
 
        return await GetRenameInfoAsync(document, triggerToken, cancellationToken).ConfigureAwait(false);
    }
 
    private static async Task<SyntaxToken> GetTriggerTokenAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var token = await syntaxTree.GetTouchingWordAsync(position, syntaxFacts, cancellationToken, findInsideTrivia: true).ConfigureAwait(false);
        return token;
    }
 
    private static async Task<SymbolicRenameInfo> GetRenameInfoAsync(
        Document document,
        SyntaxToken triggerToken,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        if (syntaxFacts.IsReservedOrContextualKeyword(triggerToken))
            return new SymbolicRenameInfo(FeaturesResources.You_must_rename_an_identifier);
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var semanticFacts = document.GetRequiredLanguageService<ISemanticFactsService>();
 
        var tokenRenameInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, triggerToken, cancellationToken);
 
        // Rename was invoked on a member group reference in a nameof expression.
        // Trigger the rename on any of the candidate symbols but force the 
        // RenameOverloads option to be on.
        var triggerSymbol = tokenRenameInfo.HasSymbols ? tokenRenameInfo.Symbols.First() : null;
        if (triggerSymbol == null)
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
 
        // see https://github.com/dotnet/roslyn/issues/10898
        // we are disabling rename for tuple fields for now
        // 1) compiler does not return correct location information in these symbols
        // 2) renaming tuple fields seems a complex enough thing to require some design
        if (triggerSymbol.ContainingType?.IsTupleType == true)
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
 
        // If rename is invoked on a member group reference in a nameof expression, then the
        // RenameOverloads option should be forced on.
        var forceRenameOverloads = tokenRenameInfo.IsMemberGroup;
        var symbol = await RenameUtilities.TryGetRenamableSymbolAsync(document, triggerToken.SpanStart, cancellationToken: cancellationToken).ConfigureAwait(false);
        if (symbol == null)
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
 
        if (symbol.Kind == SymbolKind.Alias && symbol.IsExtern)
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
 
        // Cannot rename constructors in VB.  TODO: this logic should be in the VB subclass of this type.
        if (symbol.Kind == SymbolKind.NamedType &&
            symbol.Language == LanguageNames.VisualBasic &&
            triggerToken.ToString().Equals("New", StringComparison.OrdinalIgnoreCase))
        {
            var originalSymbol = await SymbolFinder.FindSymbolAtPositionAsync(
                semanticModel, triggerToken.SpanStart, document.Project.Solution.Services, cancellationToken: cancellationToken).ConfigureAwait(false);
 
            if (originalSymbol != null && originalSymbol.IsConstructor())
                return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
        }
 
        var issuesService = document.GetRequiredLanguageService<IRenameIssuesService>();
        if (issuesService.CheckLanguageSpecificIssues(semanticModel, symbol, triggerToken, out var langError))
            return new SymbolicRenameInfo(langError);
 
        // we allow implicit locals and parameters of Event handlers
        if (symbol.IsImplicitlyDeclared &&
            symbol.Kind != SymbolKind.Local &&
            !(symbol.Kind == SymbolKind.Parameter &&
              symbol.ContainingSymbol.Kind == SymbolKind.Method &&
              symbol.ContainingType != null &&
              symbol.ContainingType.IsDelegateType() &&
              symbol.ContainingType.AssociatedSymbol != null))
        {
            // We enable the parameter in RaiseEvent, if the Event is declared with a signature. If the Event is declared as a 
            // delegate type, we do not have a connection between the delegate type and the event.
            // this prevents a rename in this case :(.
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
        }
 
        if (symbol.Kind == SymbolKind.Property && symbol.ContainingType.IsAnonymousType)
            return new SymbolicRenameInfo(FeaturesResources.Renaming_anonymous_type_members_is_not_yet_supported);
 
        if (symbol.IsErrorType())
            return new SymbolicRenameInfo(FeaturesResources.Please_resolve_errors_in_your_code_before_renaming_this_element);
 
        if (symbol.Kind == SymbolKind.Method && ((IMethodSymbol)symbol).MethodKind == MethodKind.UserDefinedOperator)
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_operators);
 
        var symbolLocations = symbol.Locations;
 
        // Does our symbol exist in an unchangeable location?
        using var _ = ArrayBuilder<DocumentSpan>.GetInstance(out var documentSpans);
        foreach (var location in symbolLocations)
        {
            if (location.IsInMetadata)
            {
                return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_elements_that_are_defined_in_metadata);
            }
            else if (location.IsInSource)
            {
                var solution = document.Project.Solution;
                var sourceDocument = solution.GetRequiredDocument(location.SourceTree);
 
                if (sourceDocument is SourceGeneratedDocument)
                {
                    // The file is generated so doesn't count towards valid spans 
                    // we can edit.
                    continue;
                }
 
                if (document.Project.IsSubmission)
                {
                    var projectIdOfLocation = sourceDocument.Project.Id;
 
                    if (solution.Projects.Any(p => p.IsSubmission && p.ProjectReferences.Any(r => r.ProjectId == projectIdOfLocation)))
                        return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_elements_from_previous_submissions);
                }
                else
                {
                    // We eventually need to return the symbol locations, so we must convert each location to a DocumentSpan since our return type is language-agnostic.
                    documentSpans.Add(new DocumentSpan(sourceDocument, location.SourceSpan));
                }
            }
            else
            {
                return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
            }
        }
 
        // No valid spans available in source we can edit
        if (documentSpans.Count == 0)
        {
            return new SymbolicRenameInfo(FeaturesResources.You_cannot_rename_this_element);
        }
 
        var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var triggerText = sourceText.ToString(triggerToken.Span);
 
        return new SymbolicRenameInfo(
            document, triggerToken, triggerText, symbol, forceRenameOverloads, documentSpans.ToImmutable());
    }
}