|
// 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());
}
}
|