From 25c93feda118f8ec7c408a20a63f6cd1579d9b42 Mon Sep 17 00:00:00 2001 From: Josef Pihrt Date: Sat, 19 Aug 2023 22:11:06 +0200 Subject: [PATCH] Make SymbolRenamer public (#1161) --- ChangeLog.md | 1 + .../RenameSymbolCommandResult.cs | 5 +- .../Commands/RenameSymbolCommand.cs | 55 +- .../Options/RenameSymbolCommandLineOptions.cs | 23 +- .../Rename/CliCompilationErrorResolution.cs} | 4 +- .../Rename/CliSymbolRenameState.cs | 333 +++++ src/Core/IsExternalInit.cs | 10 + .../CodeFixes/CodeFixerOptions.cs | 9 +- src/Workspaces.Core/Logging/LogHelpers.cs | 12 +- .../Rename/CompilationErrorResolution.cs | 24 + .../Rename/SymbolRenameProgress.cs | 56 + .../Rename/SymbolRenameResult.cs | 33 +- .../Rename/SymbolRenameState.cs | 864 +++++++++++++ src/Workspaces.Core/Rename/SymbolRenamer.cs | 1095 ++--------------- .../Rename/SymbolRenamerOptions.cs | 80 +- tools/generate_api_list.ps1 | 6 + 16 files changed, 1473 insertions(+), 1137 deletions(-) rename src/{Workspaces.Core/Rename/RenameErrorResolution.cs => CommandLine/Rename/CliCompilationErrorResolution.cs} (74%) create mode 100644 src/CommandLine/Rename/CliSymbolRenameState.cs create mode 100644 src/Core/IsExternalInit.cs create mode 100644 src/Workspaces.Core/Rename/CompilationErrorResolution.cs create mode 100644 src/Workspaces.Core/Rename/SymbolRenameProgress.cs create mode 100644 src/Workspaces.Core/Rename/SymbolRenameState.cs create mode 100644 tools/generate_api_list.ps1 diff --git a/ChangeLog.md b/ChangeLog.md index 9febd45f62..f81d50c04c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Enabled by default. - Add analyzer "Unnecessary enum flag" [RCS1258](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1258.md) ([#886](https://github.com/JosefPihrt/Roslynator/pull/886)). - Enabled by default. +- Make `Roslynator.Rename.SymbolRenamer` public ([#1161](https://github.com/josefpihrt/roslynator/pull/1161)) ### Fixed diff --git a/src/CommandLine/CommandResults/RenameSymbolCommandResult.cs b/src/CommandLine/CommandResults/RenameSymbolCommandResult.cs index b4135af688..e4b2017b5f 100644 --- a/src/CommandLine/CommandResults/RenameSymbolCommandResult.cs +++ b/src/CommandLine/CommandResults/RenameSymbolCommandResult.cs @@ -7,11 +7,8 @@ namespace Roslynator.CommandLine; internal class RenameSymbolCommandResult : CommandResult { - public RenameSymbolCommandResult(CommandStatus status, ImmutableArray renameResults) + public RenameSymbolCommandResult(CommandStatus status) : base(status) { - RenameResults = renameResults; } - - public ImmutableArray RenameResults { get; } } diff --git a/src/CommandLine/Commands/RenameSymbolCommand.cs b/src/CommandLine/Commands/RenameSymbolCommand.cs index adaa02f563..4bb1692d87 100644 --- a/src/CommandLine/Commands/RenameSymbolCommand.cs +++ b/src/CommandLine/Commands/RenameSymbolCommand.cs @@ -2,11 +2,12 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Roslynator.CommandLine.Rename; using Roslynator.Rename; using static Roslynator.Logger; @@ -18,8 +19,7 @@ public RenameSymbolCommand( RenameSymbolCommandLineOptions options, in ProjectFilter projectFilter, RenameScopeFilter scopeFilter, - Visibility visibility, - RenameErrorResolution errorResolution, + CliCompilationErrorResolution errorResolution, IEnumerable ignoredCompilerDiagnostics, int codeContext, Func predicate, @@ -27,7 +27,6 @@ public RenameSymbolCommand( { Options = options; ScopeFilter = scopeFilter; - Visibility = visibility; ErrorResolution = errorResolution; IgnoredCompilerDiagnostics = ignoredCompilerDiagnostics; CodeContext = codeContext; @@ -39,9 +38,7 @@ public RenameSymbolCommand( public RenameScopeFilter ScopeFilter { get; } - public Visibility Visibility { get; } - - public RenameErrorResolution ErrorResolution { get; } + public CliCompilationErrorResolution ErrorResolution { get; } public IEnumerable IgnoredCompilerDiagnostics { get; } @@ -57,8 +54,7 @@ public override async Task ExecuteAsync(ProjectOrSolu var projectFilter = new ProjectFilter(Options.Projects, Options.IgnoredProjects, Language); - SymbolRenamer renamer = null; - ImmutableArray results = default; + SymbolRenameState renamer = null; if (projectOrSolution.IsProject) { @@ -72,7 +68,7 @@ public override async Task ExecuteAsync(ProjectOrSolu Stopwatch stopwatch = Stopwatch.StartNew(); - results = await renamer.AnalyzeProjectAsync(project, cancellationToken); + await renamer.RenameSymbolsAsync(project, cancellationToken); stopwatch.Stop(); @@ -84,37 +80,36 @@ public override async Task ExecuteAsync(ProjectOrSolu renamer = GetSymbolRenamer(solution); - results = await renamer.AnalyzeSolutionAsync(f => projectFilter.IsMatch(f), cancellationToken); + await renamer.RenameSymbolsAsync(solution.Projects.Where(p => projectFilter.IsMatch(p)), cancellationToken); } - return new RenameSymbolCommandResult(CommandStatus.Success, results); + return new RenameSymbolCommandResult(CommandStatus.Success); - SymbolRenamer GetSymbolRenamer(Solution solution) + SymbolRenameState GetSymbolRenamer(Solution solution) { - VisibilityFilter visibilityFilter = Visibility switch + var options = new SymbolRenamerOptions() { - Visibility.Public => VisibilityFilter.All, - Visibility.Internal => VisibilityFilter.Internal | VisibilityFilter.Private, - Visibility.Private => VisibilityFilter.Private, - _ => throw new InvalidOperationException() + SkipTypes = (ScopeFilter & RenameScopeFilter.Type) != 0, + SkipMembers = (ScopeFilter & RenameScopeFilter.Member) != 0, + SkipLocals = (ScopeFilter & RenameScopeFilter.Local) != 0, + IncludeGeneratedCode = Options.IncludeGeneratedCode, + DryRun = Options.DryRun, }; - var options = new SymbolRenamerOptions( - scopeFilter: ScopeFilter, - visibilityFilter: visibilityFilter, - errorResolution: ErrorResolution, - ignoredCompilerDiagnosticIds: IgnoredCompilerDiagnostics, - codeContext: CodeContext, - includeGeneratedCode: Options.IncludeGeneratedCode, - ask: Options.Ask, - dryRun: Options.DryRun, - interactive: Options.Interactive); + if (IgnoredCompilerDiagnostics is not null) + { + foreach (string id in IgnoredCompilerDiagnostics) + options.IgnoredCompilerDiagnosticIds.Add(id); + } - return new SymbolRenamer( + return new CliSymbolRenameState( solution, predicate: Predicate, getNewName: GetNewName, - userDialog: new ConsoleDialog(ConsoleDialogDefinition.Default, " "), + ask: Options.Ask, + interactive: Options.Interactive, + codeContext: -1, + errorResolution: ErrorResolution, options: options); } } diff --git a/src/CommandLine/Options/RenameSymbolCommandLineOptions.cs b/src/CommandLine/Options/RenameSymbolCommandLineOptions.cs index 2b61a17028..6118983b1d 100644 --- a/src/CommandLine/Options/RenameSymbolCommandLineOptions.cs +++ b/src/CommandLine/Options/RenameSymbolCommandLineOptions.cs @@ -18,25 +18,18 @@ public class RenameSymbolCommandLineOptions : MSBuildCommandLineOptions longName: OptionNames.Ask, HelpText = "Ask whether to rename a symbol.")] public bool Ask { get; set; } -#if DEBUG - [Option( - longName: OptionNames.CodeContext, - HelpText = "Number of lines to display before and after a line with symbol definition.", - MetaValue = "", - Default = -1)] - public int CodeContext { get; set; } -#endif + [Option( shortName: OptionShortNames.DryRun, longName: "dry-run", HelpText = "List symbols to be renamed but do not save changes to a disk.")] public bool DryRun { get; set; } -#if DEBUG + [Option( longName: OptionNames.IgnoredCompilerDiagnostics, - HelpText = "A list of compiler diagnostics that should be ignored.")] + HelpText = "A space separated list of compiler diagnostics that should be ignored.")] public IEnumerable IgnoredCompilerDiagnostics { get; set; } -#endif + [Option( shortName: OptionShortNames.IncludeGeneratedCode, longName: "include-generated-code", @@ -83,12 +76,4 @@ public class RenameSymbolCommandLineOptions : MSBuildCommandLineOptions HelpText = "Symbol groups to be included. Allowed values are type, member and local.", MetaValue = "")] public IEnumerable Scope { get; set; } -#if DEBUG - [Option( - longName: OptionNames.Visibility, - Default = nameof(Roslynator.Visibility.Public), - HelpText = "Defines a maximal visibility of a symbol to be renamed. Allowed values are public (default), internal or private.", - MetaValue = "")] - public string Visibility { get; set; } -#endif } diff --git a/src/Workspaces.Core/Rename/RenameErrorResolution.cs b/src/CommandLine/Rename/CliCompilationErrorResolution.cs similarity index 74% rename from src/Workspaces.Core/Rename/RenameErrorResolution.cs rename to src/CommandLine/Rename/CliCompilationErrorResolution.cs index afd7f6b46b..1952698961 100644 --- a/src/Workspaces.Core/Rename/RenameErrorResolution.cs +++ b/src/CommandLine/Rename/CliCompilationErrorResolution.cs @@ -1,8 +1,8 @@ // Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Roslynator.Rename; +namespace Roslynator.CommandLine.Rename; -internal enum RenameErrorResolution +internal enum CliCompilationErrorResolution { None = 0, Abort = 1, diff --git a/src/CommandLine/Rename/CliSymbolRenameState.cs b/src/CommandLine/Rename/CliSymbolRenameState.cs new file mode 100644 index 0000000000..e3f67425c2 --- /dev/null +++ b/src/CommandLine/Rename/CliSymbolRenameState.cs @@ -0,0 +1,333 @@ +// Copyright (c) Josef Pihrt and Contributors. 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.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Roslynator.FindSymbols; +using Roslynator.Rename; +using static Roslynator.Logger; + +namespace Roslynator.CommandLine.Rename; + +internal class CliSymbolRenameState : SymbolRenameState +{ + public CliSymbolRenameState( + Solution solution, + Func predicate, + Func getNewName, + bool ask, + bool interactive, + int codeContext, + CliCompilationErrorResolution errorResolution, + SymbolRenamerOptions options) : base(solution, predicate, getNewName, options: options) + { + Ask = ask; + Interactive = interactive; + CodeContext = codeContext; + ErrorResolution = errorResolution; + DryRun = Options.DryRun; + } + + public bool Ask { get; set; } + + public bool Interactive { get; set; } + + public int CodeContext { get; } + + public bool DryRun { get; set; } + + public CliCompilationErrorResolution ErrorResolution { get; set; } + + public ConsoleDialog UserDialog { get; set; } + + protected override async Task RenameSymbolsAsync(ImmutableArray projects, CancellationToken cancellationToken = default) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + TimeSpan lastElapsed = TimeSpan.Zero; + + List renameScopes = GetRenameScopes().ToList(); + for (int i = 0; i < renameScopes.Count; i++) + { + WriteLine($"Rename {GetScopePluralName(renameScopes[i])} {$"{i + 1}/{renameScopes.Count}"}", Verbosity.Minimal); + + for (int j = 0; j < projects.Length; j++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Project project = CurrentSolution.GetProject(projects[j]); + + WriteLine($" Rename {GetScopePluralName(renameScopes[i])} in '{project.Name}' {$"{j + 1}/{projects.Length}"}", ConsoleColors.Cyan, Verbosity.Minimal); + + await AnalyzeProjectAsync(project, renameScopes[i], cancellationToken); + + WriteLine($" Done renaming {GetScopePluralName(renameScopes[i])} in '{project.Name}' in {stopwatch.Elapsed - lastElapsed:mm\\:ss\\.ff}", Verbosity.Normal); + } + } + + stopwatch.Stop(); + + WriteLine($"Done renaming symbols in solution '{CurrentSolution.FilePath}' in {stopwatch.Elapsed:mm\\:ss\\.ff}", Verbosity.Minimal); + } + + public override async Task RenameSymbolsAsync(Project project, CancellationToken cancellationToken = default) + { + List renameScopes = GetRenameScopes().ToList(); + + for (int i = 0; i < renameScopes.Count; i++) + { + WriteLine($"Rename {GetScopePluralName(renameScopes[i])} {$"{i + 1}/{renameScopes.Count}"}", Verbosity.Minimal); + + await AnalyzeProjectAsync(project, renameScopes[i], cancellationToken); + } + } + + protected override async Task<(string NewName, Solution NewSolution)> RenameSymbolAsync( + ISymbol symbol, + string symbolId, + List ignoreIds, + IFindSymbolService findSymbolService, + TextSpan span, + Document document, + CancellationToken cancellationToken) + { + LogHelpers.WriteSymbolDefinition(symbol, baseDirectoryPath: Path.GetDirectoryName(document.Project.FilePath), " ", Verbosity.Normal); + + if (ShouldWrite(Verbosity.Detailed) + || CodeContext >= 0) + { + SourceText sourceText = await document.GetTextAsync(cancellationToken); + Verbosity verbosity = (CodeContext >= 0) ? Verbosity.Normal : Verbosity.Detailed; + LogHelpers.WriteLineSpan(span, CodeContext, sourceText, " ", verbosity); + } + + Solution newSolution = null; + string newName = GetNewName?.Invoke(symbol) ?? symbol.Name; + bool interactive = Interactive; + int compilerErrorCount = 0; + + while (true) + { + string newName2 = GetSymbolNewName(newName, symbol, findSymbolService, interactive: interactive); + + if (newName2 is null) + { + ignoreIds?.Add(symbolId); + return default; + } + + newName = newName2; + + WriteLine( + $" Rename '{symbol.Name}' to '{newName}'", + (DryRun) ? ConsoleColors.DarkGray : ConsoleColors.Green, + Verbosity.Minimal); + + if (DryRun) + return default; + + try + { + newSolution = await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync( + CurrentSolution, + symbol, + new Microsoft.CodeAnalysis.Rename.SymbolRenameOptions(RenameOverloads: true), + newName, + cancellationToken); + } + catch (InvalidOperationException ex) + { + WriteLine($" Cannot rename '{symbol.Name}' to '{newName}': {ex.Message}", ConsoleColors.Yellow, Verbosity.Normal); +#if DEBUG + WriteLine(ex.ToString()); +#endif + ignoreIds?.Add(symbolId); + return default; + } + + if (ErrorResolution != CliCompilationErrorResolution.None) + { + Project newProject = newSolution.GetDocument(document.Id).Project; + Compilation compilation = await newProject.GetCompilationAsync(cancellationToken); + ImmutableArray diagnostics = compilation.GetDiagnostics(cancellationToken); + + compilerErrorCount = LogHelpers.WriteCompilerErrors( + diagnostics, + Path.GetDirectoryName(newProject.FilePath), + ignoredCompilerDiagnosticIds: Options.IgnoredCompilerDiagnosticIds, + indentation: " "); + } + + if (compilerErrorCount > 0 + && ErrorResolution != CliCompilationErrorResolution.List) + { + if (ErrorResolution == CliCompilationErrorResolution.Fix) + { + interactive = true; + continue; + } + else if (ErrorResolution == CliCompilationErrorResolution.Skip) + { + ignoreIds?.Add(symbolId); + return default; + } + else if (ErrorResolution == CliCompilationErrorResolution.Ask + && UserDialog is not null) + { + switch (UserDialog.ShowDialog("Rename symbol?")) + { + case DialogResult.None: + case DialogResult.No: + { + ignoreIds?.Add(symbolId); + return default; + } + case DialogResult.NoToAll: + { + ErrorResolution = CliCompilationErrorResolution.Skip; + ignoreIds?.Add(symbolId); + return default; + } + case DialogResult.Yes: + { + break; + } + case DialogResult.YesToAll: + { + ErrorResolution = CliCompilationErrorResolution.None; + break; + } + default: + { + throw new InvalidOperationException(); + } + } + } + else if (ErrorResolution == CliCompilationErrorResolution.Abort) + { + throw new OperationCanceledException(); + } + else + { + throw new InvalidOperationException(); + } + } + + break; + } + + if (Ask + && UserDialog is not null + && (compilerErrorCount == 0 || ErrorResolution != CliCompilationErrorResolution.Ask)) + { + switch (UserDialog.ShowDialog("Rename symbol?")) + { + case DialogResult.None: + case DialogResult.No: + { + ignoreIds?.Add(symbolId); + return default; + } + case DialogResult.NoToAll: + { + DryRun = true; + ignoreIds?.Add(symbolId); + return default; + } + case DialogResult.Yes: + { + break; + } + case DialogResult.YesToAll: + { + Ask = false; + break; + } + default: + { + throw new InvalidOperationException(); + } + } + } + + return (newName, newSolution); + } + + private static string GetSymbolNewName( + string newName, + ISymbol symbol, + IFindSymbolService findSymbolService, + bool interactive) + { + if (interactive) + { + bool isAttribute = symbol is INamedTypeSymbol typeSymbol + && typeSymbol.InheritsFrom(MetadataNames.System_Attribute); + + while (true) + { + string newName2 = ConsoleUtility.ReadUserInput(newName, " New name: "); + + if (string.IsNullOrEmpty(newName2)) + return null; + + if (string.Equals(newName, newName2, StringComparison.Ordinal)) + break; + + bool isValidIdentifier = findSymbolService.SyntaxFacts.IsValidIdentifier(newName2); + + if (isValidIdentifier + && (!isAttribute || newName2.EndsWith("Attribute"))) + { + newName = newName2; + break; + } + + ConsoleOut.WriteLine( + (!isValidIdentifier) + ? " New name is invalid" + : " New name is invalid, attribute name must end with 'Attribute'", + ConsoleColor.Yellow); + } + } + + if (string.Equals(symbol.Name, newName, StringComparison.Ordinal)) + return null; + + if (!interactive) + { + if (!findSymbolService.SyntaxFacts.IsValidIdentifier(newName)) + { + WriteLine($" New name is invalid: {newName}", ConsoleColors.Yellow, Verbosity.Minimal); + return null; + } + + if (symbol is INamedTypeSymbol typeSymbol + && typeSymbol.InheritsFrom(MetadataNames.System_Attribute) + && !newName.EndsWith("Attribute")) + { + WriteLine($" New name is invalid: {newName}. Attribute name must end with 'Attribute'.", ConsoleColors.Yellow, Verbosity.Minimal); + return null; + } + } + + return newName; + } + + private static string GetScopePluralName(RenameScope scope) + { + return scope switch + { + RenameScope.Type => "types", + RenameScope.Member => "members", + RenameScope.Local => "locals", + _ => throw new InvalidOperationException($"Unknown enum value '{scope}'."), + }; + } +} diff --git a/src/Core/IsExternalInit.cs b/src/Core/IsExternalInit.cs new file mode 100644 index 0000000000..d74a96d951 --- /dev/null +++ b/src/Core/IsExternalInit.cs @@ -0,0 +1,10 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Runtime.CompilerServices; + +using System.ComponentModel; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} diff --git a/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs b/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs index 1373b01a39..88f16df2f5 100644 --- a/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs +++ b/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs @@ -39,9 +39,14 @@ public CodeFixerOptions( ignoredDiagnosticIds: ignoredDiagnosticIds) { IgnoreCompilerErrors = ignoreCompilerErrors; - IgnoredCompilerDiagnosticIds = ignoredCompilerDiagnosticIds?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; DiagnosticIdsFixableOneByOne = diagnosticIdsFixableOneByOne?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; + if (ignoredCompilerDiagnosticIds is not null) + { + foreach (string id in ignoredCompilerDiagnosticIds) + IgnoredCompilerDiagnosticIds.Add(id); + } + if (diagnosticFixMap is not null) { DiagnosticFixMap = diagnosticFixMap @@ -70,7 +75,7 @@ public CodeFixerOptions( public bool IgnoreCompilerErrors { get; } - public ImmutableHashSet IgnoredCompilerDiagnosticIds { get; } + public HashSet IgnoredCompilerDiagnosticIds { get; } = new(); public string FileBanner { get; } diff --git a/src/Workspaces.Core/Logging/LogHelpers.cs b/src/Workspaces.Core/Logging/LogHelpers.cs index 83052a65fb..9147040740 100644 --- a/src/Workspaces.Core/Logging/LogHelpers.cs +++ b/src/Workspaces.Core/Logging/LogHelpers.cs @@ -477,17 +477,17 @@ static string GetSymbolTitle(ISymbol symbol) public static int WriteCompilerErrors( ImmutableArray diagnostics, string baseDirectoryPath = null, - ImmutableHashSet ignoredCompilerDiagnosticIds = null, + HashSet ignoredCompilerDiagnosticIds = null, IFormatProvider formatProvider = null, string indentation = null, int limit = 1000) { - ignoredCompilerDiagnosticIds ??= ImmutableHashSet.Empty; + IEnumerable filteredDiagnostics = diagnostics.Where(f => f.Severity == DiagnosticSeverity.Error); - using (IEnumerator en = diagnostics - .Where(f => f.Severity == DiagnosticSeverity.Error - && !ignoredCompilerDiagnosticIds.Contains(f.Id)) - .GetEnumerator()) + if (ignoredCompilerDiagnosticIds is not null) + filteredDiagnostics = filteredDiagnostics.Where(f => !ignoredCompilerDiagnosticIds.Contains(f.Id)); + + using (IEnumerator en = filteredDiagnostics.GetEnumerator()) { if (en.MoveNext()) { diff --git a/src/Workspaces.Core/Rename/CompilationErrorResolution.cs b/src/Workspaces.Core/Rename/CompilationErrorResolution.cs new file mode 100644 index 0000000000..f9f1005281 --- /dev/null +++ b/src/Workspaces.Core/Rename/CompilationErrorResolution.cs @@ -0,0 +1,24 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Roslynator.Rename; + +/// +/// Specifies how to handle compilation errors that occur after renaming a symbol. +/// +public enum CompilationErrorResolution +{ + /// + /// Ignore compilation errors. + /// + Ignore = 0, + + /// + /// Throw an exception if renaming of a symbol causes compilation errors. + /// + Throw = 1, + + /// + /// Skip renaming of a symbol if it causes compilation errors. + /// + Skip = 2, +} diff --git a/src/Workspaces.Core/Rename/SymbolRenameProgress.cs b/src/Workspaces.Core/Rename/SymbolRenameProgress.cs new file mode 100644 index 0000000000..3407962dd3 --- /dev/null +++ b/src/Workspaces.Core/Rename/SymbolRenameProgress.cs @@ -0,0 +1,56 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.CodeAnalysis; + +namespace Roslynator.Rename; + +/// +/// Represents in information about renaming a symbol. +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public readonly struct SymbolRenameProgress +{ + /// + /// Initializes a new instance of . + /// + /// + /// + /// + /// + internal SymbolRenameProgress( + ISymbol symbol, + string newName, + SymbolRenameResult result, + Exception exception = null) + { + Symbol = symbol; + NewName = newName; + Result = result; + Exception = exception; + } + + /// + /// Symbols being renamed. + /// + public ISymbol Symbol { get; } + + /// + /// New name of the symbol. + /// + public string NewName { get; } + + /// + /// /// Specifies if the rename operation succeeded or not. + /// + public SymbolRenameResult Result { get; } + + /// + /// Exception that occurred during renaming. May be null. + /// + public Exception Exception { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay => Symbol.ToDisplayString(SymbolDisplayFormats.FullName); +} diff --git a/src/Workspaces.Core/Rename/SymbolRenameResult.cs b/src/Workspaces.Core/Rename/SymbolRenameResult.cs index 94b4347019..d18e7e15ab 100644 --- a/src/Workspaces.Core/Rename/SymbolRenameResult.cs +++ b/src/Workspaces.Core/Rename/SymbolRenameResult.cs @@ -1,25 +1,24 @@ // Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Diagnostics; - namespace Roslynator.Rename; -[DebuggerDisplay("{DebuggerDisplay,nq}")] -internal readonly struct SymbolRenameResult +/// +/// Specifies the result of renaming a symbol. +/// +public enum SymbolRenameResult { - public SymbolRenameResult(string oldName, string newName, string symbolId) - { - OldName = oldName; - NewName = newName; - SymbolId = symbolId; - } - - public string OldName { get; } - - public string NewName { get; } + /// + /// Symbol was renamed successfully. + /// + Success, - public string SymbolId { get; } + /// + /// throws an exception. + /// + Error, - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => (OldName is not null) ? $"{OldName} {NewName} {SymbolId}" : "Uninitialized"; + /// + /// Renaming of a symbol caused compilation errors. + /// + CompilationError, } diff --git a/src/Workspaces.Core/Rename/SymbolRenameState.cs b/src/Workspaces.Core/Rename/SymbolRenameState.cs new file mode 100644 index 0000000000..df2422a1cb --- /dev/null +++ b/src/Workspaces.Core/Rename/SymbolRenameState.cs @@ -0,0 +1,864 @@ +// Copyright (c) Josef Pihrt and Contributors. 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.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Text; +using Roslynator.FindSymbols; +using Roslynator.Host.Mef; + +namespace Roslynator.Rename; + +internal class SymbolRenameState +{ + private readonly DiffTracker _diffTracker = new(); + + public SymbolRenameState( + Solution solution, + Func predicate, + Func getNewName, + SymbolRenamerOptions options = null, + IProgress progress = null) + { + Workspace = solution.Workspace; + + Predicate = predicate; + GetNewName = getNewName; + Progress = progress; + Options = options ?? new SymbolRenamerOptions(); + } + + protected Workspace Workspace { get; } + + protected Solution CurrentSolution => Workspace.CurrentSolution; + + public SymbolRenamerOptions Options { get; } + + protected IProgress Progress { get; } + + protected Func GetNewName { get; } + + protected Func Predicate { get; } + + private bool DryRun => Options.DryRun; + + public Task RenameSymbolsAsync(CancellationToken cancellationToken = default) + { + ImmutableArray projectIds = CurrentSolution + .GetProjectDependencyGraph() + .GetTopologicallySortedProjects(cancellationToken) + .ToImmutableArray(); + + return RenameSymbolsAsync(projectIds, cancellationToken); + } + + public Task RenameSymbolsAsync( + IEnumerable projects, + CancellationToken cancellationToken = default) + { + ImmutableArray projectIds = CurrentSolution + .GetProjectDependencyGraph() + .GetTopologicallySortedProjects(cancellationToken) + .Join(projects, id => id, p => p.Id, (id, _) => id) + .ToImmutableArray(); + + return RenameSymbolsAsync(projectIds, cancellationToken); + } + + protected virtual async Task RenameSymbolsAsync( + ImmutableArray projects, + CancellationToken cancellationToken = default) + { + foreach (RenameScope renameScope in GetRenameScopes()) + { + for (int j = 0; j < projects.Length; j++) + { + cancellationToken.ThrowIfCancellationRequested(); + + Project project = CurrentSolution.GetProject(projects[j]); + + await AnalyzeProjectAsync(project, renameScope, cancellationToken).ConfigureAwait(false); + } + } + } + + public virtual async Task RenameSymbolsAsync( + Project project, + CancellationToken cancellationToken = default) + { + foreach (RenameScope renameScope in GetRenameScopes()) + { + cancellationToken.ThrowIfCancellationRequested(); + + await AnalyzeProjectAsync(project, renameScope, cancellationToken).ConfigureAwait(false); + } + } + + protected IEnumerable GetRenameScopes() + { + if (!Options.SkipTypes) + yield return RenameScope.Type; + + if (!Options.SkipMembers) + yield return RenameScope.Member; + + if (!Options.SkipLocals) + yield return RenameScope.Local; + } + + protected async Task AnalyzeProjectAsync( + Project project, + RenameScope scope, + CancellationToken cancellationToken = default) + { + project = CurrentSolution.GetProject(project.Id); + + IFindSymbolService findSymbolService = MefWorkspaceServices.Default.GetService(project.Language); + + if (findSymbolService is null) + throw new InvalidOperationException($"Language '{project.Language}' not supported."); + + ImmutableArray previousIds = ImmutableArray.Empty; + ImmutableArray previousPreviousIds = ImmutableArray.Empty; + + var ignoreSymbolIds = new HashSet(StringComparer.Ordinal); + + while (true) + { + var symbolProvider = new SymbolProvider(Options.IncludeGeneratedCode); + + IEnumerable symbols = await symbolProvider.GetSymbolsAsync(project, scope, cancellationToken).ConfigureAwait(false); + + if (scope == RenameScope.Type) + { + symbols = SymbolListHelpers.SortTypeSymbols(symbols); + } + else if (scope == RenameScope.Member) + { + symbols = SymbolListHelpers.SortAndFilterMemberSymbols(symbols); + } + else if (scope == RenameScope.Local) + { + await RenameLocalSymbolsAsync(symbols, findSymbolService, cancellationToken).ConfigureAwait(false); + break; + } + else + { + throw new InvalidOperationException($"Unknown enum value '{scope}'."); + } + + ImmutableArray symbolData = symbols + .Select(symbol => new SymbolData(symbol, GetSymbolId(symbol), project.GetDocumentId(symbol.Locations[0].SourceTree))) + .ToImmutableArray(); + + int length = symbolData.Length; + + if (length == 0) + break; + + if (length == previousIds.Length + && !symbolData.Select(f => f.Id).Except(previousIds, StringComparer.Ordinal).Any()) + { + break; + } + + if (length == previousPreviousIds.Length + && !symbolData.Select(f => f.Id).Except(previousPreviousIds, StringComparer.Ordinal).Any()) + { + break; + } + + ImmutableArray symbolData2 = symbolData + .Where(f => !ignoreSymbolIds.Contains(f.Id) + && Predicate?.Invoke(f.Symbol) != false) + .ToImmutableArray(); + + List ignoredSymbolIds = await RenameSymbolsAsync( + symbolData2, + findSymbolService, + cancellationToken) + .ConfigureAwait(false); + + if (DryRun + || scope == RenameScope.Local + || symbolData2.Length == ignoredSymbolIds.Count) + { + break; + } + + foreach (string symbolId in ignoredSymbolIds) + { + Debug.Assert(!ignoreSymbolIds.Contains(symbolId), symbolId); + ignoreSymbolIds.Add(symbolId); + } + + previousPreviousIds = previousIds; + previousIds = ImmutableArray.CreateRange(symbolData, f => f.Id); + + project = CurrentSolution.GetProject(project.Id); + } + } + + private async Task> RenameSymbolsAsync( + IEnumerable symbols, + IFindSymbolService findSymbolService, + CancellationToken cancellationToken) + { + List ignoredSymbolIds = null; + DiffTracker diffTracker = null; + + if (!DryRun) + { + ignoredSymbolIds = new List(); + diffTracker = new DiffTracker(); + } + + foreach (SymbolData symbolData in symbols) + { + cancellationToken.ThrowIfCancellationRequested(); + + ISymbol symbol = symbolData.Symbol; + Document document = CurrentSolution.GetDocument(symbolData.DocumentId); + + if (document is null) + throw new InvalidOperationException($"Cannot find document for symbol '{symbol.ToDisplayString(SymbolDisplayFormats.FullName)}'."); + + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + TextSpan span = DiffTracker.GetCurrentSpan(symbol.Locations[0].SourceSpan, document.Id, diffTracker); + + await RenameSymbolAsync( + symbolData, + span, + document, + semanticModel, + findSymbolService: findSymbolService, + diffTracker: diffTracker, + ignoreIds: ignoredSymbolIds, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + return ignoredSymbolIds; + } + + private async Task RenameLocalSymbolsAsync( + IEnumerable symbols, + IFindSymbolService findSymbolService, + CancellationToken cancellationToken) + { + foreach (IGrouping grouping in symbols + .Where(f => f.IsKind(SymbolKind.Event, SymbolKind.Field, SymbolKind.Method, SymbolKind.Property) + && f.ContainingType.TypeKind != TypeKind.Enum) + .Select(f => f.DeclaringSyntaxReferences[0]) + .OrderBy(f => f.SyntaxTree.FilePath) + .GroupBy(f => f.SyntaxTree)) + { + cancellationToken.ThrowIfCancellationRequested(); + + Document document = CurrentSolution.GetDocument(grouping.Key); + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + foreach (SyntaxReference syntaxReference in grouping + .OrderByDescending(f => f.Span.Start)) + { + cancellationToken.ThrowIfCancellationRequested(); + + SyntaxNode node = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + DiffTracker diffTracker = (DryRun) ? null : new DiffTracker(); + HashSet localFunctionIndexes = null; + HashSet localSymbolIndexes = null; + int i = 0; + + foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(node, semanticModel, cancellationToken) + .OrderBy(f => f, LocalSymbolComparer.Instance)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (Predicate?.Invoke(symbol) != false) + { + if (symbol.Kind == SymbolKind.Method + || (symbol.IsKind(SymbolKind.Parameter, SymbolKind.TypeParameter) + && symbol.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction })) + { + (localFunctionIndexes ??= new HashSet()).Add(i); + } + else + { + (localSymbolIndexes ??= new HashSet()).Add(i); + } + } + + i++; + } + + if (localFunctionIndexes is not null) + { + await RenameLocalFunctionsAndItsParametersAsync( + node, + document.Id, + localFunctionIndexes, + diffTracker, + findSymbolService, + cancellationToken) + .ConfigureAwait(false); + } + + if (localSymbolIndexes is not null) + { + await RenameLocalsAndLambdaParametersAsync( + node, + document.Id, + localSymbolIndexes, + diffTracker, + findSymbolService, + cancellationToken) + .ConfigureAwait(false); + } + } + } + } + + private async Task RenameLocalFunctionsAndItsParametersAsync( + SyntaxNode node, + DocumentId documentId, + HashSet indexes, + DiffTracker diffTracker, + IFindSymbolService findSymbolService, + CancellationToken cancellationToken) + { + while (indexes.Count > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + Document document = CurrentSolution.GetDocument(documentId); + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + TextSpan span = DiffTracker.GetCurrentSpan(node.Span, documentId, diffTracker); + + if (!root.FullSpan.Contains(span)) + { + Debug.Fail(""); + break; + } + + SyntaxNode currentNode = root.FindNode(span); + + if (node.SpanStart != currentNode.SpanStart + || node.RawKind != currentNode.RawKind) + { + Debug.Fail(""); + break; + } + + int i = 0; + DiffTracker diffTracker2 = (DryRun) ? null : new DiffTracker(); + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(currentNode, semanticModel, cancellationToken) + .OrderBy(f => f, LocalSymbolComparer.Instance)) + { + if (indexes.Contains(i)) + { + if (semanticModel is null) + { + document = CurrentSolution.GetDocument(documentId); + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + } + + var symbolData = new SymbolData(symbol, GetSymbolId(symbol), documentId); + + TextSpan span2 = DiffTracker.GetCurrentSpan(symbol.Locations[0].SourceSpan, documentId, diffTracker2); + + bool success = await RenameSymbolAsync( + symbolData, + span2, + document, + semanticModel, + findSymbolService, + diffTracker: diffTracker2, + ignoreIds: null, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (success) + { + indexes.Remove(i); + } + else + { + break; + } + + if (indexes.Count == 0) + break; + + semanticModel = null; + } + + i++; + } + + if (diffTracker is not null + && diffTracker2 is not null) + { + diffTracker.Add(diffTracker2); + } + } + } + + private async Task RenameLocalsAndLambdaParametersAsync( + SyntaxNode node, + DocumentId documentId, + HashSet indexes, + DiffTracker diffTracker, + IFindSymbolService findSymbolService, + CancellationToken cancellationToken) + { + while (indexes.Count > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + Document document = CurrentSolution.GetDocument(documentId); + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + TextSpan span = DiffTracker.GetCurrentSpan(node.Span, documentId, diffTracker); + + if (!root.FullSpan.Contains(span)) + { + Debug.Fail(""); + break; + } + + SyntaxNode currentNode = root.FindNode(span); + + if (node.SpanStart != currentNode.SpanStart + || node.RawKind != currentNode.RawKind) + { + Debug.Fail(""); + break; + } + + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + int i = 0; + foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(currentNode, semanticModel, cancellationToken) + .OrderBy(f => f, LocalSymbolComparer.Instance)) + { + if (indexes.Remove(i)) + { + var symbolData = new SymbolData(symbol, symbol.Name, documentId); + + bool success = await RenameSymbolAsync( + symbolData, + symbol.Locations[0].SourceSpan, + document, + semanticModel, + findSymbolService, + diffTracker: diffTracker, + ignoreIds: null, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + Debug.Assert(success); + break; + } + + i++; + } + } + } + + private async Task RenameSymbolAsync( + SymbolData symbolData, + TextSpan span, + Document document, + SemanticModel semanticModel, + IFindSymbolService findSymbolService, + DiffTracker diffTracker, + List ignoreIds, + CancellationToken cancellationToken) + { + ISymbol symbol = symbolData.Symbol; + string symbolId = symbolData.Id; + + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (!root.FullSpan.Contains(span)) + { + Debug.Fail(span.ToString()); + return false; + } + + SyntaxToken identifier = root.FindToken(span.Start); + + Debug.Assert(span == identifier.Span, $"{span}\n{identifier.Span}"); + + SyntaxNode node = findSymbolService.FindDeclaration(identifier.Parent); + + if (!string.Equals(symbol.Name, identifier.ValueText, StringComparison.Ordinal)) + return false; + + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + ISymbol currentSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) + ?? semanticModel.GetSymbol(node, cancellationToken); + + if (currentSymbol is null) + { + Debug.Fail(symbolId); + + ignoreIds?.Add(symbolId); + return false; + } + + if (diffTracker is not null + && _diffTracker.SpanExists(span, document.Id)) + { + ignoreIds?.Add(GetSymbolId(currentSymbol)); + return false; + } + + string currentSymbolId = GetSymbolId(currentSymbol); + + if (currentSymbolId is not null) + { + if (!string.Equals(symbolId, currentSymbolId, StringComparison.Ordinal)) + return false; + } + else if (!string.Equals(symbol.Name, currentSymbol.Name, StringComparison.Ordinal)) + { + return false; + } + + symbol = currentSymbol; + + (string newName, Solution newSolution) = await RenameSymbolAsync(symbol, symbolId, ignoreIds, findSymbolService, span, document, cancellationToken).ConfigureAwait(false); + + IEnumerable referencedSymbols = await Microsoft.CodeAnalysis.FindSymbols.SymbolFinder.FindReferencesAsync( + symbol, + document.Project.Solution, + cancellationToken) + .ConfigureAwait(false); + + Solution oldSolution = CurrentSolution; + + if (!Workspace.TryApplyChanges(newSolution)) + throw new InvalidOperationException($"Cannot apply changes to a solution when renaming symbol '{symbol.ToDisplayString(SymbolDisplayFormats.FullName)}'."); + + if (diffTracker is null + && ignoreIds is null) + { + return true; + } + + int diff = newName.Length - symbol.Name.Length; + int oldIdentifierDiff = identifier.Text.Length - identifier.ValueText.Length; + + Debug.Assert(oldIdentifierDiff == 0 || oldIdentifierDiff == 1, oldIdentifierDiff.ToString()); + + HashSet locations = await GetLocationsAsync(referencedSymbols, symbol).ConfigureAwait(false); + + document = CurrentSolution.GetDocument(symbolData.DocumentId); + semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + Location oldLocation = symbol.Locations[0]; + TextSpan oldSpan = oldLocation.SourceSpan; + int diffCount = locations.Count(f => f.SourceTree == oldLocation.SourceTree && f.SourceSpan.Start < oldLocation.SourceSpan.Start); + var newSpan = new TextSpan(oldSpan.Start + (diff * diffCount), newName.Length); + SyntaxToken newIdentifier = root.FindToken(newSpan.Start); + int newIdentifierDiff = newIdentifier.Text.Length - newIdentifier.ValueText.Length; + int identifierDiff = newIdentifierDiff - oldIdentifierDiff; + + // '@default' > 'default' or vice versa + if (newIdentifier.Span != newSpan) + { + var newSpan2 = new TextSpan( + oldSpan.Start + ((diff + ((oldIdentifierDiff > 0) ? -1 : 1)) * diffCount), + newName.Length + ((oldIdentifierDiff > 0) ? 0 : 1)); + + SyntaxToken newIdentifier2 = root.FindToken(newSpan2.Start); + + Debug.Assert(newIdentifier2.Span == newSpan2, newIdentifier2.Span.ToString() + "\n" + newSpan2.ToString()); + + if (newIdentifier2.Span == newSpan2) + { + newSpan = newSpan2; + newIdentifier = newIdentifier2; + newIdentifierDiff = newIdentifier.Text.Length - newIdentifier.ValueText.Length; + + Debug.Assert(newIdentifierDiff == 0 || newIdentifierDiff == 1, newIdentifierDiff.ToString()); + + identifierDiff = newIdentifierDiff - oldIdentifierDiff; + } + } + + diff += identifierDiff; + + if (diffTracker is not null) + { + diffTracker.AddLocations(locations, diff, oldSolution); + _diffTracker.AddLocations(locations, diff, oldSolution); + } +#if DEBUG + Debug.Assert(string.Equals(newName, newIdentifier.ValueText, StringComparison.Ordinal), $"{newName}\n{newIdentifier.ValueText}"); + + foreach (IGrouping grouping in locations + .GroupBy(f => newSolution.GetDocument(oldSolution.GetDocumentId(f.SourceTree)).Id)) + { + DocumentId documentId = grouping.Key; + Document newDocument = newSolution.GetDocument(documentId); + int offset = 0; + + foreach (TextSpan span2 in grouping + .Select(f => f.SourceSpan) + .OrderBy(f => f.Start)) + { + var s = new TextSpan(span2.Start + offset, span2.Length + diff); + SyntaxNode r = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxToken t = r.FindToken(s.Start, findInsideTrivia: true); + + // C# string literal token (inside SuppressMessageAttribute) + if (t.RawKind == 8511) + { + string text = t.ValueText.Substring(s.Start - t.SpanStart, newName.Length); + + Debug.Assert(string.Equals(newName, text, StringComparison.Ordinal), text); + } + else + { + Debug.Assert(findSymbolService.CanBeRenamed(t)); + Debug.Assert(t.Span == s); + } + + offset += diff; + } + } +#endif + if (string.Equals(newName, newIdentifier.ValueText, StringComparison.Ordinal) + && ignoreIds is not null) + { + SyntaxNode newNode = findSymbolService.FindDeclaration(newIdentifier.Parent); + + symbol = semanticModel.GetDeclaredSymbol(newNode, cancellationToken) + ?? semanticModel.GetSymbol(newNode, cancellationToken); + + Debug.Assert(symbol is not null, GetSymbolId(symbol)); + + if (symbol is not null) + ignoreIds.Add(GetSymbolId(symbol)); + } + + return true; + + async Task> GetLocationsAsync(IEnumerable referencedSymbols, ISymbol symbol = null) + { + var locations = new HashSet(); + + foreach (Location location in GetLocations()) + { + TextSpan span = location.SourceSpan; + + if (symbol is not null) + { + // 'this' and 'base' constructor references + if (symbol.Name.Length == 4 + || symbol.Name.Length != span.Length) + { + SyntaxNode root = await location.SourceTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxToken token = root.FindToken(span.Start, findInsideTrivia: true); + + Debug.Assert(token.Span == span); + + if (token.Span == span + && !findSymbolService.CanBeRenamed(token)) + { + continue; + } + } + } + + locations.Add(location); + } + + return locations; + + IEnumerable GetLocations() + { + foreach (ReferencedSymbol referencedSymbol in referencedSymbols) + { + if (referencedSymbol.Definition is not IMethodSymbol methodSymbol + || !methodSymbol.MethodKind.Is(MethodKind.PropertyGet, MethodKind.PropertySet, MethodKind.EventAdd, MethodKind.EventRemove)) + { + foreach (Location location in referencedSymbol.Definition.Locations) + yield return location; + } + + foreach (ReferenceLocation referenceLocation in referencedSymbol.Locations) + { + if (!referenceLocation.IsImplicit && !referenceLocation.IsCandidateLocation) + yield return referenceLocation.Location; + } + } + } + } + } + + protected virtual async Task<(string NewName, Solution NewSolution)> RenameSymbolAsync( + ISymbol symbol, + string symbolId, + List ignoreIds, + IFindSymbolService findSymbolService, + TextSpan span, + Document document, + CancellationToken cancellationToken) + { + Solution newSolution = null; + string newName = GetNewName(symbol); + + if (!findSymbolService.SyntaxFacts.IsValidIdentifier(newName)) + throw new InvalidOperationException($"'{newName}' is not valid identifier. Cannot rename symbol '{symbol.ToDisplayString(SymbolDisplayFormats.FullName)}'."); + + if (DryRun) + { + Report(symbol, newName, SymbolRenameResult.Success); + return default; + } + + try + { + var options = new Microsoft.CodeAnalysis.Rename.SymbolRenameOptions( + RenameOverloads: Options.RenameOverloads, + RenameInStrings: Options.RenameInStrings, + RenameInComments: Options.RenameInComments, + RenameFile: Options.RenameFile); + + newSolution = await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync( + CurrentSolution, + symbol, + options, + newName, + cancellationToken) + .ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + Report(symbol, newName, SymbolRenameResult.Error, ex); + + ignoreIds?.Add(symbolId); + return default; + } + + CompilationErrorResolution resolution = Options.CompilationErrorResolution; + + if (resolution != CompilationErrorResolution.Ignore) + { + Project newProject = newSolution.GetDocument(document.Id).Project; + Compilation compilation = await newProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + foreach (Diagnostic diagnostic in compilation.GetDiagnostics(cancellationToken)) + { + if (!Options.IgnoredCompilerDiagnosticIds.Contains(diagnostic.Id)) + { + Report(symbol, newName, SymbolRenameResult.CompilationError); + + if (resolution == CompilationErrorResolution.Skip) + { + ignoreIds?.Add(symbolId); + return default; + } + else if (resolution == CompilationErrorResolution.Throw) + { + throw new InvalidOperationException("Renaming of a symbol causes compiler diagnostics."); + } + else + { + throw new InvalidOperationException($"Unknown enum value '{resolution}'."); + } + } + } + } + + Report(symbol, newName, SymbolRenameResult.Success); + + return (newName, newSolution); + } + + private void Report( + ISymbol symbol, + string newName, + SymbolRenameResult result, + Exception exception = null) + { + Progress?.Report(new SymbolRenameProgress(symbol, newName, result, exception)); + } + + private static string GetSymbolId(ISymbol symbol) + { + string id; + + switch (symbol.Kind) + { + case SymbolKind.Local: + { + return null; + } + case SymbolKind.Method: + { + var methodSymbol = (IMethodSymbol)symbol; + + if (methodSymbol.MethodKind == MethodKind.LocalFunction) + { + id = symbol.Name; + ISymbol cs = symbol.ContainingSymbol; + + while (cs is IMethodSymbol { MethodKind: MethodKind.LocalFunction }) + { + id = cs.Name + "." + id; + cs = cs.ContainingSymbol; + } + + return id; + } + + break; + } + case SymbolKind.Parameter: + case SymbolKind.TypeParameter: + { + ISymbol cs = symbol.ContainingSymbol; + + if (cs is IMethodSymbol methodSymbol) + { + if (methodSymbol.MethodKind == MethodKind.AnonymousFunction) + return null; + + if (methodSymbol.MethodKind == MethodKind.LocalFunction) + { + id = cs.Name + " " + symbol.Name; + cs = cs.ContainingSymbol; + + while (cs is IMethodSymbol { MethodKind: MethodKind.LocalFunction }) + { + id = cs.Name + "." + id; + cs = cs.ContainingSymbol; + } + + return id; + } + } + + return symbol.ContainingSymbol.GetDocumentationCommentId() + " " + (symbol.GetDocumentationCommentId() ?? symbol.Name); + } + } + + return symbol.GetDocumentationCommentId(); + } +} diff --git a/src/Workspaces.Core/Rename/SymbolRenamer.cs b/src/Workspaces.Core/Rename/SymbolRenamer.cs index 93cfa6970b..55e4b4ec36 100644 --- a/src/Workspaces.Core/Rename/SymbolRenamer.cs +++ b/src/Workspaces.Core/Rename/SymbolRenamer.cs @@ -2,1070 +2,117 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Text; -using Roslynator.FindSymbols; -using Roslynator.Host.Mef; -using static Roslynator.Logger; namespace Roslynator.Rename; -internal class SymbolRenamer +/// +/// Provides a set of static methods for renaming symbols in a solution or a project. +/// +public static class SymbolRenamer { - private readonly DiffTracker _diffTracker = new(); - private readonly Func _predicate; - private readonly Func _getNewName; - - public SymbolRenamer( + /// + /// Renames symbols in the specified solution. + /// + /// + /// + /// + /// + /// + /// + public static async Task RenameSymbolsAsync( Solution solution, Func predicate, Func getNewName, - IUserDialog userDialog = null, - SymbolRenamerOptions options = null) - { - Workspace = solution.Workspace; - - _predicate = predicate; - _getNewName = getNewName; - - UserDialog = userDialog; - Options = options ?? SymbolRenamerOptions.Default; - - ErrorResolution = Options.ErrorResolution; - Ask = Options.Ask; - DryRun = Options.DryRun; - } - - public Workspace Workspace { get; } - - private Solution CurrentSolution => Workspace.CurrentSolution; - - public IUserDialog UserDialog { get; } - - public SymbolRenamerOptions Options { get; } - - private RenameErrorResolution ErrorResolution { get; set; } - - private bool Ask { get; set; } - - private bool DryRun { get; set; } - - public async Task> AnalyzeSolutionAsync( - Func predicate, - CancellationToken cancellationToken = default) - { - ImmutableArray projects = CurrentSolution - .GetProjectDependencyGraph() - .GetTopologicallySortedProjects(cancellationToken) - .ToImmutableArray(); - - var results = new List>(); - Stopwatch stopwatch = Stopwatch.StartNew(); - TimeSpan lastElapsed = TimeSpan.Zero; - - List renameScopes = GetRenameScopes(); - - for (int i = 0; i < renameScopes.Count; i++) - { - WriteLine($"Rename {GetPluralName(renameScopes[i])} {$"{i + 1}/{renameScopes.Count}"}", Verbosity.Minimal); - - for (int j = 0; j < projects.Length; j++) - { - cancellationToken.ThrowIfCancellationRequested(); - - Project project = CurrentSolution.GetProject(projects[j]); - - if (predicate is null || predicate(project)) - { - WriteLine($" Rename {GetPluralName(renameScopes[i])} in '{project.Name}' {$"{j + 1}/{projects.Length}"}", ConsoleColors.Cyan, Verbosity.Minimal); - - ImmutableArray projectResults = await AnalyzeProjectAsync(project, renameScopes[i], cancellationToken).ConfigureAwait(false); - - results.Add(projectResults); - - WriteLine($" Done renaming {GetPluralName(renameScopes[i])} in '{project.Name}' in {stopwatch.Elapsed - lastElapsed:mm\\:ss\\.ff}", Verbosity.Normal); - } - else - { - WriteLine($" Skip '{project.Name}' {$"{j + 1}/{projects.Length}"}", ConsoleColors.DarkGray, Verbosity.Minimal); - } - - lastElapsed = stopwatch.Elapsed; - } - } - - stopwatch.Stop(); - - WriteLine($"Done renaming symbols in solution '{CurrentSolution.FilePath}' in {stopwatch.Elapsed:mm\\:ss\\.ff}", Verbosity.Minimal); - - return results.SelectMany(f => f).ToImmutableArray(); - } - - public async Task> AnalyzeProjectAsync( - Project project, + SymbolRenamerOptions options = null, + IProgress progress = null, CancellationToken cancellationToken = default) { - ImmutableArray results = ImmutableArray.Empty; - - List renameScopes = GetRenameScopes(); - - for (int i = 0; i < renameScopes.Count; i++) - { - WriteLine($"Rename {GetPluralName(renameScopes[i])} {$"{i + 1}/{renameScopes.Count}"}", Verbosity.Minimal); - results.AddRange(await AnalyzeProjectAsync(project, renameScopes[i], cancellationToken).ConfigureAwait(false)); - } - - return results; - } - - private static string GetPluralName(RenameScope scope) - { - return scope switch - { - RenameScope.Type => "types", - RenameScope.Member => "members", - RenameScope.Local => "locals", - _ => throw new InvalidOperationException($"Unknown enum value '{scope}'."), - }; - } - - private List GetRenameScopes() - { - var renameScopes = new List(); - - if ((Options.ScopeFilter & RenameScopeFilter.Type) != 0) - renameScopes.Add(RenameScope.Type); - - if ((Options.ScopeFilter & RenameScopeFilter.Member) != 0) - renameScopes.Add(RenameScope.Member); + var renamer = new SymbolRenameState(solution, predicate, getNewName, options, progress); - if ((Options.ScopeFilter & RenameScopeFilter.Local) != 0) - renameScopes.Add(RenameScope.Local); - - return renameScopes; + await renamer.RenameSymbolsAsync(cancellationToken).ConfigureAwait(false); } - private async Task> AnalyzeProjectAsync( - Project project, - RenameScope scope, + /// + /// Renames symbols in the specified projects. All projects must be in the same solution. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task RenameSymbolsAsync( + IEnumerable projects, + Func predicate, + Func getNewName, + SymbolRenamerOptions options = null, + IProgress progress = null, CancellationToken cancellationToken = default) { - project = CurrentSolution.GetProject(project.Id); - - IFindSymbolService service = MefWorkspaceServices.Default.GetService(project.Language); - - if (service is null) - return ImmutableArray.Empty; - - ImmutableArray previousIds = ImmutableArray.Empty; - ImmutableArray previousPreviousIds = ImmutableArray.Empty; - - ImmutableArray.Builder results = ImmutableArray.CreateBuilder(); - - var ignoreSymbolIds = new HashSet(StringComparer.Ordinal); - - while (true) - { - var symbolProvider = new SymbolProvider(Options.IncludeGeneratedCode); - - IEnumerable symbols = await symbolProvider.GetSymbolsAsync(project, scope, cancellationToken).ConfigureAwait(false); - - if (scope == RenameScope.Type) - { - symbols = SymbolListHelpers.SortTypeSymbols(symbols); - } - else if (scope == RenameScope.Member) - { - symbols = SymbolListHelpers.SortAndFilterMemberSymbols(symbols); - } - else if (scope == RenameScope.Local) - { - List localResults = await RenameLocalSymbolsAsync(symbols, service, cancellationToken).ConfigureAwait(false); - - results.AddRange(localResults); - break; - } - else - { - throw new InvalidOperationException(); - } - - ImmutableArray symbolData = symbols - .Select(symbol => new SymbolData(symbol, GetSymbolId(symbol), project.GetDocumentId(symbol.Locations[0].SourceTree))) - .ToImmutableArray(); - - int length = symbolData.Length; - - if (length == 0) - break; - - if (length == previousIds.Length - && !symbolData.Select(f => f.Id).Except(previousIds, StringComparer.Ordinal).Any()) - { - break; - } - - if (length == previousPreviousIds.Length - && !symbolData.Select(f => f.Id).Except(previousPreviousIds, StringComparer.Ordinal).Any()) - { - break; - } - - ImmutableArray symbolData2 = symbolData - .Where(f => !ignoreSymbolIds.Contains(f.Id) - && f.Symbol.IsVisible(Options.VisibilityFilter) - && _predicate?.Invoke(f.Symbol) != false) - .ToImmutableArray(); - - (List symbolResults, List ignoreIds) = await RenameSymbolsAsync( - symbolData2, - service, - cancellationToken) - .ConfigureAwait(false); - - results.AddRange(symbolResults); - - if (DryRun - || scope == RenameScope.Local - || symbolData2.Length == ignoreIds.Count) - { - break; - } - - foreach (string id in ignoreIds) - { - Debug.Assert(!ignoreSymbolIds.Contains(id), id); - ignoreSymbolIds.Add(id); - } - - previousPreviousIds = previousIds; - previousIds = ImmutableArray.CreateRange(symbolData, f => f.Id); - - project = CurrentSolution.GetProject(project.Id); - } - - return results.ToImmutableArray(); - } - - private async Task<(List results, List ignoreIds)> RenameSymbolsAsync( - IEnumerable symbols, - IFindSymbolService findSymbolService, - CancellationToken cancellationToken) - { - var results = new List(); - List ignoreIds = null; - DiffTracker diffTracker = null; + if (projects is null) + throw new ArgumentNullException(nameof(projects)); - if (!DryRun) - { - ignoreIds = new List(); - diffTracker = new DiffTracker(); - } + List projectList = EnumerateProjects().ToList(); - foreach (SymbolData symbolData in symbols) - { - cancellationToken.ThrowIfCancellationRequested(); + if (projectList.Count == 0) + throw new ArgumentException("Sequence of projects contains no elements.", nameof(projects)); - ISymbol symbol = symbolData.Symbol; - Document document = CurrentSolution.GetDocument(symbolData.DocumentId); - - if (document is null) - { - ignoreIds?.Add(symbolData.Id); - WriteLine($" Cannot find document for '{symbol.Name}'", ConsoleColors.Yellow, Verbosity.Detailed); - continue; - } + var renamer = new SymbolRenameState(projectList[0].Solution, predicate, getNewName, options, progress); - SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + await renamer.RenameSymbolsAsync(projectList, cancellationToken).ConfigureAwait(false); - TextSpan span = DiffTracker.GetCurrentSpan(symbol.Locations[0].SourceSpan, document.Id, diffTracker); - - await RenameSymbolAsync( - symbolData, - span, - document, - semanticModel, - findSymbolService: findSymbolService, - diffTracker: diffTracker, - ignoreIds: ignoreIds, - results: results, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - return (results, ignoreIds); - } - - private async Task> RenameLocalSymbolsAsync( - IEnumerable symbols, - IFindSymbolService findSymbolService, - CancellationToken cancellationToken) - { - var results = new List(); - - foreach (IGrouping grouping in symbols - .Where(f => f.IsKind(SymbolKind.Event, SymbolKind.Field, SymbolKind.Method, SymbolKind.Property) - && f.ContainingType.TypeKind != TypeKind.Enum) - .Select(f => f.DeclaringSyntaxReferences[0]) - .OrderBy(f => f.SyntaxTree.FilePath) - .GroupBy(f => f.SyntaxTree)) + IEnumerable EnumerateProjects() { - cancellationToken.ThrowIfCancellationRequested(); - - Document document = CurrentSolution.GetDocument(grouping.Key); - SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - foreach (SyntaxReference syntaxReference in grouping - .OrderByDescending(f => f.Span.Start)) + using (IEnumerator en = projects.GetEnumerator()) { - cancellationToken.ThrowIfCancellationRequested(); - - SyntaxNode node = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); - DiffTracker diffTracker = (DryRun) ? null : new DiffTracker(); - HashSet localFunctionIndexes = null; - HashSet localSymbolIndexes = null; - int i = 0; - - foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(node, semanticModel, cancellationToken) - .OrderBy(f => f, LocalSymbolComparer.Instance)) + if (en.MoveNext()) { - cancellationToken.ThrowIfCancellationRequested(); + Solution solution = en.Current.Solution; + + yield return en.Current; - if (_predicate?.Invoke(symbol) != false) + while (en.MoveNext()) { - if (symbol.Kind == SymbolKind.Method - || (symbol.IsKind(SymbolKind.Parameter, SymbolKind.TypeParameter) - && symbol.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction })) + if (en.Current.Solution.Id == solution.Id) { - (localFunctionIndexes ??= new HashSet()).Add(i); + yield return en.Current; } else { - (localSymbolIndexes ??= new HashSet()).Add(i); + throw new InvalidOperationException("All projects must be from the same solution."); } } - - i++; - } - - if (localFunctionIndexes is not null) - { - await RenameLocalFunctionsAndItsParametersAsync( - node, - document.Id, - localFunctionIndexes, - results, - diffTracker, - findSymbolService, - cancellationToken) - .ConfigureAwait(false); - } - - if (localSymbolIndexes is not null) - { - await RenameLocalsAndLambdaParametersAsync( - node, - document.Id, - localSymbolIndexes, - results, - diffTracker, - findSymbolService, - cancellationToken) - .ConfigureAwait(false); - } - } - } - - return results; - } - - private async Task RenameLocalFunctionsAndItsParametersAsync( - SyntaxNode node, - DocumentId documentId, - HashSet indexes, - List results, - DiffTracker diffTracker, - IFindSymbolService findSymbolService, - CancellationToken cancellationToken) - { - while (indexes.Count > 0) - { - cancellationToken.ThrowIfCancellationRequested(); - - Document document = CurrentSolution.GetDocument(documentId); - SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - TextSpan span = DiffTracker.GetCurrentSpan(node.Span, documentId, diffTracker); - - if (!root.FullSpan.Contains(span)) - { - Debug.Fail(""); - break; - } - - SyntaxNode currentNode = root.FindNode(span); - - if (node.SpanStart != currentNode.SpanStart - || node.RawKind != currentNode.RawKind) - { - Debug.Fail(""); - break; - } - - int i = 0; - DiffTracker diffTracker2 = (DryRun) ? null : new DiffTracker(); - SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(currentNode, semanticModel, cancellationToken) - .OrderBy(f => f, LocalSymbolComparer.Instance)) - { - if (indexes.Contains(i)) - { - if (semanticModel is null) - { - document = CurrentSolution.GetDocument(documentId); - semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - } - - var symbolData = new SymbolData(symbol, GetSymbolId(symbol), documentId); - - TextSpan span2 = DiffTracker.GetCurrentSpan(symbol.Locations[0].SourceSpan, documentId, diffTracker2); - - bool success = await RenameSymbolAsync( - symbolData, - span2, - document, - semanticModel, - findSymbolService, - diffTracker: diffTracker2, - ignoreIds: null, - results: results, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - if (success) - { - indexes.Remove(i); - } - else - { - break; - } - - if (indexes.Count == 0) - break; - - semanticModel = null; - } - - i++; - } - - if (diffTracker is not null - && diffTracker2 is not null) - { - diffTracker.Add(diffTracker2); - } - } - } - - private async Task RenameLocalsAndLambdaParametersAsync( - SyntaxNode node, - DocumentId documentId, - HashSet indexes, - List results, - DiffTracker diffTracker, - IFindSymbolService findSymbolService, - CancellationToken cancellationToken) - { - while (indexes.Count > 0) - { - cancellationToken.ThrowIfCancellationRequested(); - - Document document = CurrentSolution.GetDocument(documentId); - SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - TextSpan span = DiffTracker.GetCurrentSpan(node.Span, documentId, diffTracker); - - if (!root.FullSpan.Contains(span)) - { - Debug.Fail(""); - break; - } - - SyntaxNode currentNode = root.FindNode(span); - - if (node.SpanStart != currentNode.SpanStart - || node.RawKind != currentNode.RawKind) - { - Debug.Fail(""); - break; - } - - SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - int i = 0; - foreach (ISymbol symbol in findSymbolService.FindLocalSymbols(currentNode, semanticModel, cancellationToken) - .OrderBy(f => f, LocalSymbolComparer.Instance)) - { - if (indexes.Remove(i)) - { - var symbolData = new SymbolData(symbol, symbol.Name, documentId); - - bool success = await RenameSymbolAsync( - symbolData, - symbol.Locations[0].SourceSpan, - document, - semanticModel, - findSymbolService, - diffTracker: diffTracker, - ignoreIds: null, - results: results, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - Debug.Assert(success); - break; } - - i++; } } } - private async Task RenameSymbolAsync( - SymbolData symbolData, - TextSpan span, - Document document, - SemanticModel semanticModel, - IFindSymbolService findSymbolService, - DiffTracker diffTracker, - List ignoreIds, - List results, - CancellationToken cancellationToken) - { - ISymbol symbol = symbolData.Symbol; - string symbolId = symbolData.Id; - - SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - if (!root.FullSpan.Contains(span)) - { - Debug.Fail(span.ToString()); - return false; - } - - SyntaxToken identifier = root.FindToken(span.Start); - - Debug.Assert(span == identifier.Span, $"{span}\n{identifier.Span}"); - - SyntaxNode node = findSymbolService.FindDeclaration(identifier.Parent); - - if (!string.Equals(symbol.Name, identifier.ValueText, StringComparison.Ordinal)) - return false; - - semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - - ISymbol currentSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) - ?? semanticModel.GetSymbol(node, cancellationToken); - - if (currentSymbol is null) - { - Debug.Fail(symbolId); - - ignoreIds?.Add(symbolId); - return false; - } - - if (diffTracker is not null - && _diffTracker.SpanExists(span, document.Id)) - { - ignoreIds?.Add(GetSymbolId(currentSymbol)); - return false; - } - - string currentSymbolId = GetSymbolId(currentSymbol); - - if (currentSymbolId is not null) - { - if (!string.Equals(symbolId, currentSymbolId, StringComparison.Ordinal)) - return false; - } - else if (!string.Equals(symbol.Name, currentSymbol.Name, StringComparison.Ordinal)) - { - return false; - } - - symbol = currentSymbol; - - LogHelpers.WriteSymbolDefinition(symbol, baseDirectoryPath: Path.GetDirectoryName(document.Project.FilePath), " ", Verbosity.Normal); - - if (ShouldWrite(Verbosity.Detailed) - || Options.CodeContext >= 0) - { - SourceText sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - Verbosity verbosity = (Options.CodeContext >= 0) ? Verbosity.Normal : Verbosity.Detailed; - LogHelpers.WriteLineSpan(span, Options.CodeContext, sourceText, " ", verbosity); - } - - Solution newSolution = null; - string newName = _getNewName?.Invoke(symbol) ?? symbol.Name; - bool interactive = Options.Interactive; - int compilerErrorCount = 0; - - while (true) - { - string newName2 = GetNewName(newName, symbol, findSymbolService, interactive: interactive); - - if (newName2 is null) - { - ignoreIds?.Add(symbolId); - return true; - } - - newName = newName2; - - WriteLine( - $" Rename '{symbol.Name}' to '{newName}'", - (DryRun) ? ConsoleColors.DarkGray : ConsoleColors.Green, - Verbosity.Minimal); - - if (DryRun) - { - results.Add(new SymbolRenameResult(symbol.Name, newName, symbolId)); - return true; - } - - try - { - newSolution = await Microsoft.CodeAnalysis.Rename.Renamer.RenameSymbolAsync( - CurrentSolution, - symbol, - new Microsoft.CodeAnalysis.Rename.SymbolRenameOptions(RenameOverloads: true), - newName, - cancellationToken) - .ConfigureAwait(false); - } - catch (InvalidOperationException ex) - { - WriteLine($" Cannot rename '{symbol.Name}' to '{newName}': {ex.Message}", ConsoleColors.Yellow, Verbosity.Normal); -#if DEBUG - WriteLine(ex.ToString()); -#endif - ignoreIds?.Add(symbolId); - return true; - } - - if (ErrorResolution != RenameErrorResolution.None) - { - Project newProject = newSolution.GetDocument(document.Id).Project; - Compilation compilation = await newProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - ImmutableArray diagnostics = compilation.GetDiagnostics(cancellationToken); - - compilerErrorCount = LogHelpers.WriteCompilerErrors( - diagnostics, - Path.GetDirectoryName(newProject.FilePath), - ignoredCompilerDiagnosticIds: Options.IgnoredCompilerDiagnosticIds, - indentation: " "); - } - - if (compilerErrorCount > 0 - && ErrorResolution != RenameErrorResolution.List) - { - if (ErrorResolution == RenameErrorResolution.Fix) - { - interactive = true; - continue; - } - else if (ErrorResolution == RenameErrorResolution.Skip) - { - ignoreIds?.Add(symbolId); - return true; - } - else if (ErrorResolution == RenameErrorResolution.Ask - && UserDialog is not null) - { - switch (UserDialog.ShowDialog("Rename symbol?")) - { - case DialogResult.None: - case DialogResult.No: - { - ignoreIds?.Add(symbolId); - return true; - } - case DialogResult.NoToAll: - { - ErrorResolution = RenameErrorResolution.Skip; - ignoreIds?.Add(symbolId); - return true; - } - case DialogResult.Yes: - { - break; - } - case DialogResult.YesToAll: - { - ErrorResolution = RenameErrorResolution.None; - break; - } - default: - { - throw new InvalidOperationException(); - } - } - } - else if (ErrorResolution == RenameErrorResolution.Abort) - { - throw new OperationCanceledException(); - } - else - { - throw new InvalidOperationException(); - } - } - - break; - } - - if (Ask - && UserDialog is not null - && (compilerErrorCount == 0 || ErrorResolution != RenameErrorResolution.Ask)) - { - switch (UserDialog.ShowDialog("Rename symbol?")) - { - case DialogResult.None: - case DialogResult.No: - { - ignoreIds?.Add(symbolId); - return true; - } - case DialogResult.NoToAll: - { - DryRun = true; - ignoreIds?.Add(symbolId); - return true; - } - case DialogResult.Yes: - { - break; - } - case DialogResult.YesToAll: - { - Ask = false; - break; - } - default: - { - throw new InvalidOperationException(); - } - } - } - - IEnumerable referencedSymbols = await Microsoft.CodeAnalysis.FindSymbols.SymbolFinder.FindReferencesAsync( - symbol, - document.Project.Solution, - cancellationToken) - .ConfigureAwait(false); - - Solution oldSolution = CurrentSolution; - - if (!Workspace.TryApplyChanges(newSolution)) - { - Debug.Fail($"Cannot apply changes to solution '{newSolution.FilePath}'"); - WriteLine($" Cannot apply changes to solution '{newSolution.FilePath}'", ConsoleColors.Yellow, Verbosity.Normal); - - ignoreIds?.Add(GetSymbolId(symbol)); - return true; - } - - results.Add(new SymbolRenameResult(symbol.Name, newName, symbolId)); - - if (diffTracker is null - && ignoreIds is null) - { - return true; - } - - int diff = newName.Length - symbol.Name.Length; - int oldIdentifierDiff = identifier.Text.Length - identifier.ValueText.Length; - - Debug.Assert(oldIdentifierDiff == 0 || oldIdentifierDiff == 1, oldIdentifierDiff.ToString()); - - HashSet locations = await GetLocationsAsync(referencedSymbols, symbol).ConfigureAwait(false); - - document = CurrentSolution.GetDocument(symbolData.DocumentId); - semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - Location oldLocation = symbol.Locations[0]; - TextSpan oldSpan = oldLocation.SourceSpan; - int diffCount = locations.Count(f => f.SourceTree == oldLocation.SourceTree && f.SourceSpan.Start < oldLocation.SourceSpan.Start); - var newSpan = new TextSpan(oldSpan.Start + (diff * diffCount), newName.Length); - SyntaxToken newIdentifier = root.FindToken(newSpan.Start); - int newIdentifierDiff = newIdentifier.Text.Length - newIdentifier.ValueText.Length; - int identifierDiff = newIdentifierDiff - oldIdentifierDiff; - - // '@default' > 'default' or vice versa - if (newIdentifier.Span != newSpan) - { - var newSpan2 = new TextSpan( - oldSpan.Start + ((diff + ((oldIdentifierDiff > 0) ? -1 : 1)) * diffCount), - newName.Length + ((oldIdentifierDiff > 0) ? 0 : 1)); - - SyntaxToken newIdentifier2 = root.FindToken(newSpan2.Start); - - Debug.Assert(newIdentifier2.Span == newSpan2, newIdentifier2.Span.ToString() + "\n" + newSpan2.ToString()); - - if (newIdentifier2.Span == newSpan2) - { - newSpan = newSpan2; - newIdentifier = newIdentifier2; - newIdentifierDiff = newIdentifier.Text.Length - newIdentifier.ValueText.Length; - - Debug.Assert(newIdentifierDiff == 0 || newIdentifierDiff == 1, newIdentifierDiff.ToString()); - - identifierDiff = newIdentifierDiff - oldIdentifierDiff; - } - } - - diff += identifierDiff; - - if (diffTracker is not null) - { - diffTracker.AddLocations(locations, diff, oldSolution); - _diffTracker.AddLocations(locations, diff, oldSolution); - } -#if DEBUG - Debug.Assert(string.Equals(newName, newIdentifier.ValueText, StringComparison.Ordinal), $"{newName}\n{newIdentifier.ValueText}"); - - foreach (IGrouping grouping in locations - .GroupBy(f => newSolution.GetDocument(oldSolution.GetDocumentId(f.SourceTree)).Id)) - { - DocumentId documentId = grouping.Key; - Document newDocument = newSolution.GetDocument(documentId); - int offset = 0; - - foreach (TextSpan span2 in grouping - .Select(f => f.SourceSpan) - .OrderBy(f => f.Start)) - { - var s = new TextSpan(span2.Start + offset, span2.Length + diff); - SyntaxNode r = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - SyntaxToken t = r.FindToken(s.Start, findInsideTrivia: true); - - // C# string literal token (inside SuppressMessageAttribute) - if (t.RawKind == 8511) - { - string text = t.ValueText.Substring(s.Start - t.SpanStart, newName.Length); - - Debug.Assert(string.Equals(newName, text, StringComparison.Ordinal), text); - } - else - { - Debug.Assert(findSymbolService.CanBeRenamed(t)); - Debug.Assert(t.Span == s); - } - - offset += diff; - } - } -#endif - if (string.Equals(newName, newIdentifier.ValueText, StringComparison.Ordinal) - && ignoreIds is not null) - { - SyntaxNode newNode = findSymbolService.FindDeclaration(newIdentifier.Parent); - - symbol = semanticModel.GetDeclaredSymbol(newNode, cancellationToken) - ?? semanticModel.GetSymbol(newNode, cancellationToken); - - Debug.Assert(symbol is not null, GetSymbolId(symbol)); - - if (symbol is not null) - ignoreIds.Add(GetSymbolId(symbol)); - } - - return true; - - async Task> GetLocationsAsync(IEnumerable referencedSymbols, ISymbol symbol = null) - { - var locations = new HashSet(); - - foreach (Location location in GetLocations()) - { - TextSpan span = location.SourceSpan; - - if (symbol is not null) - { - // 'this' and 'base' constructor references - if (symbol.Name.Length == 4 - || symbol.Name.Length != span.Length) - { - SyntaxNode root = await location.SourceTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - SyntaxToken token = root.FindToken(span.Start, findInsideTrivia: true); - - Debug.Assert(token.Span == span); - - if (token.Span == span - && !findSymbolService.CanBeRenamed(token)) - { - continue; - } - } - } - - locations.Add(location); - } - - return locations; - - IEnumerable GetLocations() - { - foreach (ReferencedSymbol referencedSymbol in referencedSymbols) - { - if (referencedSymbol.Definition is not IMethodSymbol methodSymbol - || !methodSymbol.MethodKind.Is(MethodKind.PropertyGet, MethodKind.PropertySet, MethodKind.EventAdd, MethodKind.EventRemove)) - { - foreach (Location location in referencedSymbol.Definition.Locations) - yield return location; - } - - foreach (ReferenceLocation referenceLocation in referencedSymbol.Locations) - { - if (!referenceLocation.IsImplicit && !referenceLocation.IsCandidateLocation) - yield return referenceLocation.Location; - } - } - } - } - } - - private static string GetNewName( - string newName, - ISymbol symbol, - IFindSymbolService findSymbolService, - bool interactive) - { - if (interactive) - { - bool isAttribute = symbol is INamedTypeSymbol typeSymbol - && typeSymbol.InheritsFrom(MetadataNames.System_Attribute); - - while (true) - { - string newName2 = ConsoleUtility.ReadUserInput(newName, " New name: "); - - if (string.IsNullOrEmpty(newName2)) - return null; - - if (string.Equals(newName, newName2, StringComparison.Ordinal)) - break; - - bool isValidIdentifier = findSymbolService.SyntaxFacts.IsValidIdentifier(newName2); - - if (isValidIdentifier - && (!isAttribute || newName2.EndsWith("Attribute"))) - { - newName = newName2; - break; - } - - ConsoleOut.WriteLine( - (!isValidIdentifier) - ? " New name is invalid" - : " New name is invalid, attribute name must end with 'Attribute'", - ConsoleColor.Yellow); - } - } - - if (string.Equals(symbol.Name, newName, StringComparison.Ordinal)) - return null; - - if (!interactive) - { - if (!findSymbolService.SyntaxFacts.IsValidIdentifier(newName)) - { - WriteLine($" New name is invalid: {newName}", ConsoleColors.Yellow, Verbosity.Minimal); - return null; - } - - if (symbol is INamedTypeSymbol typeSymbol - && typeSymbol.InheritsFrom(MetadataNames.System_Attribute) - && !newName.EndsWith("Attribute")) - { - WriteLine($" New name is invalid: {newName}. Attribute name must end with 'Attribute'.", ConsoleColors.Yellow, Verbosity.Minimal); - return null; - } - } - - return newName; - } - - private static string GetSymbolId(ISymbol symbol) + /// + /// Renames symbols in the specified project. + /// + /// + /// + /// + /// + /// + /// + public static async Task RenameSymbolsAsync( + Project project, + Func predicate, + Func getNewName, + SymbolRenamerOptions options = null, + IProgress progress = null, + CancellationToken cancellationToken = default) { - string id; - - switch (symbol.Kind) - { - case SymbolKind.Local: - { - return null; - } - case SymbolKind.Method: - { - var methodSymbol = (IMethodSymbol)symbol; - - if (methodSymbol.MethodKind == MethodKind.LocalFunction) - { - id = symbol.Name; - ISymbol cs = symbol.ContainingSymbol; - - while (cs is IMethodSymbol { MethodKind: MethodKind.LocalFunction }) - { - id = cs.Name + "." + id; - cs = cs.ContainingSymbol; - } - - return id; - } - - break; - } - case SymbolKind.Parameter: - case SymbolKind.TypeParameter: - { - ISymbol cs = symbol.ContainingSymbol; - - if (cs is IMethodSymbol methodSymbol) - { - if (methodSymbol.MethodKind == MethodKind.AnonymousFunction) - return null; - - if (methodSymbol.MethodKind == MethodKind.LocalFunction) - { - id = cs.Name + " " + symbol.Name; - cs = cs.ContainingSymbol; - - while (cs is IMethodSymbol { MethodKind: MethodKind.LocalFunction }) - { - id = cs.Name + "." + id; - cs = cs.ContainingSymbol; - } - - return id; - } - } - - return symbol.ContainingSymbol.GetDocumentationCommentId() + " " + (symbol.GetDocumentationCommentId() ?? symbol.Name); - } - } + var renamer = new SymbolRenameState(project.Solution, predicate, getNewName, options, progress); - return symbol.GetDocumentationCommentId(); + await renamer.RenameSymbolsAsync(project, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs b/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs index 9a71af7c51..62074bb942 100644 --- a/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs +++ b/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs @@ -1,51 +1,65 @@ // Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Collections.Immutable; namespace Roslynator.Rename; -internal class SymbolRenamerOptions +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +/// +/// Represents options for . +/// +public class SymbolRenamerOptions { - internal SymbolRenamerOptions( - RenameScopeFilter scopeFilter = RenameScopeFilter.All, - VisibilityFilter visibilityFilter = VisibilityFilter.All, - RenameErrorResolution errorResolution = RenameErrorResolution.None, - IEnumerable ignoredCompilerDiagnosticIds = null, - int codeContext = -1, - bool includeGeneratedCode = false, - bool ask = false, - bool dryRun = false, - bool interactive = false) - { - ScopeFilter = scopeFilter; - VisibilityFilter = visibilityFilter; - ErrorResolution = errorResolution; - IgnoredCompilerDiagnosticIds = ignoredCompilerDiagnosticIds?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; - CodeContext = codeContext; - IncludeGeneratedCode = includeGeneratedCode; - Ask = ask; - DryRun = dryRun; - Interactive = interactive; - } + /// + /// Do not rename type symbols (classes, structs, interfaces etc.). + /// + public bool SkipTypes { get; set; } - public static SymbolRenamerOptions Default { get; } = new(); + /// + /// Do not rename member symbols (methods, properties, fields etc.). + /// + public bool SkipMembers { get; set; } - public RenameScopeFilter ScopeFilter { get; } + /// + /// Do not rename local symbols (like local variables). + /// + public bool SkipLocals { get; set; } - public VisibilityFilter VisibilityFilter { get; } + public CompilationErrorResolution CompilationErrorResolution { get; set; } = CompilationErrorResolution.Throw; - public RenameErrorResolution ErrorResolution { get; } + /// + /// A list of compiler diagnostic IDs that should be ignored. + /// + public HashSet IgnoredCompilerDiagnosticIds { get; } = new(); - public ImmutableHashSet IgnoredCompilerDiagnosticIds { get; } + /// + /// Include symbols that are part of generated code. + /// + public bool IncludeGeneratedCode { get; set; } - public int CodeContext { get; } + /// + /// Do not save changes to disk. + /// + public bool DryRun { get; set; } - public bool IncludeGeneratedCode { get; } + /// + /// If the symbol is a method rename its overloads as well. + /// + public bool RenameOverloads { get; set; } - public bool Ask { get; } + /// + /// Rename identifiers in string literals that match the name of the symbol. + /// + public bool RenameInStrings { get; set; } - public bool DryRun { get; } + /// + /// Rename identifiers in comments that match the name of the symbol. + /// + public bool RenameInComments { get; set; } - public bool Interactive { get; } + /// + /// If the symbol is a type renames the file containing the type declaration as well. + /// + public bool RenameFile { get; set; } } diff --git a/tools/generate_api_list.ps1 b/tools/generate_api_list.ps1 new file mode 100644 index 0000000000..b3d099f09c --- /dev/null +++ b/tools/generate_api_list.ps1 @@ -0,0 +1,6 @@ +roslynator list-symbols generate_ref_docs.sln ` + --properties Configuration=Release ` + --visibility public ` + --depth member ` + --ignored-parts containing-namespace assembly-attributes ` + --output "api.txt"