// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using System.Text.RegularExpressions;
using System.Collections.Immutable;
namespace Microsoft.CodeAnalysis.Rename
{
///
/// A helper class that contains some of the methods and filters that must be used when
/// processing the raw results from the FindReferences API.
///
internal sealed partial class RenameLocations
{
internal static class ReferenceProcessing
{
///
/// 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.
///
public static async Task GetRenamableSymbolAsync(
Document document, int position, CancellationToken cancellationToken)
{
var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false);
if (symbol == null)
{
return default;
}
var symbolAndProjectId = SymbolAndProjectId.Create(symbol, document.Project.Id);
var definitionSymbol = await FindDefinitionSymbolAsync(symbolAndProjectId, document.Project.Solution, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(definitionSymbol.Symbol);
return definitionSymbol;
}
///
/// Given a symbol, finds the symbol that actually defines the name that we're using.
///
public static async Task FindDefinitionSymbolAsync(
SymbolAndProjectId symbolAndProjectId, Solution solution, CancellationToken cancellationToken)
{
var symbol = symbolAndProjectId.Symbol;
Contract.ThrowIfNull(symbol);
Contract.ThrowIfNull(solution);
// Make sure we're on the original source definition if we can be
var foundSymbolAndProjectId = await SymbolFinder.FindSourceDefinitionAsync(
symbolAndProjectId, solution, cancellationToken).ConfigureAwait(false);
var bestSymbolAndProjectId = foundSymbolAndProjectId.Symbol != null
? foundSymbolAndProjectId
: symbolAndProjectId;
symbol = bestSymbolAndProjectId.Symbol;
// 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 bestSymbolAndProjectId.WithSymbol(
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 bestSymbolAndProjectId.WithSymbol(
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 == MethodKind.Constructor ||
methodSymbol.MethodKind == MethodKind.StaticConstructor ||
methodSymbol.MethodKind == MethodKind.Destructor)
{
return bestSymbolAndProjectId.WithSymbol(
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 bestSymbolAndProjectId.WithSymbol(
fieldSymbol.AssociatedSymbol);
}
}
// in case this is e.g. an overridden property accessor, we'll treat the property itself as the definition symbol
var propertyAndProjectId = await GetPropertyFromAccessorOrAnOverride(bestSymbolAndProjectId, solution, cancellationToken).ConfigureAwait(false);
return propertyAndProjectId.Symbol != null
? propertyAndProjectId
: bestSymbolAndProjectId;
}
private static async Task 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(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);
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 IsPropertyAccessorOrAnOverride(referencedSymbol, solution, cancellationToken).ConfigureAwait(false) ||
await IsPropertyAccessorOrAnOverride(originalSymbol, solution, cancellationToken).ConfigureAwait(false))
{
return true;
}
if (referencedSymbol.ContainingSymbol != null &&
referencedSymbol.ContainingSymbol.Kind == SymbolKind.NamedType &&
((INamedTypeSymbol)referencedSymbol.ContainingSymbol).TypeKind == TypeKind.Interface &&
!originalSymbol.ExplicitInterfaceImplementations().Any(s => s.Equals(referencedSymbol)))
{
return true;
}
return false;
}
internal static async Task GetPropertyFromAccessorOrAnOverride(
SymbolAndProjectId symbolAndProjectId, Solution solution, CancellationToken cancellationToken)
{
var symbol = symbolAndProjectId.Symbol;
if (symbol.IsPropertyAccessor())
{
return symbolAndProjectId.WithSymbol(
((IMethodSymbol)symbol).AssociatedSymbol);
}
if (symbol.IsOverride && symbol.OverriddenMember() != null)
{
var originalSourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(
symbolAndProjectId.WithSymbol(symbol.OverriddenMember()),
solution, cancellationToken).ConfigureAwait(false);
if (originalSourceSymbol.Symbol != null)
{
return await GetPropertyFromAccessorOrAnOverride(originalSourceSymbol, solution, cancellationToken).ConfigureAwait(false);
}
}
if (symbol.Kind == SymbolKind.Method &&
symbol.ContainingType.TypeKind == TypeKind.Interface)
{
var methodImplementors = await SymbolFinder.FindImplementationsAsync(
symbolAndProjectId, solution, cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var methodImplementor in methodImplementors)
{
var propertyAccessorOrAnOverride = await GetPropertyFromAccessorOrAnOverride(methodImplementor, solution, cancellationToken).ConfigureAwait(false);
if (propertyAccessorOrAnOverride.Symbol != null)
{
return propertyAccessorOrAnOverride;
}
}
}
return default;
}
private static async Task IsPropertyAccessorOrAnOverride(
ISymbol symbol, Solution solution, CancellationToken cancellationToken)
{
var result = await GetPropertyFromAccessorOrAnOverride(
SymbolAndProjectId.Create(symbol, projectId: null),
solution, cancellationToken).ConfigureAwait(false);
return result.Symbol != null;
}
private static string TrimNameToAfterLastDot(string name)
{
int position = name.LastIndexOf('.');
if (position == -1)
{
return name;
}
else
{
return name.Substring(position + 1);
}
}
///
/// Given a ISymbol, returns the renameable locations for a given symbol.
///
public static async Task> GetRenamableDefinitionLocationsAsync(
ISymbol referencedSymbol, ISymbol originalSymbol, Solution solution, CancellationToken cancellationToken)
{
var shouldIncludeSymbol = await ShouldIncludeSymbolAsync(referencedSymbol, originalSymbol, solution, false, cancellationToken).ConfigureAwait(false);
if (!shouldIncludeSymbol)
{
return ImmutableArray.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.Empty;
}
var results = ArrayBuilder.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();
results.Add(new RenameLocation(location, solution.GetDocument(location.SourceTree).Id));
return results.ToImmutableAndFree();
}
var isRenamableAccessor = await IsPropertyAccessorOrAnOverride(referencedSymbol, solution, cancellationToken).ConfigureAwait(false);
foreach (var location in referencedSymbol.Locations)
{
if (location.IsInSource)
{
results.Add(new RenameLocation(
location,
solution.GetDocument(location.SourceTree).Id,
isRenamableAccessor: 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 syntaxFacts = solution.GetDocument(referencedSymbol.Locations[0].SourceTree).GetLanguageService();
var namedType = (INamedTypeSymbol)referencedSymbol;
foreach (var method in namedType.GetMembers().OfType())
{
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.IsKeyword(token) && token.ValueText == referencedSymbol.Name)
{
results.Add(new RenameLocation(location, solution.GetDocument(location.SourceTree).Id));
}
}
}
}
}
}
return results.ToImmutableAndFree();
}
internal static async Task> GetRenamableReferenceLocationsAsync(ISymbol referencedSymbol, ISymbol originalSymbol, ReferenceLocation location, Solution solution, CancellationToken cancellationToken)
{
var shouldIncludeSymbol = await ShouldIncludeSymbolAsync(referencedSymbol, originalSymbol, solution, true, cancellationToken).ConfigureAwait(false);
if (!shouldIncludeSymbol)
{
return SpecializedCollections.EmptyEnumerable();
}
// 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();
}
var results = new List();
// 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();
results.Add(new RenameLocation(aliasLocation, solution.GetDocument(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();
results.Add(new RenameLocation(aliasLocation, solution.GetDocument(aliasLocation.SourceTree).Id));
}
}
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 IsPropertyAccessorOrAnOverride(referencedSymbol, solution, cancellationToken).ConfigureAwait(false)));
}
}
return results;
}
internal static async Task, IEnumerable>> GetRenamableLocationsInStringsAndCommentsAsync(
ISymbol originalSymbol,
Solution solution,
ISet renameLocations,
bool renameInStrings,
bool renameInComments,
CancellationToken cancellationToken)
{
if (!renameInStrings && !renameInComments)
{
return new Tuple, IEnumerable>(null, null);
}
var renameText = originalSymbol.Name;
List stringLocations = renameInStrings ? new List() : null;
List commentLocations = renameInComments ? new List() : null;
foreach (var documentsGroupedByLanguage in RenameUtilities.GetDocumentsAffectedByRename(originalSymbol, solution, renameLocations).GroupBy(d => d.Project.Language))
{
var syntaxFactsLanguageService = solution.Workspace.Services.GetLanguageServices(documentsGroupedByLanguage.Key).GetService();
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 new Tuple, IEnumerable>(stringLocations, commentLocations);
}
private static async Task AddLocationsToRenameInStringsAsync(Document document, string renameText, ISyntaxFactsService syntaxFactsService, List renameLocations, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(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, isRenameInStrings: true, isRenameInComments: false);
}
}
private static async Task AddLocationsToRenameInCommentsAsync(Document document, string renameText, List renameLocations, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(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, isRenameInStrings: false, isRenameInComments: true);
}
}
private static void AddLocationsToRenameInStringsAndComments(
Document document,
SyntaxTree tree,
string renameText,
IEnumerable> renameStringsAndPositions,
List renameLocations,
bool isRenameInStrings,
bool isRenameInComments)
{
var regex = GetRegexForMatch(renameText);
foreach (var renameStringAndPosition in renameStringsAndPositions)
{
string renameString = renameStringAndPosition.Item1;
int renameStringPosition = renameStringAndPosition.Item2;
var containingSpan = renameStringAndPosition.Item3;
MatchCollection matches = regex.Matches(renameString);
foreach (Match match in matches)
{
int 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);
}
}
}
private static Regex GetRegexForMatch(string matchText)
{
var matchString = string.Format(@"\b{0}\b", matchText);
return new Regex(matchString, RegexOptions.CultureInvariant);
}
internal static string ReplaceMatchingSubStrings(string replaceInsideString, string matchText, string replacementText)
{
var regex = GetRegexForMatch(matchText);
return regex.Replace(replaceInsideString, replacementText);
}
}
}
}