From 824debec797c3513fc88d1c250832d94356d8a2e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:03:07 -0800 Subject: [PATCH 001/203] v3.3.0: Logging updates and dropped EOL PowerShell --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0640253e9..52b0afa2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ Please update to PowerShell 7.4 LTS going forward. This release contains a logging overhaul which purposely removes our dependency on Serilog and should lead to improved stability with PowerShell 5.1 (by avoiding a major GAC assembly conflict). +## v3.3.0 +### Friday, November 15, 2024 + +See more details at the GitHub Release for [v3.3.0](https://github.com/PowerShell/PowerShellEditorServices/releases/tag/v3.3.0). + +Logging updates and dropped EOL PowerShell ## v3.21.0 ### Wednesday, October 30, 2024 From 8b270f92802814ff0a833438047f0cd7e932c4dc Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:09:25 +1100 Subject: [PATCH 002/203] adding in rename symbol service --- .../Server/PsesLanguageServer.cs | 1 + .../PowerShell/Handlers/RenameSymbol.cs | 134 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index c106d34c6..5d75d4994 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -123,6 +123,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() // NOTE: The OnInitialize delegate gets run when we first receive the // _Initialize_ request: // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs new file mode 100644 index 000000000..70f5b788a --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Management.Automation.Language; +using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +namespace Microsoft.PowerShell.EditorServices.Handlers +{ + [Serial, Method("powerShell/renameSymbol")] + internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } + + internal class RenameSymbolParams : IRequest + { + public string FileName { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string RenameTo { get; set; } + } + internal class TextChange + { + public string NewText { get; set; } + public int StartLine { get; set; } + public int StartColumn { get; set; } + public int EndLine { get; set; } + public int EndColumn { get; set; } + } + internal class ModifiedFileResponse + { + public string FileName { get; set; } + public List Changes { get; set; } + + } + internal class RenameSymbolResult + { + public List Changes { get; set; } + } + + internal class RenameSymbolHandler : IRenameSymbolHandler + { + private readonly IInternalPowerShellExecutionService _executionService; + + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + + public RenameSymbolHandler(IInternalPowerShellExecutionService executionService, + ILoggerFactory loggerFactory, + WorkspaceService workspaceService) + { + _logger = loggerFactory.CreateLogger(); + _workspaceService = workspaceService; + _executionService = executionService; + } + + /// Method to get a symbols parent function(s) if any + internal static IEnumerable GetParentFunction(SymbolReference symbol, Ast Ast) + { + return Ast.FindAll(ast => + { + return ast.Extent.StartLineNumber <= symbol.ScriptRegion.StartLineNumber && + ast.Extent.EndLineNumber >= symbol.ScriptRegion.EndLineNumber && + ast is FunctionDefinitionAst; + }, true); + } + internal static IEnumerable GetVariablesWithinExtent(Ast symbol, Ast Ast) + { + return Ast.FindAll(ast => + { + return ast.Extent.StartLineNumber >= symbol.Extent.StartLineNumber && + ast.Extent.EndLineNumber <= symbol.Extent.EndLineNumber && + ast is VariableExpressionAst; + }, true); + } + public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) + { + if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) + { + _logger.LogDebug("Failed to open file!"); + return null; + } + // Locate the Symbol in the file + // Look at its parent to find its script scope + // I.E In a function + // Lookup all other occurances of the symbol + // replace symbols that fall in the same scope as the initial symbol + + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); + Ast ast = scriptFile.ScriptAst; + + RenameSymbolResult response = new() + { + Changes = new List + { + new ModifiedFileResponse() + { + FileName = request.FileName, + Changes = new List() + } + } + }; + + foreach (Ast e in GetParentFunction(symbol, ast)) + { + foreach (Ast v in GetVariablesWithinExtent(e, ast)) + { + TextChange change = new() + { + StartColumn = v.Extent.StartColumnNumber - 1, + StartLine = v.Extent.StartLineNumber - 1, + EndColumn = v.Extent.EndColumnNumber - 1, + EndLine = v.Extent.EndLineNumber - 1, + NewText = request.RenameTo + }; + response.Changes[0].Changes.Add(change); + } + } + + PSCommand psCommand = new(); + psCommand + .AddScript("Return 'Not sure how to make this non Async :('") + .AddStatement(); + IReadOnlyList result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); + return response; + } + } +} From 8b6670449a98299b14052ba755b59370caf0a439 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:18:32 +1100 Subject: [PATCH 003/203] switched to using a task not sure if there is a better way --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 70f5b788a..fba015f12 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -46,8 +46,6 @@ internal class RenameSymbolResult internal class RenameSymbolHandler : IRenameSymbolHandler { - private readonly IInternalPowerShellExecutionService _executionService; - private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; @@ -57,7 +55,6 @@ public RenameSymbolHandler(IInternalPowerShellExecutionService executionService, { _logger = loggerFactory.CreateLogger(); _workspaceService = workspaceService; - _executionService = executionService; } /// Method to get a symbols parent function(s) if any @@ -79,19 +76,19 @@ internal static IEnumerable GetVariablesWithinExtent(Ast symbol, Ast Ast) ast is VariableExpressionAst; }, true); } - public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) + public Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) { _logger.LogDebug("Failed to open file!"); - return null; + return Task.FromResult(null); } // Locate the Symbol in the file // Look at its parent to find its script scope // I.E In a function // Lookup all other occurances of the symbol // replace symbols that fall in the same scope as the initial symbol - + return Task.Run(()=>{ SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); Ast ast = scriptFile.ScriptAst; @@ -127,8 +124,9 @@ public async Task Handle(RenameSymbolParams request, Cancell psCommand .AddScript("Return 'Not sure how to make this non Async :('") .AddStatement(); - IReadOnlyList result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); + //IReadOnlyList result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); return response; + }); } } } From 7d958b618c332de5418e1b38cffb2c8c2a25b326 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 26 Mar 2023 19:42:41 +1100 Subject: [PATCH 004/203] completed rename function --- .../PowerShell/Handlers/RenameSymbol.cs | 177 ++++++++++++++---- 1 file changed, 137 insertions(+), 40 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index fba015f12..8a4e22dec 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -1,18 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediatR; using System.Management.Automation.Language; using OmniSharp.Extensions.JsonRpc; -using Microsoft.PowerShell.EditorServices.Services.PowerShell; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using System.Linq; + namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/renameSymbol")] @@ -37,10 +37,29 @@ internal class ModifiedFileResponse { public string FileName { get; set; } public List Changes { get; set; } + public ModifiedFileResponse(string fileName) + { + FileName = fileName; + Changes = new List(); + } + public void AddTextChange(Ast Symbol, string NewText) + { + Changes.Add( + new TextChange + { + StartColumn = Symbol.Extent.StartColumnNumber - 1, + StartLine = Symbol.Extent.StartLineNumber - 1, + EndColumn = Symbol.Extent.EndColumnNumber - 1, + EndLine = Symbol.Extent.EndLineNumber - 1, + NewText = NewText + } + ); + } } internal class RenameSymbolResult { + public RenameSymbolResult() => Changes = new List(); public List Changes { get; set; } } @@ -49,7 +68,7 @@ internal class RenameSymbolHandler : IRenameSymbolHandler private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; - public RenameSymbolHandler(IInternalPowerShellExecutionService executionService, + public RenameSymbolHandler( ILoggerFactory loggerFactory, WorkspaceService workspaceService) { @@ -58,14 +77,14 @@ public RenameSymbolHandler(IInternalPowerShellExecutionService executionService, } /// Method to get a symbols parent function(s) if any - internal static IEnumerable GetParentFunction(SymbolReference symbol, Ast Ast) + internal static List GetParentFunctions(SymbolReference symbol, Ast Ast) { - return Ast.FindAll(ast => + return new List(Ast.FindAll(ast => { return ast.Extent.StartLineNumber <= symbol.ScriptRegion.StartLineNumber && ast.Extent.EndLineNumber >= symbol.ScriptRegion.EndLineNumber && ast is FunctionDefinitionAst; - }, true); + }, true)); } internal static IEnumerable GetVariablesWithinExtent(Ast symbol, Ast Ast) { @@ -76,57 +95,135 @@ internal static IEnumerable GetVariablesWithinExtent(Ast symbol, Ast Ast) ast is VariableExpressionAst; }, true); } - public Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) + internal static Ast GetLargestExtentInCollection(IEnumerable Nodes) + { + Ast LargestNode = null; + foreach (Ast Node in Nodes) + { + LargestNode ??= Node; + if (Node.Extent.EndLineNumber - Node.Extent.StartLineNumber > + LargestNode.Extent.EndLineNumber - LargestNode.Extent.StartLineNumber) + { + LargestNode = Node; + } + } + return LargestNode; + } + internal static Ast GetSmallestExtentInCollection(IEnumerable Nodes) + { + Ast SmallestNode = null; + foreach (Ast Node in Nodes) + { + SmallestNode ??= Node; + if (Node.Extent.EndLineNumber - Node.Extent.StartLineNumber < + SmallestNode.Extent.EndLineNumber - SmallestNode.Extent.StartLineNumber) + { + SmallestNode = Node; + } + } + return SmallestNode; + } + internal static List GetFunctionExcludedNestedFunctions(Ast function, SymbolReference symbol) + { + IEnumerable nestedFunctions = function.FindAll(ast => ast is FunctionDefinitionAst && ast != function, true); + List excludeExtents = new(); + foreach (Ast nestedfunction in nestedFunctions) + { + if (IsVarInFunctionParamBlock(nestedfunction, symbol)) + { + excludeExtents.Add(nestedfunction); + } + } + return excludeExtents; + } + internal static bool IsVarInFunctionParamBlock(Ast Function, SymbolReference symbol) + { + Ast paramBlock = Function.Find(ast => ast is ParamBlockAst, true); + if (paramBlock != null) + { + IEnumerable variables = paramBlock.FindAll(ast => + { + return ast is VariableExpressionAst && + ast.Parent is ParameterAst; + }, true); + foreach (VariableExpressionAst variable in variables.Cast()) + { + if (variable.Extent.Text == symbol.ScriptRegion.Text) + { + return true; + } + } + } + return false; + } + + public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) { _logger.LogDebug("Failed to open file!"); - return Task.FromResult(null); + return await Task.FromResult(null).ConfigureAwait(false); } // Locate the Symbol in the file // Look at its parent to find its script scope // I.E In a function // Lookup all other occurances of the symbol // replace symbols that fall in the same scope as the initial symbol - return Task.Run(()=>{ - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); - Ast ast = scriptFile.ScriptAst; - - RenameSymbolResult response = new() + return await Task.Run(() => { - Changes = new List - { - new ModifiedFileResponse() + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + if (symbol == null) { - FileName = request.FileName, - Changes = new List() + return null; } - } - }; + IEnumerable SymbolOccurances = SymbolsService.FindOccurrencesInFile( + scriptFile, + request.Line + 1, + request.Column + 1); - foreach (Ast e in GetParentFunction(symbol, ast)) - { - foreach (Ast v in GetVariablesWithinExtent(e, ast)) + ModifiedFileResponse FileModifications = new(request.FileName); + Ast token = scriptFile.ScriptAst.Find(ast => + { + return ast.Extent.StartLineNumber == symbol.ScriptRegion.StartLineNumber && + ast.Extent.StartColumnNumber == symbol.ScriptRegion.StartColumnNumber; + }, true); + + if (symbol.Type is SymbolType.Function) { - TextChange change = new() + string functionName = !symbol.Name.Contains("function ") ? symbol.Name : symbol.Name.Replace("function ", "").Replace(" ()", ""); + + FunctionDefinitionAst funcDef = (FunctionDefinitionAst)scriptFile.ScriptAst.Find(ast => + { + return ast is FunctionDefinitionAst astfunc && + astfunc.Name == functionName; + }, true); + // No nice way to actually update the function name other than manually specifying the location + // going to assume all function definitions start with "function " + FileModifications.Changes.Add(new TextChange + { + NewText = request.RenameTo, + StartLine = funcDef.Extent.StartLineNumber - 1, + EndLine = funcDef.Extent.StartLineNumber - 1, + StartColumn = funcDef.Extent.StartColumnNumber + "function ".Length - 1, + EndColumn = funcDef.Extent.StartColumnNumber + "function ".Length + funcDef.Name.Length - 1 + }); + IEnumerable CommandCalls = scriptFile.ScriptAst.FindAll(ast => { - StartColumn = v.Extent.StartColumnNumber - 1, - StartLine = v.Extent.StartLineNumber - 1, - EndColumn = v.Extent.EndColumnNumber - 1, - EndLine = v.Extent.EndLineNumber - 1, - NewText = request.RenameTo - }; - response.Changes[0].Changes.Add(change); + return ast is StringConstantExpressionAst funccall && + ast.Parent is CommandAst && + funccall.Value == funcDef.Name; + }, true); + foreach (Ast CommandCall in CommandCalls) + { + FileModifications.AddTextChange(CommandCall, request.RenameTo); + } } - } - - PSCommand psCommand = new(); - psCommand - .AddScript("Return 'Not sure how to make this non Async :('") - .AddStatement(); - //IReadOnlyList result = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); - return response; - }); + RenameSymbolResult result = new(); + result.Changes.Add(FileModifications); + return result; + }).ConfigureAwait(false); } } } From 1e4d0bf726abac5b66943b8c13d9b6607d7fa862 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 26 Mar 2023 19:54:13 +1100 Subject: [PATCH 005/203] slight refactoring --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 8a4e22dec..b5b1e6a2c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -174,14 +174,8 @@ public async Task Handle(RenameSymbolParams request, Cancell SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( request.Line + 1, request.Column + 1); - if (symbol == null) - { - return null; - } - IEnumerable SymbolOccurances = SymbolsService.FindOccurrencesInFile( - scriptFile, - request.Line + 1, - request.Column + 1); + + if (symbol == null){return null;} ModifiedFileResponse FileModifications = new(request.FileName); Ast token = scriptFile.ScriptAst.Find(ast => @@ -211,9 +205,9 @@ public async Task Handle(RenameSymbolParams request, Cancell }); IEnumerable CommandCalls = scriptFile.ScriptAst.FindAll(ast => { - return ast is StringConstantExpressionAst funccall && + return ast is StringConstantExpressionAst funcCall && ast.Parent is CommandAst && - funccall.Value == funcDef.Name; + funcCall.Value == funcDef.Name; }, true); foreach (Ast CommandCall in CommandCalls) { From ac1b1c8d4c81885cfdc8daeeb81564c7c5f117e6 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 29 Mar 2023 20:55:25 +1100 Subject: [PATCH 006/203] Adding in unit teste for refactoring functions --- .../PowerShell/Handlers/RenameSymbol.cs | 81 +++++++---- .../Refactoring/FunctionsMultiple.ps1 | 17 +++ .../Refactoring/FunctionsNestedSimple.ps1 | 11 ++ .../Refactoring/FunctionsSingle.ps1 | 5 + .../Refactoring/RefactorsFunctionData.cs | 42 ++++++ .../Refactoring/RefactorFunctionTests.cs | 129 ++++++++++++++++++ 6 files changed, 255 insertions(+), 30 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index b5b1e6a2c..53f683257 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -157,6 +157,53 @@ internal static bool IsVarInFunctionParamBlock(Ast Function, SymbolReference sym return false; } + internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) + { + if (symbol.Type is not SymbolType.Function) + { + return null; + } + + // we either get the CommandAst or the FunctionDeginitionAts + string functionName = !symbol.Name.Contains("function ") ? symbol.Name : symbol.Name.Replace("function ", "").Replace(" ()", ""); + + FunctionDefinitionAst funcDef = (FunctionDefinitionAst)scriptAst.Find(ast => + { + return ast is FunctionDefinitionAst astfunc && + astfunc.Name == functionName; + }, true); + if (funcDef == null) + { + return null; + } + // No nice way to actually update the function name other than manually specifying the location + // going to assume all function definitions start with "function " + + ModifiedFileResponse FileModifications = new(request.FileName); + + FileModifications.Changes.Add(new TextChange + { + NewText = request.RenameTo, + StartLine = funcDef.Extent.StartLineNumber - 1, + EndLine = funcDef.Extent.StartLineNumber - 1, + StartColumn = funcDef.Extent.StartColumnNumber + "function ".Length - 1, + EndColumn = funcDef.Extent.StartColumnNumber + "function ".Length + funcDef.Name.Length - 1 + }); + + IEnumerable CommandCalls = scriptAst.FindAll(ast => + { + return ast is StringConstantExpressionAst funcCall && + ast.Parent is CommandAst && + funcCall.Value == funcDef.Name; + }, true); + + foreach (Ast CommandCall in CommandCalls) + { + FileModifications.AddTextChange(CommandCall, request.RenameTo); + } + + return FileModifications; + } public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) @@ -175,45 +222,19 @@ public async Task Handle(RenameSymbolParams request, Cancell request.Line + 1, request.Column + 1); - if (symbol == null){return null;} + if (symbol == null) { return null; } - ModifiedFileResponse FileModifications = new(request.FileName); Ast token = scriptFile.ScriptAst.Find(ast => { return ast.Extent.StartLineNumber == symbol.ScriptRegion.StartLineNumber && ast.Extent.StartColumnNumber == symbol.ScriptRegion.StartColumnNumber; }, true); - + ModifiedFileResponse FileModifications = null; if (symbol.Type is SymbolType.Function) { - string functionName = !symbol.Name.Contains("function ") ? symbol.Name : symbol.Name.Replace("function ", "").Replace(" ()", ""); - - FunctionDefinitionAst funcDef = (FunctionDefinitionAst)scriptFile.ScriptAst.Find(ast => - { - return ast is FunctionDefinitionAst astfunc && - astfunc.Name == functionName; - }, true); - // No nice way to actually update the function name other than manually specifying the location - // going to assume all function definitions start with "function " - FileModifications.Changes.Add(new TextChange - { - NewText = request.RenameTo, - StartLine = funcDef.Extent.StartLineNumber - 1, - EndLine = funcDef.Extent.StartLineNumber - 1, - StartColumn = funcDef.Extent.StartColumnNumber + "function ".Length - 1, - EndColumn = funcDef.Extent.StartColumnNumber + "function ".Length + funcDef.Name.Length - 1 - }); - IEnumerable CommandCalls = scriptFile.ScriptAst.FindAll(ast => - { - return ast is StringConstantExpressionAst funcCall && - ast.Parent is CommandAst && - funcCall.Value == funcDef.Name; - }, true); - foreach (Ast CommandCall in CommandCalls) - { - FileModifications.AddTextChange(CommandCall, request.RenameTo); - } + FileModifications = RefactorFunction(symbol, scriptFile.ScriptAst, request); } + RenameSymbolResult result = new(); result.Changes.Add(FileModifications); return result; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 new file mode 100644 index 000000000..f38f00257 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 @@ -0,0 +1,17 @@ +function One { + write-host "One Hello World" +} +function Two { + write-host "Two Hello World" + One +} + +function Three { + write-host "Three Hello" + Two +} + +Function Four { + Write-host "Four Hello" + One +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 new file mode 100644 index 000000000..2d0746723 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 @@ -0,0 +1,11 @@ +function Outer { + write-host "Hello World" + + function Inner { + write-host "Hello World" + } + Inner + +} + +SingleFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 new file mode 100644 index 000000000..f8670166d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 @@ -0,0 +1,5 @@ +function SingleFunction { + write-host "Hello World" +} + +SingleFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs new file mode 100644 index 000000000..e67d27937 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Microsoft.PowerShell.EditorServices.Handlers; + +namespace PowerShellEditorServices.Test.Shared.Refactoring +{ + internal static class RefactorsFunctionData + { + public static readonly RenameSymbolParams FunctionsMultiple = new() + { + // rename function Two { ...} + FileName = "FunctionsMultiple.ps1", + Column = 9, + Line = 3, + RenameTo = "TwoFours" + }; + public static readonly RenameSymbolParams FunctionsMultipleFromCommandDef = new() + { + // ... write-host "Three Hello" ... + // Two + // + FileName = "FunctionsMultiple.ps1", + Column = 5, + Line = 15, + RenameTo = "OnePlusOne" + }; + public static readonly RenameSymbolParams FunctionsSingleParams = new() + { + FileName = "FunctionsSingle.ps1", + Column = 9, + Line = 0, + RenameTo = "OneMethod" + }; + public static readonly RenameSymbolParams FunctionsSingleNested = new() + { + FileName = "FunctionsNestedSimple.ps1", + Column = 16, + Line = 4, + RenameTo = "OneMethod" + }; + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs new file mode 100644 index 000000000..f03bfe92e --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using PowerShellEditorServices.Test.Shared.Refactoring; + +namespace PowerShellEditorServices.Test.Refactoring +{ + [Trait("Category", "RefactorFunction")] + public class RefactorFunctionTests : IDisposable + + { + private readonly PsesInternalHost psesHost; + private readonly WorkspaceService workspace; + public void Dispose() + { +#pragma warning disable VSTHRD002 + psesHost.StopAsync().Wait(); +#pragma warning restore VSTHRD002 + GC.SuppressFinalize(this); + } + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", fileName))); + public RefactorFunctionTests() + { + psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); + } + [Fact] + public void RefactorFunctionSingle() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsSingleParams; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 9 && + item.EndColumn == 23 && + item.StartLine == 0 && + item.EndLine == 0 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 0 && + item.EndColumn == 14 && + item.StartLine == 4 && + item.EndLine == 4 && + request.RenameTo == item.NewText; + }); + } + [Fact] + public void RefactorMultipleFromCommandDef() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsMultipleFromCommandDef; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Equal(3, changes.Changes.Count); + + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 9 && + item.EndColumn == 12 && + item.StartLine == 0 && + item.EndLine == 0 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 4 && + item.EndColumn == 7 && + item.StartLine == 5 && + item.EndLine == 5 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 4 && + item.EndColumn == 7 && + item.StartLine == 15 && + item.EndLine == 15 && + request.RenameTo == item.NewText; + }); + } + [Fact] + public void RefactorNestedFunction() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsMultiple; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Equal(2, changes.Changes.Count); + + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 13 && + item.EndColumn == 16 && + item.StartLine == 4 && + item.EndLine == 4 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 4 && + item.EndColumn == 10 && + item.StartLine == 6 && + item.EndLine == 6 && + request.RenameTo == item.NewText; + }); + } + } +} From 2d7bec421f0749c5fad5b150115b90f2e1709b9d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 29 Mar 2023 21:03:09 +1100 Subject: [PATCH 007/203] test case for a function that is flat or inline --- .../Refactoring/FunctionsFlat.ps1 | 1 + .../Refactoring/RefactorsFunctionData.cs | 9 +++++- .../Refactoring/RefactorFunctionTests.cs | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 new file mode 100644 index 000000000..939e723ee --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 @@ -0,0 +1 @@ +{function Cat1 {write-host "The Cat"};function Dog {Cat1;write-host "jumped ..."}Dog} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs index e67d27937..4cbefea5a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs @@ -31,12 +31,19 @@ internal static class RefactorsFunctionData Line = 0, RenameTo = "OneMethod" }; - public static readonly RenameSymbolParams FunctionsSingleNested = new() + public static readonly RenameSymbolParams FunctionsSingleNested = new() { FileName = "FunctionsNestedSimple.ps1", Column = 16, Line = 4, RenameTo = "OneMethod" }; + public static readonly RenameSymbolParams FunctionsSimpleFlat = new() + { + FileName = "FunctionsFlat.ps1", + Column = 81, + Line = 0, + RenameTo = "ChangedFlat" + }; } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index f03bfe92e..888803527 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -125,5 +125,33 @@ public void RefactorNestedFunction() request.RenameTo == item.NewText; }); } + [Fact] + public void RefactorFlatFunction() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsSimpleFlat; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Equal(2, changes.Changes.Count); + + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 47 && + item.EndColumn == 50 && + item.StartLine == 0 && + item.EndLine == 0 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 81 && + item.EndColumn == 84 && + item.StartLine == 0 && + item.EndLine == 0 && + request.RenameTo == item.NewText; + }); + } } } From 0863aa7a095770fb5c426110bfc64ed92b00f672 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:31:18 +0100 Subject: [PATCH 008/203] added new test case --- .../Refactoring/FunctionsNestedOverlap.ps1 | 30 +++++++++++++++++ .../Refactoring/RefactorsFunctionData.cs | 7 ++++ .../Refactoring/RefactorFunctionTests.cs | 32 +++++++++++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 new file mode 100644 index 000000000..aa1483936 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 @@ -0,0 +1,30 @@ + +function Inner { + write-host "I'm the First Inner" +} +function foo { + function Inner { + write-host "Shouldnt be called or renamed at all." + } +} +function Inner { + write-host "I'm the First Inner" +} + +function Outer { + write-host "I'm the Outer" + Inner + function Inner { + write-host "I'm in the Inner Inner" + } + Inner + +} +Outer + +function Inner { + write-host "I'm the outer Inner" +} + +Outer +Inner diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs index 4cbefea5a..f88030385 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs @@ -38,6 +38,13 @@ internal static class RefactorsFunctionData Line = 4, RenameTo = "OneMethod" }; + public static readonly RenameSymbolParams FunctionsNestedOverlap = new() + { + FileName = "FunctionsNestedOverlap.ps1", + Column = 5, + Line = 15, + RenameTo = "OneMethod" + }; public static readonly RenameSymbolParams FunctionsSimpleFlat = new() { FileName = "FunctionsFlat.ps1", diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 888803527..1e24f15ef 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -98,7 +98,7 @@ public void RefactorMultipleFromCommandDef() }); } [Fact] - public void RefactorNestedFunction() + public void RefactorFunctionMultiple() { RenameSymbolParams request = RefactorsFunctionData.FunctionsMultiple; ScriptFile scriptFile = GetTestScript(request.FileName); @@ -126,7 +126,35 @@ public void RefactorNestedFunction() }); } [Fact] - public void RefactorFlatFunction() + public void RefactorNestedOverlapedFunction() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlap; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Equal(2, changes.Changes.Count); + + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 13 && + item.EndColumn == 16 && + item.StartLine == 8 && + item.EndLine == 8 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 4 && + item.EndColumn == 10 && + item.StartLine == 10 && + item.EndLine == 10 && + request.RenameTo == item.NewText; + }); + } + [Fact] + public void RefactorFunctionSimpleFlat() { RenameSymbolParams request = RefactorsFunctionData.FunctionsSimpleFlat; ScriptFile scriptFile = GetTestScript(request.FileName); From 77f4693b78ce6f6dcad90cee8f167e4dc9481982 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:55:46 +0100 Subject: [PATCH 009/203] initial commit --- .vscode/launch.json | 26 +++ .vscode/tasks.json | 41 ++++ .../PowerShell/Handlers/RenameSymbol.cs | 192 ++++++++++++++++-- .../PowerShell/Refactoring/FunctionVistor.cs | 0 .../Refactoring/RefactorsFunctionData.cs | 9 +- .../Refactoring/RefactorFunctionTests.cs | 32 ++- 6 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..69f85c365 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/test/PowerShellEditorServices.Test.E2E/bin/Debug/net7.0/PowerShellEditorServices.Test.E2E.dll", + "args": [], + "cwd": "${workspaceFolder}/test/PowerShellEditorServices.Test.E2E", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..18313ef31 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/PowerShellEditorServices.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/PowerShellEditorServices.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/PowerShellEditorServices.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 53f683257..1882399db 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using System.Linq; +using System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -77,21 +78,31 @@ public RenameSymbolHandler( } /// Method to get a symbols parent function(s) if any - internal static List GetParentFunctions(SymbolReference symbol, Ast Ast) + internal static List GetParentFunctions(SymbolReference symbol, Ast scriptAst) { - return new List(Ast.FindAll(ast => + return new List(scriptAst.FindAll(ast => { - return ast.Extent.StartLineNumber <= symbol.ScriptRegion.StartLineNumber && - ast.Extent.EndLineNumber >= symbol.ScriptRegion.EndLineNumber && - ast is FunctionDefinitionAst; + return ast is FunctionDefinitionAst && + // Less that start line + (ast.Extent.StartLineNumber < symbol.ScriptRegion.StartLineNumber-1 || ( + // OR same start line but less column start + ast.Extent.StartLineNumber <= symbol.ScriptRegion.StartLineNumber-1 && + ast.Extent.StartColumnNumber <= symbol.ScriptRegion.StartColumnNumber-1)) && + // AND Greater end line + (ast.Extent.EndLineNumber > symbol.ScriptRegion.EndLineNumber+1 || + // OR same end line but greater end column + (ast.Extent.EndLineNumber >= symbol.ScriptRegion.EndLineNumber+1 && + ast.Extent.EndColumnNumber >= symbol.ScriptRegion.EndColumnNumber+1)) + + ; }, true)); } - internal static IEnumerable GetVariablesWithinExtent(Ast symbol, Ast Ast) + internal static IEnumerable GetVariablesWithinExtent(SymbolReference symbol, Ast Ast) { return Ast.FindAll(ast => { - return ast.Extent.StartLineNumber >= symbol.Extent.StartLineNumber && - ast.Extent.EndLineNumber <= symbol.Extent.EndLineNumber && + return ast.Extent.StartLineNumber >= symbol.ScriptRegion.StartLineNumber && + ast.Extent.EndLineNumber <= symbol.ScriptRegion.EndLineNumber && ast is VariableExpressionAst; }, true); } @@ -157,6 +168,142 @@ internal static bool IsVarInFunctionParamBlock(Ast Function, SymbolReference sym return false; } + internal static FunctionDefinitionAst GetFunctionDefByCommandAst(SymbolReference Symbol, Ast scriptAst) + { + // Determins a functions definnition based on an inputed CommandAst object + // Gets all function definitions before the inputted CommandAst with the same name + // Sorts them from furthest to closest + // loops from the end of the list and checks if the function definition is a nested function + + + // We either get the CommandAst or the FunctionDefinitionAts + string functionName = ""; + List results = new(); + if (!Symbol.Name.Contains("function ")) + { + // + // Handle a CommandAst as the input + // + functionName = Symbol.Name; + + // Get the list of function definitions before this command call + List FunctionDefinitions = scriptAst.FindAll(ast => + { + return ast is FunctionDefinitionAst funcdef && + funcdef.Name.ToLower() == functionName.ToLower() && + (funcdef.Extent.EndLineNumber < Symbol.NameRegion.StartLineNumber || + (funcdef.Extent.EndColumnNumber < Symbol.NameRegion.StartColumnNumber && + funcdef.Extent.EndLineNumber <= Symbol.NameRegion.StartLineNumber)); + }, true).Cast().ToList(); + + // Last element after the sort should be the closes definition to the symbol inputted + FunctionDefinitions.Sort((a, b) => + { + return a.Extent.EndColumnNumber + a.Extent.EndLineNumber - + b.Extent.EndLineNumber + b.Extent.EndColumnNumber; + }); + + // retreive the ast object for the + StringConstantExpressionAst call = (StringConstantExpressionAst)scriptAst.Find(ast => + { + return ast is StringConstantExpressionAst funcCall && + ast.Parent is CommandAst && + funcCall.Value == Symbol.Name && + funcCall.Extent.StartLineNumber == Symbol.NameRegion.StartLineNumber && + funcCall.Extent.StartColumnNumber == Symbol.NameRegion.StartColumnNumber; + }, true); + + // Check if the definition is a nested call or not + // define what we think is this function definition + FunctionDefinitionAst SymbolsDefinition = null; + for (int i = FunctionDefinitions.Count() - 1; i > 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + // Get the elements parent functions if any + // Follow the parent looking for the first functionDefinition if any + Ast parent = element.Parent; + while (parent != null) + { + if (parent is FunctionDefinitionAst check) + { + + break; + } + parent = parent.Parent; + } + if (parent == null) + { + SymbolsDefinition = element; + break; + } + else + { + // check if the call and the definition are in the same parent function call + if (call.Parent == parent) + { + SymbolsDefinition = element; + } + } + // TODO figure out how to decide which function to be refactor + // / eliminate functions that are out of scope for this refactor call + } + // Closest same named function definition that is within the same function + // As the symbol but not in another function the symbol is nt apart of + return SymbolsDefinition; + } + // probably got a functiondefinition laready which defeats the point + return null; + } + internal static List GetFunctionReferences(SymbolReference function, Ast scriptAst) + { + List results = new(); + string FunctionName = function.Name.Replace("function ", "").Replace(" ()", ""); + + // retreive the ast object for the function + FunctionDefinitionAst functionAst = (FunctionDefinitionAst)scriptAst.Find(ast => + { + return ast is FunctionDefinitionAst funcCall && + funcCall.Name == function.Name & + funcCall.Extent.StartLineNumber == function.NameRegion.StartLineNumber && + funcCall.Extent.StartColumnNumber ==function.NameRegion.StartColumnNumber; + }, true); + Ast parent = functionAst.Parent; + + while (parent != null) + { + if (parent is FunctionDefinitionAst funcdef) + { + break; + } + parent = parent.Parent; + } + + if (parent != null) + { + List calls = (List)scriptAst.FindAll(ast => + { + return ast is StringConstantExpressionAst command && + command.Parent is CommandAst && command.Value == FunctionName && + // Command is greater than the function definition start line + (command.Extent.StartLineNumber > functionAst.Extent.EndLineNumber || + // OR starts after the end column line + (command.Extent.StartLineNumber >= functionAst.Extent.EndLineNumber && + command.Extent.StartColumnNumber >= functionAst.Extent.EndColumnNumber)) && + // AND the command is within the parent function the function is nested in + (command.Extent.EndLineNumber < parent.Extent.EndLineNumber || + // OR ends before the endcolumnline for the parent function + (command.Extent.EndLineNumber <= parent.Extent.EndLineNumber && + command.Extent.EndColumnNumber <= parent.Extent.EndColumnNumber + )); + },true); + + + }else{ + + } + + return results; + } internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) { if (symbol.Type is not SymbolType.Function) @@ -164,37 +311,38 @@ internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, As return null; } - // we either get the CommandAst or the FunctionDeginitionAts + // We either get the CommandAst or the FunctionDefinitionAts string functionName = !symbol.Name.Contains("function ") ? symbol.Name : symbol.Name.Replace("function ", "").Replace(" ()", ""); - - FunctionDefinitionAst funcDef = (FunctionDefinitionAst)scriptAst.Find(ast => + _ = GetFunctionDefByCommandAst(symbol, scriptAst); + _ = GetFunctionReferences(symbol, scriptAst); + IEnumerable funcDef = (IEnumerable)scriptAst.Find(ast => { return ast is FunctionDefinitionAst astfunc && astfunc.Name == functionName; }, true); - if (funcDef == null) - { - return null; - } + + + // No nice way to actually update the function name other than manually specifying the location // going to assume all function definitions start with "function " - ModifiedFileResponse FileModifications = new(request.FileName); - + // TODO update this to be the actual definition to rename + FunctionDefinitionAst funcDefToRename = funcDef.First(); FileModifications.Changes.Add(new TextChange { NewText = request.RenameTo, - StartLine = funcDef.Extent.StartLineNumber - 1, - EndLine = funcDef.Extent.StartLineNumber - 1, - StartColumn = funcDef.Extent.StartColumnNumber + "function ".Length - 1, - EndColumn = funcDef.Extent.StartColumnNumber + "function ".Length + funcDef.Name.Length - 1 + StartLine = funcDefToRename.Extent.StartLineNumber - 1, + EndLine = funcDefToRename.Extent.StartLineNumber - 1, + StartColumn = funcDefToRename.Extent.StartColumnNumber + "function ".Length - 1, + EndColumn = funcDefToRename.Extent.StartColumnNumber + "function ".Length + funcDefToRename.Name.Length - 1 }); + // TODO update this based on if there is nesting IEnumerable CommandCalls = scriptAst.FindAll(ast => { return ast is StringConstantExpressionAst funcCall && ast.Parent is CommandAst && - funcCall.Value == funcDef.Name; + funcCall.Value == funcDefToRename.Name; }, true); foreach (Ast CommandCall in CommandCalls) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs new file mode 100644 index 000000000..e69de29bb diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs index f88030385..3b933c376 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs @@ -38,13 +38,20 @@ internal static class RefactorsFunctionData Line = 4, RenameTo = "OneMethod" }; - public static readonly RenameSymbolParams FunctionsNestedOverlap = new() + public static readonly RenameSymbolParams FunctionsNestedOverlapCommand = new() { FileName = "FunctionsNestedOverlap.ps1", Column = 5, Line = 15, RenameTo = "OneMethod" }; + public static readonly RenameSymbolParams FunctionsNestedOverlapFunction = new() + { + FileName = "FunctionsNestedOverlap.ps1", + Column = 14, + Line = 16, + RenameTo = "OneMethod" + }; public static readonly RenameSymbolParams FunctionsSimpleFlat = new() { FileName = "FunctionsFlat.ps1", diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 1e24f15ef..7c0afe817 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -126,9 +126,9 @@ public void RefactorFunctionMultiple() }); } [Fact] - public void RefactorNestedOverlapedFunction() + public void RefactorNestedOverlapedFunctionCommand() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlap; + RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlapCommand; ScriptFile scriptFile = GetTestScript(request.FileName); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( request.Line + 1, @@ -154,6 +154,34 @@ public void RefactorNestedOverlapedFunction() }); } [Fact] + public void RefactorNestedOverlapedFunctionFunction() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlapFunction; + ScriptFile scriptFile = GetTestScript(request.FileName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); + Assert.Equal(2, changes.Changes.Count); + + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 14 && + item.EndColumn == 16 && + item.StartLine == 16 && + item.EndLine == 17 && + request.RenameTo == item.NewText; + }); + Assert.Contains(changes.Changes, item => + { + return item.StartColumn == 4 && + item.EndColumn == 10 && + item.StartLine == 19 && + item.EndLine == 19 && + request.RenameTo == item.NewText; + }); + } + [Fact] public void RefactorFunctionSimpleFlat() { RenameSymbolParams request = RefactorsFunctionData.FunctionsSimpleFlat; From f458287293855b150ae68a85ec25964ca72c6109 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:57:57 +0100 Subject: [PATCH 010/203] converted visitor class from powershell to C# --- .../PowerShell/Refactoring/FunctionVistor.cs | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs index e69de29bb..90252e1aa 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; +using System; + +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + internal class FunctionRename : ICustomAstVisitor2 + { + private readonly string OldName; + private readonly string NewName; + internal Stack ScopeStack = new(); + internal bool ShouldRename = false; + internal List Modifications = new(); + private readonly List Log = new(); + internal int StartLineNumber; + internal int StartColumnNumber; + internal FunctionDefinitionAst TargetFunctionAst; + internal FunctionDefinitionAst DuplicateFunctionAst; + internal readonly Ast ScriptAst; + + public FunctionRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + this.OldName = OldName; + this.StartLineNumber = StartLineNumber; + this.StartColumnNumber = StartColumnNumber; + this.ScriptAst = ScriptAst; + + Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + if (Node != null) + { + if (Node is FunctionDefinitionAst FuncDef) + { + TargetFunctionAst = FuncDef; + } + if (Node is CommandAst CommDef) + { + TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + if (TargetFunctionAst == null) + { + Log.Add("Failed to get the Commands Function Definition"); + } + this.StartColumnNumber = TargetFunctionAst.Extent.StartColumnNumber; + this.StartLineNumber = TargetFunctionAst.Extent.StartLineNumber; + } + } + } + public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + Ast result = null; + // Looking for a function + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is FunctionDefinitionAst FuncDef && + FuncDef.Name == OldName; + }, true); + // Looking for a a Command call + if (null == result) + { + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is CommandAst CommDef && + CommDef.GetCommandName() == OldName; + }, true); + } + + return result; + } + + public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + // Look up the targetted object + CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => + { + return ast is CommandAst CommDef && + CommDef.GetCommandName() == OldName && + CommDef.Extent.StartLineNumber == StartLineNumber && + CommDef.Extent.StartColumnNumber == StartColumnNumber; + }, true); + + string FunctionName = TargetCommand.GetCommandName(); + + List FunctionDefinitions = (List)ScriptFile.FindAll(ast => + { + return ast is FunctionDefinitionAst FuncDef && + (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || + (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && + FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); + }, true); + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Sort function definitions + FunctionDefinitions.Sort((a, b) => + { + return a.Extent.EndColumnNumber + a.Extent.EndLineNumber - + b.Extent.EndLineNumber + b.Extent.EndColumnNumber; + }); + // Determine which function definition is the right one + FunctionDefinitionAst CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetCommand.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + + public object VisitFunctionDefinition(FunctionDefinitionAst ast) + { + ScopeStack.Push("function_" + ast.Name); + + if (ast.Name == OldName) + { + if (ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber) + { + TargetFunctionAst = ast; + TextChange Change = new() + { + NewText = NewName, + StartLine = ast.Extent.StartLineNumber - 1, + StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, + EndLine = ast.Extent.StartLineNumber - 1, + EndColumn = ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1, + }; + + Modifications.Add(Change); + ShouldRename = true; + } + else + { + // Entering a duplicate functions scope and shouldnt rename + ShouldRename = false; + DuplicateFunctionAst = ast; + } + } + ast.Visit(this); + + ScopeStack.Pop(); + return null; + } + + public object VisitLoopStatement(LoopStatementAst ast) + { + + ScopeStack.Push("Loop"); + + ast.Body.Visit(this); + + ScopeStack.Pop(); + return null; + } + + public object VisitScriptBlock(ScriptBlockAst ast) + { + ScopeStack.Push("scriptblock"); + + ast.BeginBlock?.Visit(this); + ast.ProcessBlock?.Visit(this); + ast.EndBlock?.Visit(this); + ast.DynamicParamBlock.Visit(this); + + if (ShouldRename && TargetFunctionAst.Parent.Parent == ast) + { + ShouldRename = false; + } + + if (DuplicateFunctionAst.Parent.Parent == ast) + { + ShouldRename = true; + } + ScopeStack.Pop(); + + return null; + } + + public object VisitPipeline(PipelineAst ast) + { + foreach (Ast element in ast.PipelineElements) + { + element.Visit(this); + } + return null; + } + public object VisitAssignmentStatement(AssignmentStatementAst ast) + { + ast.Right.Visit(this); + return null; + } + public object VisitStatementBlock(StatementBlockAst ast) + { + foreach (StatementAst element in ast.Statements) + { + element.Visit(this); + } + return null; + } + public object VisitForStatement(ForStatementAst ast) + { + ast.Body.Visit(this); + return null; + } + public object VisitIfStatement(IfStatementAst ast) + { + foreach (Tuple element in ast.Clauses) + { + element.Item1.Visit(this); + element.Item2.Visit(this); + } + + ast.ElseClause?.Visit(this); + + return null; + } + public object VisitForEachStatement(ForEachStatementAst ast) + { + ast.Body.Visit(this); + return null; + } + public object VisitCommandExpression(CommandExpressionAst ast) + { + ast.Expression.Visit(this); + return null; + } + public object VisitScriptBlockExpression(ScriptBlockExpressionAst ast) + { + ast.ScriptBlock.Visit(this); + return null; + } + public object VisitNamedBlock(NamedBlockAst ast) + { + foreach (StatementAst element in ast.Statements) + { + element.Visit(this); + } + return null; + } + public object VisitCommand(CommandAst ast) + { + if (ast.GetCommandName() == OldName) + { + if (ShouldRename) + { + TextChange Change = new() + { + NewText = NewName, + StartLine = ast.Extent.StartLineNumber - 1, + StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, + EndLine = ast.Extent.StartLineNumber - 1, + EndColumn = ast.Extent.StartColumnNumber + ast.GetCommandName().Length - 1, + }; + } + } + foreach (CommandElementAst element in ast.CommandElements) + { + element.Visit(this); + } + + return null; + } + + public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); + public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); + public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => throw new NotImplementedException(); + public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); + public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => throw new NotImplementedException(); + public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => throw new NotImplementedException(); + public object VisitUsingStatement(UsingStatementAst usingStatement) => throw new NotImplementedException(); + public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); + public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); + public object VisitAttribute(AttributeAst attributeAst) => throw new NotImplementedException(); + public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); + public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => throw new NotImplementedException(); + public object VisitBlockStatement(BlockStatementAst blockStatementAst) => throw new NotImplementedException(); + public object VisitBreakStatement(BreakStatementAst breakStatementAst) => throw new NotImplementedException(); + public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); + public object VisitCommandParameter(CommandParameterAst commandParameterAst) => throw new NotImplementedException(); + public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => throw new NotImplementedException(); + public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); + public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => throw new NotImplementedException(); + public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); + public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => throw new NotImplementedException(); + public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => throw new NotImplementedException(); + public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => throw new NotImplementedException(); + public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => throw new NotImplementedException(); + public object VisitExitStatement(ExitStatementAst exitStatementAst) => throw new NotImplementedException(); + public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => throw new NotImplementedException(); + public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => throw new NotImplementedException(); + public object VisitHashtable(HashtableAst hashtableAst) => throw new NotImplementedException(); + public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => throw new NotImplementedException(); + public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); + public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => throw new NotImplementedException(); + public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => throw new NotImplementedException(); + public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => throw new NotImplementedException(); + public object VisitParamBlock(ParamBlockAst paramBlockAst) => throw new NotImplementedException(); + public object VisitParameter(ParameterAst parameterAst) => throw new NotImplementedException(); + public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => throw new NotImplementedException(); + public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => throw new NotImplementedException(); + public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => throw new NotImplementedException(); + public object VisitSubExpression(SubExpressionAst subExpressionAst) => throw new NotImplementedException(); + public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => throw new NotImplementedException(); + public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => throw new NotImplementedException(); + public object VisitTrap(TrapStatementAst trapStatementAst) => throw new NotImplementedException(); + public object VisitTryStatement(TryStatementAst tryStatementAst) => throw new NotImplementedException(); + public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => throw new NotImplementedException(); + public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => throw new NotImplementedException(); + public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => throw new NotImplementedException(); + public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => throw new NotImplementedException(); + public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => throw new NotImplementedException(); + public object VisitWhileStatement(WhileStatementAst whileStatementAst) => throw new NotImplementedException(); + } +} From d765fdad224071fd8c4fca526debb6d1f6fc795d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:34:58 +0100 Subject: [PATCH 011/203] Updated to using ps1 file with a renamed varient --- .../Refactoring/BasicFunction.ps1 | 5 + .../Refactoring/BasicFunctionRenamed.ps1 | 5 + .../Refactoring/CmdletFunction.ps1 | 21 ++++ .../Refactoring/CmdletFunctionRenamed.ps1 | 21 ++++ .../Refactoring/ForeachFunction.ps1 | 17 +++ .../Refactoring/ForeachFunctionRenamed.ps1 | 17 +++ .../Refactoring/ForeachObjectFunction.ps1 | 17 +++ .../ForeachObjectFunctionRenamed.ps1 | 17 +++ .../FunctionCallWIthinStringExpression.ps1 | 3 + ...ctionCallWIthinStringExpressionRenamed.ps1 | 3 + .../Refactoring/FunctionsFlat.ps1 | 1 - .../Refactoring/FunctionsMultiple.ps1 | 17 --- .../Refactoring/FunctionsNestedOverlap.ps1 | 30 ----- .../Refactoring/FunctionsNestedSimple.ps1 | 11 -- .../Refactoring/FunctionsSingle.ps1 | 5 - .../Refactoring/InnerFunction.ps1 | 7 ++ .../Refactoring/InnerFunctionRenamed.ps1 | 7 ++ .../Refactoring/InternalCalls.ps1 | 5 + .../Refactoring/InternalCallsRenamed.ps1 | 5 + .../Refactoring/LoopFunction.ps1 | 6 + .../Refactoring/LoopFunctionRenamed.ps1 | 6 + .../Refactoring/MultipleOccurrences.ps1 | 6 + .../MultipleOccurrencesRenamed.ps1 | 6 + .../Refactoring/NestedFunctions.ps1 | 13 ++ .../Refactoring/NestedFunctionsRenamed.ps1 | 13 ++ .../Refactoring/OuterFunction.ps1 | 7 ++ .../Refactoring/OuterFunctionRenamed.ps1 | 7 ++ .../Refactoring/RefactorsFunctionData.cs | 113 ++++++++++++------ .../Refactoring/SamenameFunctions.ps1 | 8 ++ .../Refactoring/SamenameFunctionsRenamed.ps1 | 8 ++ .../Refactoring/ScriptblockFunction.ps1 | 7 ++ .../ScriptblockFunctionRenamed.ps1 | 7 ++ 32 files changed, 320 insertions(+), 101 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 new file mode 100644 index 000000000..e13582550 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 @@ -0,0 +1,5 @@ +function foo { + Write-Host "Inside foo" +} + +foo diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 new file mode 100644 index 000000000..26ffe37f9 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 @@ -0,0 +1,5 @@ +function Renamed { + Write-Host "Inside foo" +} + +Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 new file mode 100644 index 000000000..1614b63a9 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 @@ -0,0 +1,21 @@ +function Testing-Foo { + [CmdletBinding(SupportsShouldProcess)] + param ( + $Text, + $Param + ) + + begin { + if ($PSCmdlet.ShouldProcess("Target", "Operation")) { + Testing-Foo -Text "Param" -Param [1,2,3] + } + } + + process { + Testing-Foo -Text "Param" -Param [1,2,3] + } + + end { + Testing-Foo -Text "Param" -Param [1,2,3] + } +} \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 new file mode 100644 index 000000000..ee14a9fb7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 @@ -0,0 +1,21 @@ +function Renamed { + [CmdletBinding(SupportsShouldProcess)] + param ( + $Text, + $Param + ) + + begin { + if ($PSCmdlet.ShouldProcess("Target", "Operation")) { + Renamed -Text "Param" -Param [1,2,3] + } + } + + process { + Renamed -Text "Param" -Param [1,2,3] + } + + end { + Renamed -Text "Param" -Param [1,2,3] + } +} \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 new file mode 100644 index 000000000..2454effe6 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function testing_files { + param ( + $x + ) + write-host "Printing $x" +} + +foreach ($number in $x) { + testing_files $number + + function testing_files { + write-host "------------------" + } +} +testing_files "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 new file mode 100644 index 000000000..cd0dcb424 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function Renamed { + param ( + $x + ) + write-host "Printing $x" +} + +foreach ($number in $x) { + Renamed $number + + function testing_files { + write-host "------------------" + } +} +Renamed "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 new file mode 100644 index 000000000..107c50223 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function testing_files { + param ( + $x + ) + write-host "Printing $x" +} + +$x | ForEach-Object { + testing_files $_ + + function testing_files { + write-host "------------------" + } +} +testing_files "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 new file mode 100644 index 000000000..80073c640 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function Renamed { + param ( + $x + ) + write-host "Printing $x" +} + +$x | ForEach-Object { + Renamed $_ + + function testing_files { + write-host "------------------" + } +} +Renamed "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 new file mode 100644 index 000000000..944e6d5df --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 @@ -0,0 +1,3 @@ +function foo { + write-host "This will do recursion ... $(foo)" +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 new file mode 100644 index 000000000..44d843c5a --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 @@ -0,0 +1,3 @@ +function Renamed { + write-host "This will do recursion ... $(Renamed)" +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 deleted file mode 100644 index 939e723ee..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsFlat.ps1 +++ /dev/null @@ -1 +0,0 @@ -{function Cat1 {write-host "The Cat"};function Dog {Cat1;write-host "jumped ..."}Dog} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 deleted file mode 100644 index f38f00257..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsMultiple.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -function One { - write-host "One Hello World" -} -function Two { - write-host "Two Hello World" - One -} - -function Three { - write-host "Three Hello" - Two -} - -Function Four { - Write-host "Four Hello" - One -} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 deleted file mode 100644 index aa1483936..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedOverlap.ps1 +++ /dev/null @@ -1,30 +0,0 @@ - -function Inner { - write-host "I'm the First Inner" -} -function foo { - function Inner { - write-host "Shouldnt be called or renamed at all." - } -} -function Inner { - write-host "I'm the First Inner" -} - -function Outer { - write-host "I'm the Outer" - Inner - function Inner { - write-host "I'm in the Inner Inner" - } - Inner - -} -Outer - -function Inner { - write-host "I'm the outer Inner" -} - -Outer -Inner diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 deleted file mode 100644 index 2d0746723..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsNestedSimple.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -function Outer { - write-host "Hello World" - - function Inner { - write-host "Hello World" - } - Inner - -} - -SingleFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 deleted file mode 100644 index f8670166d..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionsSingle.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -function SingleFunction { - write-host "Hello World" -} - -SingleFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 new file mode 100644 index 000000000..966fdccb7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 @@ -0,0 +1,7 @@ +function OuterFunction { + function NewInnerFunction { + Write-Host "This is the inner function" + } + NewInnerFunction +} +OuterFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 new file mode 100644 index 000000000..47e51012e --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 @@ -0,0 +1,7 @@ +function OuterFunction { + function RenamedInnerFunction { + Write-Host "This is the inner function" + } + RenamedInnerFunction +} +OuterFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 new file mode 100644 index 000000000..eae1f3a19 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 @@ -0,0 +1,5 @@ +function FunctionWithInternalCalls { + Write-Host "This function calls itself" + FunctionWithInternalCalls +} +FunctionWithInternalCalls diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 new file mode 100644 index 000000000..4926dffb9 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 @@ -0,0 +1,5 @@ +function Renamed { + Write-Host "This function calls itself" + Renamed +} +Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 new file mode 100644 index 000000000..6973855a7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 @@ -0,0 +1,6 @@ +for ($i = 0; $i -lt 2; $i++) { + function FunctionInLoop { + Write-Host "Function inside a loop" + } + FunctionInLoop +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 new file mode 100644 index 000000000..6e7632c46 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 @@ -0,0 +1,6 @@ +for ($i = 0; $i -lt 2; $i++) { + function Renamed { + Write-Host "Function inside a loop" + } + Renamed +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 new file mode 100644 index 000000000..76aeced88 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 @@ -0,0 +1,6 @@ +function foo { + Write-Host "Inside foo" +} + +foo +foo diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 new file mode 100644 index 000000000..cb78322be --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 @@ -0,0 +1,6 @@ +function Renamed { + Write-Host "Inside foo" +} + +Renamed +Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 new file mode 100644 index 000000000..8e99c337b --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 @@ -0,0 +1,13 @@ +function outer { + function foo { + Write-Host "Inside nested foo" + } + foo +} + +function foo { + Write-Host "Inside top-level foo" +} + +outer +foo diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 new file mode 100644 index 000000000..2231571ef --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 @@ -0,0 +1,13 @@ +function outer { + function bar { + Write-Host "Inside nested foo" + } + bar +} + +function foo { + Write-Host "Inside top-level foo" +} + +outer +foo diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 new file mode 100644 index 000000000..966fdccb7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 @@ -0,0 +1,7 @@ +function OuterFunction { + function NewInnerFunction { + Write-Host "This is the inner function" + } + NewInnerFunction +} +OuterFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 new file mode 100644 index 000000000..cd4062eb0 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 @@ -0,0 +1,7 @@ +function RenamedOuterFunction { + function NewInnerFunction { + Write-Host "This is the inner function" + } + NewInnerFunction +} +RenamedOuterFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs index 3b933c376..8c06f0d14 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs @@ -6,58 +6,97 @@ namespace PowerShellEditorServices.Test.Shared.Refactoring { internal static class RefactorsFunctionData { - public static readonly RenameSymbolParams FunctionsMultiple = new() + + public static readonly RenameSymbolParams FunctionsSingle = new() { - // rename function Two { ...} - FileName = "FunctionsMultiple.ps1", - Column = 9, - Line = 3, - RenameTo = "TwoFours" + FileName = "BasicFunction.ps1", + Column = 1, + Line = 5, + RenameTo = "Renamed" }; - public static readonly RenameSymbolParams FunctionsMultipleFromCommandDef = new() + public static readonly RenameSymbolParams FunctionMultipleOccurrences = new() { - // ... write-host "Three Hello" ... - // Two - // - FileName = "FunctionsMultiple.ps1", - Column = 5, - Line = 15, - RenameTo = "OnePlusOne" + FileName = "MultipleOccurrences.ps1", + Column = 1, + Line = 5, + RenameTo = "Renamed" }; - public static readonly RenameSymbolParams FunctionsSingleParams = new() + public static readonly RenameSymbolParams FunctionInnerIsNested = new() { - FileName = "FunctionsSingle.ps1", - Column = 9, - Line = 0, - RenameTo = "OneMethod" + FileName = "NestedFunctions.ps1", + Column = 5, + Line = 5, + RenameTo = "bar" }; - public static readonly RenameSymbolParams FunctionsSingleNested = new() + public static readonly RenameSymbolParams FunctionOuterHasNestedFunction = new() { - FileName = "FunctionsNestedSimple.ps1", - Column = 16, - Line = 4, - RenameTo = "OneMethod" + FileName = "OuterFunction.ps1", + Column = 10, + Line = 1, + RenameTo = "RenamedOuterFunction" }; - public static readonly RenameSymbolParams FunctionsNestedOverlapCommand = new() + public static readonly RenameSymbolParams FunctionWithInnerFunction = new() { - FileName = "FunctionsNestedOverlap.ps1", + FileName = "InnerFunction.ps1", Column = 5, - Line = 15, - RenameTo = "OneMethod" + Line = 5, + RenameTo = "RenamedInnerFunction" + }; + public static readonly RenameSymbolParams FunctionWithInternalCalls = new() + { + FileName = "InternalCalls.ps1", + Column = 1, + Line = 5, + RenameTo = "Renamed" }; - public static readonly RenameSymbolParams FunctionsNestedOverlapFunction = new() + public static readonly RenameSymbolParams FunctionCmdlet = new() { - FileName = "FunctionsNestedOverlap.ps1", + FileName = "CmdletFunction.ps1", + Column = 10, + Line = 1, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams FunctionSameName = new() + { + FileName = "SamenameFunctions.ps1", Column = 14, - Line = 16, - RenameTo = "OneMethod" + Line = 3, + RenameTo = "RenamedSameNameFunction" + }; + public static readonly RenameSymbolParams FunctionScriptblock = new() + { + FileName = "ScriptblockFunction.ps1", + Column = 5, + Line = 5, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams FunctionLoop = new() + { + FileName = "LoopFunction.ps1", + Column = 5, + Line = 5, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams FunctionForeach = new() + { + FileName = "ForeachFunction.ps1", + Column = 5, + Line = 11, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams FunctionForeachObject = new() + { + FileName = "ForeachObjectFunction.ps1", + Column = 5, + Line = 11, + RenameTo = "Renamed" }; - public static readonly RenameSymbolParams FunctionsSimpleFlat = new() + public static readonly RenameSymbolParams FunctionCallWIthinStringExpression = new() { - FileName = "FunctionsFlat.ps1", - Column = 81, - Line = 0, - RenameTo = "ChangedFlat" + FileName = "FunctionCallWIthinStringExpression.ps1", + Column = 10, + Line = 1, + RenameTo = "Renamed" }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 new file mode 100644 index 000000000..726ea6d56 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 @@ -0,0 +1,8 @@ +function SameNameFunction { + Write-Host "This is the outer function" + function SameNameFunction { + Write-Host "This is the inner function" + } + SameNameFunction +} +SameNameFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 new file mode 100644 index 000000000..669266740 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 @@ -0,0 +1,8 @@ +function SameNameFunction { + Write-Host "This is the outer function" + function RenamedSameNameFunction { + Write-Host "This is the inner function" + } + RenamedSameNameFunction +} +SameNameFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 new file mode 100644 index 000000000..de0fd1737 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 @@ -0,0 +1,7 @@ +$scriptBlock = { + function FunctionInScriptBlock { + Write-Host "Inside a script block" + } + FunctionInScriptBlock +} +& $scriptBlock diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 new file mode 100644 index 000000000..727ca6f58 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 @@ -0,0 +1,7 @@ +$scriptBlock = { + function Renamed { + Write-Host "Inside a script block" + } + Renamed +} +& $scriptBlock From bb9847c81a3b7aa2b33db86f61043b36305462be Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:35:50 +0100 Subject: [PATCH 012/203] Bug Fixes now passing all tests --- .../PowerShell/Refactoring/FunctionVistor.cs | 153 +++++---- .../Refactoring/RefactorFunctionTests.cs | 309 ++++++++++-------- 2 files changed, 258 insertions(+), 204 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs index 90252e1aa..603dc9037 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs @@ -5,6 +5,7 @@ using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Handlers; using System; +using System.Linq; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -13,8 +14,8 @@ internal class FunctionRename : ICustomAstVisitor2 private readonly string OldName; private readonly string NewName; internal Stack ScopeStack = new(); - internal bool ShouldRename = false; - internal List Modifications = new(); + internal bool ShouldRename; + public List Modifications = new(); private readonly List Log = new(); internal int StartLineNumber; internal int StartColumnNumber; @@ -25,9 +26,11 @@ internal class FunctionRename : ICustomAstVisitor2 public FunctionRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { this.OldName = OldName; + this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; + this.ShouldRename = false; Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -57,7 +60,7 @@ public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, return ast.Extent.StartLineNumber == StartLineNumber && ast.Extent.StartColumnNumber == StartColumnNumber && ast is FunctionDefinitionAst FuncDef && - FuncDef.Name == OldName; + FuncDef.Name.ToLower() == OldName.ToLower(); }, true); // Looking for a a Command call if (null == result) @@ -67,7 +70,7 @@ ast is FunctionDefinitionAst FuncDef && return ast.Extent.StartLineNumber == StartLineNumber && ast.Extent.StartColumnNumber == StartColumnNumber && ast is CommandAst CommDef && - CommDef.GetCommandName() == OldName; + CommDef.GetCommandName().ToLower() == OldName.ToLower(); }, true); } @@ -80,31 +83,32 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => { return ast is CommandAst CommDef && - CommDef.GetCommandName() == OldName && + CommDef.GetCommandName().ToLower() == OldName.ToLower() && CommDef.Extent.StartLineNumber == StartLineNumber && CommDef.Extent.StartColumnNumber == StartColumnNumber; }, true); string FunctionName = TargetCommand.GetCommandName(); - List FunctionDefinitions = (List)ScriptFile.FindAll(ast => + List FunctionDefinitions = ScriptFile.FindAll(ast => { return ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower() && (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true); + }, true).Cast().ToList(); // return the function def if we only have one match if (FunctionDefinitions.Count == 1) { return FunctionDefinitions[0]; } // Sort function definitions - FunctionDefinitions.Sort((a, b) => - { - return a.Extent.EndColumnNumber + a.Extent.EndLineNumber - - b.Extent.EndLineNumber + b.Extent.EndColumnNumber; - }); + //FunctionDefinitions.Sort((a, b) => + //{ + // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - + // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; + //}); // Determine which function definition is the right one FunctionDefinitionAst CorrectDefinition = null; for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) @@ -165,7 +169,7 @@ public object VisitFunctionDefinition(FunctionDefinitionAst ast) DuplicateFunctionAst = ast; } } - ast.Visit(this); + ast.Body.Visit(this); ScopeStack.Pop(); return null; @@ -189,14 +193,14 @@ public object VisitScriptBlock(ScriptBlockAst ast) ast.BeginBlock?.Visit(this); ast.ProcessBlock?.Visit(this); ast.EndBlock?.Visit(this); - ast.DynamicParamBlock.Visit(this); + ast.DynamicParamBlock?.Visit(this); if (ShouldRename && TargetFunctionAst.Parent.Parent == ast) { ShouldRename = false; } - if (DuplicateFunctionAst.Parent.Parent == ast) + if (DuplicateFunctionAst?.Parent.Parent == ast) { ShouldRename = true; } @@ -224,6 +228,12 @@ public object VisitStatementBlock(StatementBlockAst ast) { element.Visit(this); } + + if (DuplicateFunctionAst?.Parent == ast) + { + ShouldRename = true; + } + return null; } public object VisitForStatement(ForStatementAst ast) @@ -276,10 +286,11 @@ public object VisitCommand(CommandAst ast) { NewText = NewName, StartLine = ast.Extent.StartLineNumber - 1, - StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, + StartColumn = ast.Extent.StartColumnNumber - 1, EndLine = ast.Extent.StartLineNumber - 1, - EndColumn = ast.Extent.StartColumnNumber + ast.GetCommandName().Length - 1, + EndColumn = ast.Extent.StartColumnNumber + OldName.Length - 1, }; + Modifications.Add(Change); } } foreach (CommandElementAst element in ast.CommandElements) @@ -290,54 +301,64 @@ public object VisitCommand(CommandAst ast) return null; } - public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); - public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => throw new NotImplementedException(); - public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); - public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => throw new NotImplementedException(); - public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => throw new NotImplementedException(); - public object VisitUsingStatement(UsingStatementAst usingStatement) => throw new NotImplementedException(); - public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); - public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); - public object VisitAttribute(AttributeAst attributeAst) => throw new NotImplementedException(); - public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); - public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => throw new NotImplementedException(); - public object VisitBlockStatement(BlockStatementAst blockStatementAst) => throw new NotImplementedException(); - public object VisitBreakStatement(BreakStatementAst breakStatementAst) => throw new NotImplementedException(); - public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); - public object VisitCommandParameter(CommandParameterAst commandParameterAst) => throw new NotImplementedException(); - public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => throw new NotImplementedException(); - public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); - public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => throw new NotImplementedException(); - public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); - public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => throw new NotImplementedException(); - public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => throw new NotImplementedException(); - public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => throw new NotImplementedException(); - public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => throw new NotImplementedException(); - public object VisitExitStatement(ExitStatementAst exitStatementAst) => throw new NotImplementedException(); - public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) => throw new NotImplementedException(); - public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => throw new NotImplementedException(); - public object VisitHashtable(HashtableAst hashtableAst) => throw new NotImplementedException(); - public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => throw new NotImplementedException(); - public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => throw new NotImplementedException(); - public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => throw new NotImplementedException(); - public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => throw new NotImplementedException(); - public object VisitParamBlock(ParamBlockAst paramBlockAst) => throw new NotImplementedException(); - public object VisitParameter(ParameterAst parameterAst) => throw new NotImplementedException(); - public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => throw new NotImplementedException(); - public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => throw new NotImplementedException(); - public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => throw new NotImplementedException(); - public object VisitSubExpression(SubExpressionAst subExpressionAst) => throw new NotImplementedException(); - public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => throw new NotImplementedException(); - public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => throw new NotImplementedException(); - public object VisitTrap(TrapStatementAst trapStatementAst) => throw new NotImplementedException(); - public object VisitTryStatement(TryStatementAst tryStatementAst) => throw new NotImplementedException(); - public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => throw new NotImplementedException(); - public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => throw new NotImplementedException(); - public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => throw new NotImplementedException(); - public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => throw new NotImplementedException(); - public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => throw new NotImplementedException(); - public object VisitWhileStatement(WhileStatementAst whileStatementAst) => throw new NotImplementedException(); + public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => null; + public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => null; + public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => null; + public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => null; + public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => null; + public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => null; + public object VisitUsingStatement(UsingStatementAst usingStatement) => null; + public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => null; + public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => null; + public object VisitAttribute(AttributeAst attributeAst) => null; + public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => null; + public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => null; + public object VisitBlockStatement(BlockStatementAst blockStatementAst) => null; + public object VisitBreakStatement(BreakStatementAst breakStatementAst) => null; + public object VisitCatchClause(CatchClauseAst catchClauseAst) => null; + public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null; + public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; + public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => null; + public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => null; + public object VisitDataStatement(DataStatementAst dataStatementAst) => null; + public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => null; + public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => null; + public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => null; + public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => null; + public object VisitExitStatement(ExitStatementAst exitStatementAst) => null; + public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst){ + + foreach (ExpressionAst element in expandableStringExpressionAst.NestedExpressions) + { + element.Visit(this); + } + return null; + } + public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => null; + public object VisitHashtable(HashtableAst hashtableAst) => null; + public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => null; + public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => null; + public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => null; + public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => null; + public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => null; + public object VisitParamBlock(ParamBlockAst paramBlockAst) => null; + public object VisitParameter(ParameterAst parameterAst) => null; + public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => null; + public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => null; + public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; + public object VisitSubExpression(SubExpressionAst subExpressionAst) { + subExpressionAst.SubExpression.Visit(this); + return null; + } + public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => null; + public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => null; + public object VisitTrap(TrapStatementAst trapStatementAst) => null; + public object VisitTryStatement(TryStatementAst tryStatementAst) => null; + public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null; + public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => null; + public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => null; + public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => null; + public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => null; + public object VisitWhileStatement(WhileStatementAst whileStatementAst) => null; } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 7c0afe817..c7ba82774 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -13,6 +13,7 @@ using Xunit; using Microsoft.PowerShell.EditorServices.Services.Symbols; using PowerShellEditorServices.Test.Shared.Refactoring; +using Microsoft.PowerShell.EditorServices.Refactoring; namespace PowerShellEditorServices.Test.Refactoring { @@ -30,6 +31,40 @@ public void Dispose() GC.SuppressFinalize(this); } private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", fileName))); + + internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) + { + + string[] Lines = OriginalScript.Split( + new string[] { Environment.NewLine }, + StringSplitOptions.None); + + foreach (TextChange change in Modification.Changes) + { + string TargetLine = Lines[change.StartLine]; + string begin = TargetLine.Substring(0, change.StartColumn); + string end = TargetLine.Substring(change.EndColumn); + Lines[change.StartLine] = begin + change.NewText + end; + } + + return string.Join(Environment.NewLine, Lines); + } + + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request, SymbolReference symbol) + { + + FunctionRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); + scriptFile.ScriptAst.Visit(visitor); + ModifiedFileResponse changes = new(request.FileName) + { + Changes = visitor.Modifications + }; + return GetModifiedScript(scriptFile.Contents, changes); + } public RefactorFunctionTests() { psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); @@ -38,176 +73,174 @@ public RefactorFunctionTests() [Fact] public void RefactorFunctionSingle() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsSingleParams; + RenameSymbolParams request = RefactorsFunctionData.FunctionsSingle; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 9 && - item.EndColumn == 23 && - item.StartLine == 0 && - item.EndLine == 0 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 0 && - item.EndColumn == 14 && - item.StartLine == 4 && - item.EndLine == 4 && - request.RenameTo == item.NewText; - }); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } [Fact] - public void RefactorMultipleFromCommandDef() + public void RenameFunctionMultipleOccurrences() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsMultipleFromCommandDef; + RenameSymbolParams request = RefactorsFunctionData.FunctionMultipleOccurrences; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Equal(3, changes.Changes.Count); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 9 && - item.EndColumn == 12 && - item.StartLine == 0 && - item.EndLine == 0 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 4 && - item.EndColumn == 7 && - item.StartLine == 5 && - item.EndLine == 5 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 4 && - item.EndColumn == 7 && - item.StartLine == 15 && - item.EndLine == 15 && - request.RenameTo == item.NewText; - }); } [Fact] - public void RefactorFunctionMultiple() + public void RenameFunctionNested() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsMultiple; + RenameSymbolParams request = RefactorsFunctionData.FunctionInnerIsNested; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Equal(2, changes.Changes.Count); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 13 && - item.EndColumn == 16 && - item.StartLine == 4 && - item.EndLine == 4 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 4 && - item.EndColumn == 10 && - item.StartLine == 6 && - item.EndLine == 6 && - request.RenameTo == item.NewText; - }); + Assert.Equal(expectedContent.Contents, modifiedcontent); } [Fact] - public void RefactorNestedOverlapedFunctionCommand() + public void RenameFunctionOuterHasNestedFunction() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlapCommand; + RenameSymbolParams request = RefactorsFunctionData.FunctionOuterHasNestedFunction; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Equal(2, changes.Changes.Count); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 13 && - item.EndColumn == 16 && - item.StartLine == 8 && - item.EndLine == 8 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 4 && - item.EndColumn == 10 && - item.StartLine == 10 && - item.EndLine == 10 && - request.RenameTo == item.NewText; - }); } [Fact] - public void RefactorNestedOverlapedFunctionFunction() + public void RenameFunctionInnerIsNested() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsNestedOverlapFunction; + RenameSymbolParams request = RefactorsFunctionData.FunctionInnerIsNested; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Equal(2, changes.Changes.Count); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 14 && - item.EndColumn == 16 && - item.StartLine == 16 && - item.EndLine == 17 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 4 && - item.EndColumn == 10 && - item.StartLine == 19 && - item.EndLine == 19 && - request.RenameTo == item.NewText; - }); + Assert.Equal(expectedContent.Contents, modifiedcontent); } [Fact] - public void RefactorFunctionSimpleFlat() + public void RenameFunctionWithInternalCalls() { - RenameSymbolParams request = RefactorsFunctionData.FunctionsSimpleFlat; + RenameSymbolParams request = RefactorsFunctionData.FunctionWithInternalCalls; ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - ModifiedFileResponse changes = RenameSymbolHandler.RefactorFunction(symbol, scriptFile.ScriptAst, request); - Assert.Equal(2, changes.Changes.Count); + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 47 && - item.EndColumn == 50 && - item.StartLine == 0 && - item.EndLine == 0 && - request.RenameTo == item.NewText; - }); - Assert.Contains(changes.Changes, item => - { - return item.StartColumn == 81 && - item.EndColumn == 84 && - item.StartLine == 0 && - item.EndLine == 0 && - request.RenameTo == item.NewText; - }); + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionCmdlet() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionCmdlet; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionSameName() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionSameName; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionInSscriptblock() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionScriptblock; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionInLoop() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionLoop; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionInForeach() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionForeach; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionInForeachObject() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionForeachObject; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void RenameFunctionCallWIthinStringExpression() + { + RenameSymbolParams request = RefactorsFunctionData.FunctionCallWIthinStringExpression; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); } } } From 64e89bdfe312a8a0426d70d33447c3041c5ea1e7 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:36:15 +0100 Subject: [PATCH 013/203] Stripped un-needed functions now using FunctionRename class --- .../PowerShell/Handlers/RenameSymbol.cs | 282 +----------------- 1 file changed, 11 insertions(+), 271 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 1882399db..5ef611f96 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -11,8 +11,7 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using System.Linq; -using System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Refactoring; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -69,241 +68,11 @@ internal class RenameSymbolHandler : IRenameSymbolHandler private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; - public RenameSymbolHandler( - ILoggerFactory loggerFactory, - WorkspaceService workspaceService) + public RenameSymbolHandler(ILoggerFactory loggerFactory,WorkspaceService workspaceService) { _logger = loggerFactory.CreateLogger(); _workspaceService = workspaceService; } - - /// Method to get a symbols parent function(s) if any - internal static List GetParentFunctions(SymbolReference symbol, Ast scriptAst) - { - return new List(scriptAst.FindAll(ast => - { - return ast is FunctionDefinitionAst && - // Less that start line - (ast.Extent.StartLineNumber < symbol.ScriptRegion.StartLineNumber-1 || ( - // OR same start line but less column start - ast.Extent.StartLineNumber <= symbol.ScriptRegion.StartLineNumber-1 && - ast.Extent.StartColumnNumber <= symbol.ScriptRegion.StartColumnNumber-1)) && - // AND Greater end line - (ast.Extent.EndLineNumber > symbol.ScriptRegion.EndLineNumber+1 || - // OR same end line but greater end column - (ast.Extent.EndLineNumber >= symbol.ScriptRegion.EndLineNumber+1 && - ast.Extent.EndColumnNumber >= symbol.ScriptRegion.EndColumnNumber+1)) - - ; - }, true)); - } - internal static IEnumerable GetVariablesWithinExtent(SymbolReference symbol, Ast Ast) - { - return Ast.FindAll(ast => - { - return ast.Extent.StartLineNumber >= symbol.ScriptRegion.StartLineNumber && - ast.Extent.EndLineNumber <= symbol.ScriptRegion.EndLineNumber && - ast is VariableExpressionAst; - }, true); - } - internal static Ast GetLargestExtentInCollection(IEnumerable Nodes) - { - Ast LargestNode = null; - foreach (Ast Node in Nodes) - { - LargestNode ??= Node; - if (Node.Extent.EndLineNumber - Node.Extent.StartLineNumber > - LargestNode.Extent.EndLineNumber - LargestNode.Extent.StartLineNumber) - { - LargestNode = Node; - } - } - return LargestNode; - } - internal static Ast GetSmallestExtentInCollection(IEnumerable Nodes) - { - Ast SmallestNode = null; - foreach (Ast Node in Nodes) - { - SmallestNode ??= Node; - if (Node.Extent.EndLineNumber - Node.Extent.StartLineNumber < - SmallestNode.Extent.EndLineNumber - SmallestNode.Extent.StartLineNumber) - { - SmallestNode = Node; - } - } - return SmallestNode; - } - internal static List GetFunctionExcludedNestedFunctions(Ast function, SymbolReference symbol) - { - IEnumerable nestedFunctions = function.FindAll(ast => ast is FunctionDefinitionAst && ast != function, true); - List excludeExtents = new(); - foreach (Ast nestedfunction in nestedFunctions) - { - if (IsVarInFunctionParamBlock(nestedfunction, symbol)) - { - excludeExtents.Add(nestedfunction); - } - } - return excludeExtents; - } - internal static bool IsVarInFunctionParamBlock(Ast Function, SymbolReference symbol) - { - Ast paramBlock = Function.Find(ast => ast is ParamBlockAst, true); - if (paramBlock != null) - { - IEnumerable variables = paramBlock.FindAll(ast => - { - return ast is VariableExpressionAst && - ast.Parent is ParameterAst; - }, true); - foreach (VariableExpressionAst variable in variables.Cast()) - { - if (variable.Extent.Text == symbol.ScriptRegion.Text) - { - return true; - } - } - } - return false; - } - - internal static FunctionDefinitionAst GetFunctionDefByCommandAst(SymbolReference Symbol, Ast scriptAst) - { - // Determins a functions definnition based on an inputed CommandAst object - // Gets all function definitions before the inputted CommandAst with the same name - // Sorts them from furthest to closest - // loops from the end of the list and checks if the function definition is a nested function - - - // We either get the CommandAst or the FunctionDefinitionAts - string functionName = ""; - List results = new(); - if (!Symbol.Name.Contains("function ")) - { - // - // Handle a CommandAst as the input - // - functionName = Symbol.Name; - - // Get the list of function definitions before this command call - List FunctionDefinitions = scriptAst.FindAll(ast => - { - return ast is FunctionDefinitionAst funcdef && - funcdef.Name.ToLower() == functionName.ToLower() && - (funcdef.Extent.EndLineNumber < Symbol.NameRegion.StartLineNumber || - (funcdef.Extent.EndColumnNumber < Symbol.NameRegion.StartColumnNumber && - funcdef.Extent.EndLineNumber <= Symbol.NameRegion.StartLineNumber)); - }, true).Cast().ToList(); - - // Last element after the sort should be the closes definition to the symbol inputted - FunctionDefinitions.Sort((a, b) => - { - return a.Extent.EndColumnNumber + a.Extent.EndLineNumber - - b.Extent.EndLineNumber + b.Extent.EndColumnNumber; - }); - - // retreive the ast object for the - StringConstantExpressionAst call = (StringConstantExpressionAst)scriptAst.Find(ast => - { - return ast is StringConstantExpressionAst funcCall && - ast.Parent is CommandAst && - funcCall.Value == Symbol.Name && - funcCall.Extent.StartLineNumber == Symbol.NameRegion.StartLineNumber && - funcCall.Extent.StartColumnNumber == Symbol.NameRegion.StartColumnNumber; - }, true); - - // Check if the definition is a nested call or not - // define what we think is this function definition - FunctionDefinitionAst SymbolsDefinition = null; - for (int i = FunctionDefinitions.Count() - 1; i > 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - // Get the elements parent functions if any - // Follow the parent looking for the first functionDefinition if any - Ast parent = element.Parent; - while (parent != null) - { - if (parent is FunctionDefinitionAst check) - { - - break; - } - parent = parent.Parent; - } - if (parent == null) - { - SymbolsDefinition = element; - break; - } - else - { - // check if the call and the definition are in the same parent function call - if (call.Parent == parent) - { - SymbolsDefinition = element; - } - } - // TODO figure out how to decide which function to be refactor - // / eliminate functions that are out of scope for this refactor call - } - // Closest same named function definition that is within the same function - // As the symbol but not in another function the symbol is nt apart of - return SymbolsDefinition; - } - // probably got a functiondefinition laready which defeats the point - return null; - } - internal static List GetFunctionReferences(SymbolReference function, Ast scriptAst) - { - List results = new(); - string FunctionName = function.Name.Replace("function ", "").Replace(" ()", ""); - - // retreive the ast object for the function - FunctionDefinitionAst functionAst = (FunctionDefinitionAst)scriptAst.Find(ast => - { - return ast is FunctionDefinitionAst funcCall && - funcCall.Name == function.Name & - funcCall.Extent.StartLineNumber == function.NameRegion.StartLineNumber && - funcCall.Extent.StartColumnNumber ==function.NameRegion.StartColumnNumber; - }, true); - Ast parent = functionAst.Parent; - - while (parent != null) - { - if (parent is FunctionDefinitionAst funcdef) - { - break; - } - parent = parent.Parent; - } - - if (parent != null) - { - List calls = (List)scriptAst.FindAll(ast => - { - return ast is StringConstantExpressionAst command && - command.Parent is CommandAst && command.Value == FunctionName && - // Command is greater than the function definition start line - (command.Extent.StartLineNumber > functionAst.Extent.EndLineNumber || - // OR starts after the end column line - (command.Extent.StartLineNumber >= functionAst.Extent.EndLineNumber && - command.Extent.StartColumnNumber >= functionAst.Extent.EndColumnNumber)) && - // AND the command is within the parent function the function is nested in - (command.Extent.EndLineNumber < parent.Extent.EndLineNumber || - // OR ends before the endcolumnline for the parent function - (command.Extent.EndLineNumber <= parent.Extent.EndLineNumber && - command.Extent.EndColumnNumber <= parent.Extent.EndColumnNumber - )); - },true); - - - }else{ - - } - - return results; - } internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) { if (symbol.Type is not SymbolType.Function) @@ -311,45 +80,16 @@ internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, As return null; } - // We either get the CommandAst or the FunctionDefinitionAts - string functionName = !symbol.Name.Contains("function ") ? symbol.Name : symbol.Name.Replace("function ", "").Replace(" ()", ""); - _ = GetFunctionDefByCommandAst(symbol, scriptAst); - _ = GetFunctionReferences(symbol, scriptAst); - IEnumerable funcDef = (IEnumerable)scriptAst.Find(ast => + FunctionRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptAst); + scriptAst.Visit(visitor); + ModifiedFileResponse FileModifications = new(request.FileName) { - return ast is FunctionDefinitionAst astfunc && - astfunc.Name == functionName; - }, true); - - - - // No nice way to actually update the function name other than manually specifying the location - // going to assume all function definitions start with "function " - ModifiedFileResponse FileModifications = new(request.FileName); - // TODO update this to be the actual definition to rename - FunctionDefinitionAst funcDefToRename = funcDef.First(); - FileModifications.Changes.Add(new TextChange - { - NewText = request.RenameTo, - StartLine = funcDefToRename.Extent.StartLineNumber - 1, - EndLine = funcDefToRename.Extent.StartLineNumber - 1, - StartColumn = funcDefToRename.Extent.StartColumnNumber + "function ".Length - 1, - EndColumn = funcDefToRename.Extent.StartColumnNumber + "function ".Length + funcDefToRename.Name.Length - 1 - }); - - // TODO update this based on if there is nesting - IEnumerable CommandCalls = scriptAst.FindAll(ast => - { - return ast is StringConstantExpressionAst funcCall && - ast.Parent is CommandAst && - funcCall.Value == funcDefToRename.Name; - }, true); - - foreach (Ast CommandCall in CommandCalls) - { - FileModifications.AddTextChange(CommandCall, request.RenameTo); - } - + Changes = visitor.Modifications + }; return FileModifications; } public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) From 38c8e73fb383054a9b800c7b43b09a321fe04b4e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:02:27 +0100 Subject: [PATCH 014/203] small clean up of init of class --- .../PowerShell/Refactoring/FunctionVistor.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs index 603dc9037..d5cf5f52d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs @@ -14,7 +14,7 @@ internal class FunctionRename : ICustomAstVisitor2 private readonly string OldName; private readonly string NewName; internal Stack ScopeStack = new(); - internal bool ShouldRename; + internal bool ShouldRename = false; public List Modifications = new(); private readonly List Log = new(); internal int StartLineNumber; @@ -30,7 +30,6 @@ public FunctionRename(string OldName, string NewName, int StartLineNumber, int S this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.ShouldRename = false; Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -39,7 +38,7 @@ public FunctionRename(string OldName, string NewName, int StartLineNumber, int S { TargetFunctionAst = FuncDef; } - if (Node is CommandAst CommDef) + if (Node is CommandAst) { TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) @@ -326,14 +325,15 @@ public object VisitCommand(CommandAst ast) public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => null; public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => null; public object VisitExitStatement(ExitStatementAst exitStatementAst) => null; - public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst){ + public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { foreach (ExpressionAst element in expandableStringExpressionAst.NestedExpressions) { element.Visit(this); } return null; - } + } public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => null; public object VisitHashtable(HashtableAst hashtableAst) => null; public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => null; @@ -346,7 +346,8 @@ public object VisitExpandableStringExpression(ExpandableStringExpressionAst expa public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => null; public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => null; public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; - public object VisitSubExpression(SubExpressionAst subExpressionAst) { + public object VisitSubExpression(SubExpressionAst subExpressionAst) + { subExpressionAst.SubExpression.Visit(this); return null; } From 655bb9646b50827bf37587e7e425f09285b68efe Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:46:12 +0100 Subject: [PATCH 015/203] unneeded init of class var --- .../Services/PowerShell/Refactoring/FunctionVistor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs index d5cf5f52d..fc73034b0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs @@ -14,7 +14,7 @@ internal class FunctionRename : ICustomAstVisitor2 private readonly string OldName; private readonly string NewName; internal Stack ScopeStack = new(); - internal bool ShouldRename = false; + internal bool ShouldRename; public List Modifications = new(); private readonly List Log = new(); internal int StartLineNumber; @@ -219,6 +219,7 @@ public object VisitPipeline(PipelineAst ast) public object VisitAssignmentStatement(AssignmentStatementAst ast) { ast.Right.Visit(this); + ast.Left.Visit(this); return null; } public object VisitStatementBlock(StatementBlockAst ast) From 4e32529f5efdfbaa44fb8467aa84734a28be8774 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:29:43 +0100 Subject: [PATCH 016/203] init commit of variable visitor class --- .../PowerShell/Refactoring/VariableVisitor.cs | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs new file mode 100644 index 000000000..ccc26fb93 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; +using System; + +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + internal class VariableRename : ICustomAstVisitor2 + { + private readonly string OldName; + private readonly string NewName; + internal Stack ScopeStack = new(); + internal bool ShouldRename; + public List Modifications = new(); + internal int StartLineNumber; + internal int StartColumnNumber; + internal VariableExpressionAst TargetVariableAst; + internal VariableExpressionAst DuplicateVariableAst; + internal readonly Ast ScriptAst; + + public VariableRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + this.OldName = OldName.Replace("$", ""); + this.NewName = NewName; + this.StartLineNumber = StartLineNumber; + this.StartColumnNumber = StartColumnNumber; + this.ScriptAst = ScriptAst; + + VariableExpressionAst Node = VariableRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + if (Node != null) + { + TargetVariableAst = Node; + } + } + + public static VariableExpressionAst GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + VariableExpressionAst result = null; + // Looking for a function + result = (VariableExpressionAst)ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is VariableExpressionAst VarDef && + VarDef.VariablePath.UserPath.ToLower() == OldName.ToLower(); + }, true); + return result; + } + public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); + public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); + public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + assignmentStatementAst.Left.Visit(this); + assignmentStatementAst.Right.Visit(this); + return null; + } + public object VisitAttribute(AttributeAst attributeAst) => throw new NotImplementedException(); + public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); + public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); + public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => throw new NotImplementedException(); + public object VisitBlockStatement(BlockStatementAst blockStatementAst) => throw new NotImplementedException(); + public object VisitBreakStatement(BreakStatementAst breakStatementAst) => throw new NotImplementedException(); + public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); + public object VisitCommand(CommandAst commandAst) + { + foreach (CommandElementAst element in commandAst.CommandElements) + { + element.Visit(this); + } + + return null; + } + public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) + { + commandExpressionAst.Expression.Visit(this); + return null; + } + public object VisitCommandParameter(CommandParameterAst commandParameterAst) => throw new NotImplementedException(); + public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); + public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; + public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); + public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => throw new NotImplementedException(); + public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); + public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => throw new NotImplementedException(); + public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => throw new NotImplementedException(); + public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => throw new NotImplementedException(); + public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => throw new NotImplementedException(); + public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => throw new NotImplementedException(); + public object VisitExitStatement(ExitStatementAst exitStatementAst) => throw new NotImplementedException(); + public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { + + foreach (ExpressionAst element in expandableStringExpressionAst.NestedExpressions) + { + element.Visit(this); + } + return null; + } + public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => throw new NotImplementedException(); + public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) + { + forEachStatementAst.Body.Visit(this); + return null; + } + public object VisitForStatement(ForStatementAst forStatementAst) + { + forStatementAst.Body.Visit(this); + return null; + } + public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => throw new NotImplementedException(); + public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); + public object VisitHashtable(HashtableAst hashtableAst) => throw new NotImplementedException(); + public object VisitIfStatement(IfStatementAst ifStmtAst) + { + foreach (Tuple element in ifStmtAst.Clauses) + { + element.Item1.Visit(this); + element.Item2.Visit(this); + } + + ifStmtAst.ElseClause?.Visit(this); + + return null; + } + public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => throw new NotImplementedException(); + public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); + public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => throw new NotImplementedException(); + public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => throw new NotImplementedException(); + public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => throw new NotImplementedException(); + public object VisitNamedBlock(NamedBlockAst namedBlockAst) + { + foreach (StatementAst element in namedBlockAst.Statements) + { + element.Visit(this); + } + return null; + } + public object VisitParamBlock(ParamBlockAst paramBlockAst) => throw new NotImplementedException(); + public object VisitParameter(ParameterAst parameterAst) => throw new NotImplementedException(); + public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => throw new NotImplementedException(); + public object VisitPipeline(PipelineAst pipelineAst) + { + foreach (Ast element in pipelineAst.PipelineElements) + { + element.Visit(this); + } + return null; + } + public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => throw new NotImplementedException(); + public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => throw new NotImplementedException(); + public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) + { + ScopeStack.Push("scriptblock"); + + scriptBlockAst.BeginBlock?.Visit(this); + scriptBlockAst.ProcessBlock?.Visit(this); + scriptBlockAst.EndBlock?.Visit(this); + scriptBlockAst.DynamicParamBlock?.Visit(this); + + ScopeStack.Pop(); + + return null; + } + public object VisitLoopStatement(LoopStatementAst loopAst) + { + + ScopeStack.Push("Loop"); + + loopAst.Body.Visit(this); + + ScopeStack.Pop(); + return null; + } + public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) => throw new NotImplementedException(); + public object VisitStatementBlock(StatementBlockAst statementBlockAst) + { + foreach (StatementAst element in statementBlockAst.Statements) + { + element.Visit(this); + } + + return null; + } + public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; + public object VisitSubExpression(SubExpressionAst subExpressionAst) + { + subExpressionAst.SubExpression.Visit(this); + return null; + } + public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => throw new NotImplementedException(); + public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => throw new NotImplementedException(); + public object VisitTrap(TrapStatementAst trapStatementAst) => throw new NotImplementedException(); + public object VisitTryStatement(TryStatementAst tryStatementAst) => throw new NotImplementedException(); + public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => throw new NotImplementedException(); + public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => throw new NotImplementedException(); + public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => throw new NotImplementedException(); + public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => throw new NotImplementedException(); + public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => throw new NotImplementedException(); + public object VisitUsingStatement(UsingStatementAst usingStatement) => throw new NotImplementedException(); + public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (variableExpressionAst.VariablePath.UserPath == OldName) + { + if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && + variableExpressionAst.Extent.StartLineNumber == StartLineNumber) + { + ShouldRename = true; + TargetVariableAst = variableExpressionAst; + } + else if (variableExpressionAst.Parent is AssignmentStatementAst) + { + DuplicateVariableAst = variableExpressionAst; + ShouldRename = false; + } + + if (ShouldRename) + { + // have some modifications to account for the dollar sign prefix powershell uses for variables + TextChange Change = new() + { + NewText = NewName.Contains("$") ? NewName : "$" + NewName, + StartLine = variableExpressionAst.Extent.StartLineNumber - 1, + StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, + EndLine = variableExpressionAst.Extent.StartLineNumber - 1, + EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, + }; + + Modifications.Add(Change); + } + } + return null; + } + public object VisitWhileStatement(WhileStatementAst whileStatementAst) => throw new NotImplementedException(); + } +} From 4eb9b78dbabd5dbe6252747ef145949ea27377ce Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:29:54 +0100 Subject: [PATCH 017/203] piping for vscode rename symbol --- .../PowerShell/Handlers/RenameSymbol.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 5ef611f96..ad4365fac 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -73,7 +73,7 @@ public RenameSymbolHandler(ILoggerFactory loggerFactory,WorkspaceService workspa _logger = loggerFactory.CreateLogger(); _workspaceService = workspaceService; } - internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) + internal static ModifiedFileResponse RenameFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) { if (symbol.Type is not SymbolType.Function) { @@ -92,6 +92,25 @@ internal static ModifiedFileResponse RefactorFunction(SymbolReference symbol, As }; return FileModifications; } + internal static ModifiedFileResponse RenameVariable(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) + { + if (symbol.Type is not SymbolType.Variable) + { + return null; + } + + VariableRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptAst); + scriptAst.Visit(visitor); + ModifiedFileResponse FileModifications = new(request.FileName) + { + Changes = visitor.Modifications + }; + return FileModifications; + } public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) @@ -120,7 +139,9 @@ public async Task Handle(RenameSymbolParams request, Cancell ModifiedFileResponse FileModifications = null; if (symbol.Type is SymbolType.Function) { - FileModifications = RefactorFunction(symbol, scriptFile.ScriptAst, request); + FileModifications = RenameFunction(symbol, scriptFile.ScriptAst, request); + }else if(symbol.Type is SymbolType.Variable){ + FileModifications = RenameVarible(symbol, scriptFile.ScriptAst, request); } RenameSymbolResult result = new(); From 1a3fe4e6e92b345448430bac3de8f2335e7fe10f Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:30:07 +0100 Subject: [PATCH 018/203] initial tests for variable visitor class --- .../Refactoring/Variables.ps1 | 32 +++++++ .../Variables/RefactorsVariablesData.cs | 53 +++++++++++ .../Variables/SimpleVariableAssignment.ps1 | 2 + .../SimpleVariableAssignmentRenamed.ps1 | 2 + .../Refactoring/Variables/VariableInLoop.ps1 | 4 + .../Variables/VariableInLoopRenamed.ps1 | 4 + .../Variables/VariableInPipeline.ps1 | 3 + .../Variables/VariableInPipelineRenamed.ps1 | 3 + .../Variables/VariableInScriptblock.ps1 | 3 + .../VariableInScriptblockRenamed.ps1 | 3 + .../Variables/VariableNestedScopeFunction.ps1 | 7 ++ .../VariableNestedScopeFunctionRenamed.ps1 | 7 ++ .../Variables/VariableRedefinition.ps1 | 3 + .../Refactoring/RefactorVariableTests.cs | 88 +++++++++++++++++++ 14 files changed, 214 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoop.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoopRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinition.ps1 create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 new file mode 100644 index 000000000..7e308de45 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 @@ -0,0 +1,32 @@ +# Not same +$var = 10 +0..10 | Select-Object @{n='SomeProperty';e={ $var = 30 * $_; $var }} + +# Not same +$var = 10 +Get-ChildItem | Rename-Item -NewName { $var = $_.FullName + (Get-Random); $var } + +# Same +$var = 10 +0..10 | ForEach-Object { + $var += 5 +} + +# Not same +$var = 10 +. (Get-Module Pester) { $var = 30 } + +# Same +$var = 10 +$sb = { $var = 30 } +. $sb + +# ??? +$var = 10 +$sb = { $var = 30 } +$shouldDotSource = Get-Random -Minimum 0 -Maximum 2 +if ($shouldDotSource) { + . $sb +} else { + & $sb +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs new file mode 100644 index 000000000..de38cedf3 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Microsoft.PowerShell.EditorServices.Handlers; + +namespace PowerShellEditorServices.Test.Shared.Refactoring.Variables +{ + internal static class RenameVariableData + { + + public static readonly RenameSymbolParams SimpleVariableAssignment = new() + { + FileName = "SimpleVariableAssignment.ps1", + Column = 1, + Line = 1, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableRedefinition = new() + { + FileName = "VariableRedefinition.ps1", + Column = 1, + Line = 1, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableNestedScopeFunction = new() + { + FileName = "VariableNestedScopeFunction.ps1", + Column = 1, + Line = 1, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableInLoop = new() + { + FileName = "VariableInLoop.ps1", + Column = 1, + Line = 1, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableInPipeline = new() + { + FileName = "VariableInPipeline.ps1", + Column = 23, + Line = 2, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableInScriptblock = new() + { + FileName = "VariableInScriptblock.ps1", + Column = 23, + Line = 2, + RenameTo = "Renamed" + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 new file mode 100644 index 000000000..6097d4154 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 @@ -0,0 +1,2 @@ +$var = 10 +Write-Output $var diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 new file mode 100644 index 000000000..3962ce503 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 @@ -0,0 +1,2 @@ +$Renamed = 10 +Write-Output $Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoop.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoop.ps1 new file mode 100644 index 000000000..bf5af6be8 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoop.ps1 @@ -0,0 +1,4 @@ +$var = 10 +for ($i = 0; $i -lt $var; $i++) { + Write-Output "Count: $i" +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoopRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoopRenamed.ps1 new file mode 100644 index 000000000..cfc98f0d5 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInLoopRenamed.ps1 @@ -0,0 +1,4 @@ +$Renamed = 10 +for ($i = 0; $i -lt $Renamed; $i++) { + Write-Output "Count: $i" +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 new file mode 100644 index 000000000..036a9b108 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 @@ -0,0 +1,3 @@ +1..10 | +Where-Object { $_ -le $oldVarName } | +Write-Output diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 new file mode 100644 index 000000000..34af48896 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 @@ -0,0 +1,3 @@ +1..10 | +Where-Object { $_ -le $Renamed } | +Write-Output diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 new file mode 100644 index 000000000..9c6609aa2 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 @@ -0,0 +1,3 @@ +$var = "Hello" +$action = { Write-Output $var } +&$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 new file mode 100644 index 000000000..5dcbd9a67 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 @@ -0,0 +1,3 @@ +$Renamed = "Hello" +$action = { Write-Output $Renamed } +&$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunction.ps1 new file mode 100644 index 000000000..3c6c22651 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunction.ps1 @@ -0,0 +1,7 @@ +$var = 10 +function TestFunction { + $var = 20 + Write-Output $var +} +TestFunction +Write-Output $var diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRenamed.ps1 new file mode 100644 index 000000000..3886cf867 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRenamed.ps1 @@ -0,0 +1,7 @@ +$Renamed = 10 +function TestFunction { + $var = 20 + Write-Output $var +} +TestFunction +Write-Output $Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinition.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinition.ps1 new file mode 100644 index 000000000..1063dc887 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinition.ps1 @@ -0,0 +1,3 @@ +$var = 10 +$var = 20 +Write-Output $var diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs new file mode 100644 index 000000000..af0f5d7bc --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using PowerShellEditorServices.Test.Shared.Refactoring.Variables; +using Microsoft.PowerShell.EditorServices.Refactoring; + +namespace PowerShellEditorServices.Test.Refactoring +{ + [Trait("Category", "RenameVariables")] + public class RefactorVariableTests : IDisposable + + { + private readonly PsesInternalHost psesHost; + private readonly WorkspaceService workspace; + public void Dispose() + { +#pragma warning disable VSTHRD002 + psesHost.StopAsync().Wait(); +#pragma warning restore VSTHRD002 + GC.SuppressFinalize(this); + } + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Variables", fileName))); + + internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) + { + + string[] Lines = OriginalScript.Split( + new string[] { Environment.NewLine }, + StringSplitOptions.None); + + foreach (TextChange change in Modification.Changes) + { + string TargetLine = Lines[change.StartLine]; + string begin = TargetLine.Substring(0, change.StartColumn); + string end = TargetLine.Substring(change.EndColumn); + Lines[change.StartLine] = begin + change.NewText + end; + } + + return string.Join(Environment.NewLine, Lines); + } + + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request, SymbolReference symbol) + { + + VariableRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); + scriptFile.ScriptAst.Visit(visitor); + ModifiedFileResponse changes = new(request.FileName) + { + Changes = visitor.Modifications + }; + return GetModifiedScript(scriptFile.Contents, changes); + } + public RefactorVariableTests() + { + psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); + } + [Fact] + public void RefactorFunctionSingle() + { + RenameSymbolParams request = RenameVariableData.SimpleVariableAssignment; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + } +} From 3e6da819e693fc32014442d0f9527a5e44b815e0 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:28:32 +0100 Subject: [PATCH 019/203] fixing typo --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index ad4365fac..b8fef05a6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -141,7 +141,7 @@ public async Task Handle(RenameSymbolParams request, Cancell { FileModifications = RenameFunction(symbol, scriptFile.ScriptAst, request); }else if(symbol.Type is SymbolType.Variable){ - FileModifications = RenameVarible(symbol, scriptFile.ScriptAst, request); + FileModifications = RenameVariable(symbol, scriptFile.ScriptAst, request); } RenameSymbolResult result = new(); From d57822d33b9801b10b7f99f573353b521bf1a91a Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:28:57 +0100 Subject: [PATCH 020/203] adjusting scopestack to store Ast objects --- .../PowerShell/Refactoring/VariableVisitor.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index ccc26fb93..d98fa1a4e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -12,13 +12,14 @@ internal class VariableRename : ICustomAstVisitor2 { private readonly string OldName; private readonly string NewName; - internal Stack ScopeStack = new(); + internal Stack ScopeStack = new(); internal bool ShouldRename; public List Modifications = new(); internal int StartLineNumber; internal int StartColumnNumber; internal VariableExpressionAst TargetVariableAst; internal VariableExpressionAst DuplicateVariableAst; + internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; public VariableRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) @@ -66,6 +67,17 @@ public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatemen public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); public object VisitCommand(CommandAst commandAst) { + + // Check for dot sourcing + // TODO Handle the dot sourcing after detection + if (commandAst.InvocationOperator == TokenKind.Dot && commandAst.CommandElements.Count > 1) + { + if (commandAst.CommandElements[1] is StringConstantExpressionAst scriptPath) + { + dotSourcedScripts.Add(scriptPath.Value); + } + } + foreach (CommandElementAst element in commandAst.CommandElements) { element.Visit(this); @@ -102,15 +114,27 @@ public object VisitExpandableStringExpression(ExpandableStringExpressionAst expa public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => throw new NotImplementedException(); public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) { + ScopeStack.Push(forEachStatementAst); forEachStatementAst.Body.Visit(this); + ScopeStack.Pop(); return null; } public object VisitForStatement(ForStatementAst forStatementAst) { + forStatementAst.Condition.Visit(this); + ScopeStack.Push(forStatementAst); forStatementAst.Body.Visit(this); + ScopeStack.Pop(); + return null; + } + public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { + ScopeStack.Push(functionDefinitionAst); + + functionDefinitionAst.Body.Visit(this); + + ScopeStack.Pop(); return null; } - public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) => throw new NotImplementedException(); public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); public object VisitHashtable(HashtableAst hashtableAst) => throw new NotImplementedException(); public object VisitIfStatement(IfStatementAst ifStmtAst) @@ -153,7 +177,7 @@ public object VisitPipeline(PipelineAst pipelineAst) public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => throw new NotImplementedException(); public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) { - ScopeStack.Push("scriptblock"); + ScopeStack.Push(scriptBlockAst); scriptBlockAst.BeginBlock?.Visit(this); scriptBlockAst.ProcessBlock?.Visit(this); @@ -167,7 +191,7 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) public object VisitLoopStatement(LoopStatementAst loopAst) { - ScopeStack.Push("Loop"); + ScopeStack.Push(loopAst); loopAst.Body.Visit(this); From e5d0875223971993f39feec3a8721d790076d19c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:26:15 +0100 Subject: [PATCH 021/203] added additional tests for variablerename visitor --- .../Variables/RefactorsVariablesData.cs | 10 ++- .../Variables/VariableInScriptblockScoped.ps1 | 3 + .../VariableInScriptblockScopedRenamed.ps1 | 3 + .../Refactoring/{ => Variables}/Variables.ps1 | 0 .../Refactoring/RefactorVariableTests.cs | 61 ++++++++++++++++++- 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Variables}/Variables.ps1 (100%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index de38cedf3..b005d1c10 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -45,7 +45,15 @@ internal static class RenameVariableData public static readonly RenameSymbolParams VariableInScriptblock = new() { FileName = "VariableInScriptblock.ps1", - Column = 23, + Column = 26, + Line = 2, + RenameTo = "Renamed" + }; + + public static readonly RenameSymbolParams VariableInScriptblockScoped = new() + { + FileName = "VariableInScriptblockScoped.ps1", + Column = 36, Line = 2, RenameTo = "Renamed" }; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 new file mode 100644 index 000000000..76439a890 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 @@ -0,0 +1,3 @@ +$var = "Hello" +$action = { $var="No";Write-Output $var } +&$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 new file mode 100644 index 000000000..54e1d31e4 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 @@ -0,0 +1,3 @@ +$var = "Hello" +$action = { $Renamed="No";Write-Output $Renamed } +&$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index af0f5d7bc..1b964a076 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -34,7 +34,10 @@ public void Dispose() internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { - + Modification.Changes.Sort((a,b) =>{ + return b.EndColumn + b.EndLine - + a.EndColumn + a.EndLine; + }); string[] Lines = OriginalScript.Split( new string[] { Environment.NewLine }, StringSplitOptions.None); @@ -84,5 +87,61 @@ public void RefactorFunctionSingle() Assert.Equal(expectedContent.Contents, modifiedcontent); } + [Fact] + public void RefactorVariableNestedScopeFunction() + { + RenameSymbolParams request = RenameVariableData.VariableNestedScopeFunction; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void RefactorVariableInPipeline() + { + RenameSymbolParams request = RenameVariableData.VariableInPipeline; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void RefactorVariableInScriptBlock() + { + RenameSymbolParams request = RenameVariableData.VariableInScriptblock; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void RefactorVariableInScriptBlockScoped() + { + RenameSymbolParams request = RenameVariableData.VariableInScriptblockScoped; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } } } From 9ea0f0a0b05878fcde068f63716637513722841f Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:27:04 +0100 Subject: [PATCH 022/203] adjusting so it finds the first variable definition within scope --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index d98fa1a4e..db4573f73 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -30,10 +30,13 @@ public VariableRename(string OldName, string NewName, int StartLineNumber, int S this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - VariableExpressionAst Node = VariableRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + VariableExpressionAst Node = VariableRename.GetVariableTopAssignment(this.OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) { + TargetVariableAst = Node; + this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; + this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; } } From dad669d2cbbf0430c3f6d94ea1d244e638b25890 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:27:42 +0100 Subject: [PATCH 023/203] added function to get variable top assignment --- .../PowerShell/Refactoring/VariableVisitor.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index db4573f73..27e55b50d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -53,6 +53,67 @@ ast is VariableExpressionAst VarDef && }, true); return result; } + public static VariableExpressionAst GetVariableTopAssignment(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + static Ast GetAstParentScope(Ast node) + { + Ast parent = node.Parent; + // Walk backwards up the tree look + while (parent != null) + { + if (parent is ScriptBlockAst) + { + break; + } + parent = parent.Parent; + } + return parent; + } + + // Look up the target object + VariableExpressionAst node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + + Ast TargetParent = GetAstParentScope(node); + + List VariableAssignments = ScriptAst.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst && + VarDef.VariablePath.UserPath.ToLower() == OldName.ToLower() && + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the def if we only have one match + if (VariableAssignments.Count == 1) + { + return VariableAssignments[0]; + } + if (VariableAssignments.Count == 0) + { + return node; + } + VariableExpressionAst CorrectDefinition = null; + for (int i = VariableAssignments.Count - 1; i >= 0; i--) + { + VariableExpressionAst element = VariableAssignments[i]; + + Ast parent = GetAstParentScope(element); + + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetParent == parent) + { + CorrectDefinition = element; + } + } + return CorrectDefinition; + } public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) From deeacdb6b3e558eee73e9b2ef822579b323042b7 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:28:15 +0100 Subject: [PATCH 024/203] renamed scriptfile to scriptast --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 27e55b50d..4bb674589 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -40,11 +40,11 @@ public VariableRename(string OldName, string NewName, int StartLineNumber, int S } } - public static VariableExpressionAst GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + public static VariableExpressionAst GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { VariableExpressionAst result = null; // Looking for a function - result = (VariableExpressionAst)ScriptFile.Find(ast => + result = (VariableExpressionAst)ScriptAst.Find(ast => { return ast.Extent.StartLineNumber == StartLineNumber && ast.Extent.StartColumnNumber == StartColumnNumber && From f5bb1d59789d4afb31fe1359e29dac3fb90ed28d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:28:32 +0100 Subject: [PATCH 025/203] can visit binary expressions now --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 4bb674589..1e7c8ce4d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -125,7 +125,13 @@ public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatemen public object VisitAttribute(AttributeAst attributeAst) => throw new NotImplementedException(); public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => throw new NotImplementedException(); + public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) + { + binaryExpressionAst.Left.Visit(this); + binaryExpressionAst.Right.Visit(this); + + return null; + } public object VisitBlockStatement(BlockStatementAst blockStatementAst) => throw new NotImplementedException(); public object VisitBreakStatement(BreakStatementAst breakStatementAst) => throw new NotImplementedException(); public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); From a266d61a147dc203949312ad734c13028197de5b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:28:50 +0100 Subject: [PATCH 026/203] can visit dountil and dowhile --- .../PowerShell/Refactoring/VariableVisitor.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 1e7c8ce4d..acfd69e9b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -166,8 +166,22 @@ public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => throw new NotImplementedException(); public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); - public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => throw new NotImplementedException(); - public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => throw new NotImplementedException(); + public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) + { + doUntilStatementAst.Condition.Visit(this); + ScopeStack.Push(doUntilStatementAst); + doUntilStatementAst.Body.Visit(this); + ScopeStack.Pop(); + return null; + } + public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) + { + doWhileStatementAst.Condition.Visit(this); + ScopeStack.Push(doWhileStatementAst); + doWhileStatementAst.Body.Visit(this); + ScopeStack.Pop(); + return null; + } public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => throw new NotImplementedException(); public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => throw new NotImplementedException(); public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => throw new NotImplementedException(); From e95e56d69516d8ffa803d0b5b535ed4794f93177 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:29:14 +0100 Subject: [PATCH 027/203] logic to stop start shouldrename --- .../PowerShell/Refactoring/VariableVisitor.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index acfd69e9b..aa0c68ad5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -268,6 +268,22 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) scriptBlockAst.EndBlock?.Visit(this); scriptBlockAst.DynamicParamBlock?.Visit(this); + if (ShouldRename && TargetVariableAst.Parent.Parent == scriptBlockAst) + { + ShouldRename = false; + } + + if (DuplicateVariableAst?.Parent.Parent.Parent == scriptBlockAst) + { + ShouldRename = true; + DuplicateVariableAst = null; + } + + if (TargetVariableAst?.Parent.Parent == scriptBlockAst) + { + ShouldRename = true; + } + ScopeStack.Pop(); return null; From 7580d6734b710e7535c67e87c79d74bc8c6a4117 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:29:34 +0100 Subject: [PATCH 028/203] can visit scriptexpressions now --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index aa0c68ad5..732074521 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -298,7 +298,11 @@ public object VisitLoopStatement(LoopStatementAst loopAst) ScopeStack.Pop(); return null; } - public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) => throw new NotImplementedException(); + public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + scriptBlockExpressionAst.ScriptBlock.Visit(this); + return null; + } public object VisitStatementBlock(StatementBlockAst statementBlockAst) { foreach (StatementAst element in statementBlockAst.Statements) From 1b0dc315d3216ff8649d12e47aae844f3c3e1e8d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:29:58 +0100 Subject: [PATCH 029/203] start stop logic for if a redefinition is found --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 732074521..7190239f7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -310,6 +310,11 @@ public object VisitStatementBlock(StatementBlockAst statementBlockAst) element.Visit(this); } + if (DuplicateVariableAst?.Parent == statementBlockAst) + { + ShouldRename = true; + } + return null; } public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; From 9f3cac88dcdc9dc9ca112e6ae0e7d71188f0cc33 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:30:06 +0100 Subject: [PATCH 030/203] formatting --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 7190239f7..c3724e3b0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -5,6 +5,7 @@ using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Handlers; using System; +using System.Linq; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -211,7 +212,8 @@ public object VisitForStatement(ForStatementAst forStatementAst) ScopeStack.Pop(); return null; } - public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { + public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { ScopeStack.Push(functionDefinitionAst); functionDefinitionAst.Body.Visit(this); From 47d171661872f1f63409937e183bcf9ba9810d99 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:05:41 +0100 Subject: [PATCH 031/203] implemented visithastable --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index c3724e3b0..7632792b4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -222,7 +222,15 @@ public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAs return null; } public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); - public object VisitHashtable(HashtableAst hashtableAst) => throw new NotImplementedException(); + public object VisitHashtable(HashtableAst hashtableAst) + { + foreach (Tuple element in hashtableAst.KeyValuePairs) + { + element.Item1.Visit(this); + element.Item2.Visit(this); + } + return null; + } public object VisitIfStatement(IfStatementAst ifStmtAst) { foreach (Tuple element in ifStmtAst.Clauses) From b4cc8578c33612590ca331ababf2f0c9df877c6e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:06:10 +0100 Subject: [PATCH 032/203] function to determine if a node is within a targets scope --- .../PowerShell/Refactoring/VariableVisitor.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 7632792b4..1c9ded81e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -71,6 +71,25 @@ static Ast GetAstParentScope(Ast node) return parent; } + static bool WithinTargetsScope(Ast Target ,Ast Child){ + bool r = false; + Ast childParent = Child.Parent; + Ast TargetScope = GetAstParentScope(Target); + while (childParent != null) + { + if (childParent == TargetScope) + { + break; + } + childParent = childParent.Parent; + } + if (childParent == TargetScope) + { + r = true; + } + return r; + } + // Look up the target object VariableExpressionAst node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); From dc9af0d58c98fb85bb2aa49c1bd322a5afd2ee6b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:06:44 +0100 Subject: [PATCH 033/203] adjusted get variable top assignment for better detection --- .../PowerShell/Refactoring/VariableVisitor.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 1c9ded81e..db3a40ed0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -104,11 +104,7 @@ VarDef.Parent is AssignmentStatementAst && (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); }, true).Cast().ToList(); - // return the def if we only have one match - if (VariableAssignments.Count == 1) - { - return VariableAssignments[0]; - } + // return the def if we have no matches if (VariableAssignments.Count == 0) { return node; @@ -119,20 +115,34 @@ VarDef.Parent is AssignmentStatementAst && VariableExpressionAst element = VariableAssignments[i]; Ast parent = GetAstParentScope(element); - - // we have hit the global scope of the script file - if (null == parent) + // closest assignment statement is within the scope of the node + if (TargetParent == parent) { CorrectDefinition = element; + } + else if (node.Parent is AssignmentStatementAst) + { + // the node is probably the first assignment statement within the scope + CorrectDefinition = node; break; } - - if (TargetParent == parent) + // node is proably just a reference of an assignment statement within the global scope or higher + if (node.Parent is not AssignmentStatementAst) { - CorrectDefinition = element; + if (null == parent || null == parent.Parent) + { + // we have hit the global scope of the script file + CorrectDefinition = element; + break; + } + if (WithinTargetsScope(element,node)) + { + CorrectDefinition=element; + } } } - return CorrectDefinition; + + return CorrectDefinition ?? node; } public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); From 72eb38b2cce424430724406ec19ea777b0541bc1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:06:57 +0100 Subject: [PATCH 034/203] additional test cases --- .../Variables/RefactorsVariablesData.cs | 15 +++++++++++++++ .../VariableNestedFunctionScriptblock.ps1 | 9 +++++++++ ...VariableNestedFunctionScriptblockRenamed.ps1 | 9 +++++++++ .../VariablewWithinHastableExpression.ps1 | 3 +++ ...VariablewWithinHastableExpressionRenamed.ps1 | 3 +++ .../Refactoring/RefactorVariableTests.cs | 17 +++++++++++++++-- 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index b005d1c10..8047ed05d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -57,5 +57,20 @@ internal static class RenameVariableData Line = 2, RenameTo = "Renamed" }; + + public static readonly RenameSymbolParams VariablewWithinHastableExpression = new() + { + FileName = "VariablewWithinHastableExpression.ps1", + Column = 46, + Line = 3, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableNestedFunctionScriptblock = new() + { + FileName = "VariableNestedFunctionScriptblock.ps1", + Column = 20, + Line = 4, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 new file mode 100644 index 000000000..393b2bdfd --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 @@ -0,0 +1,9 @@ +function Sample{ + $var = "Hello" + $sb = { + write-host $var + } + & $sb + $var +} +Sample diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 new file mode 100644 index 000000000..70a51b6b6 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 @@ -0,0 +1,9 @@ +function Sample{ + $Renamed = "Hello" + $sb = { + write-host $Renamed + } + & $sb + $Renamed +} +Sample diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 new file mode 100644 index 000000000..cb3f58b1c --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 @@ -0,0 +1,3 @@ +# Not same +$var = 10 +0..10 | Select-Object @{n='SomeProperty';e={ $var = 30 * $_; $var }} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 new file mode 100644 index 000000000..0ee85fa2d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 @@ -0,0 +1,3 @@ +# Not same +$var = 10 +0..10 | Select-Object @{n='SomeProperty';e={ $Renamed = 30 * $_; $Renamed }} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 1b964a076..78092a0b0 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -74,7 +74,7 @@ public RefactorVariableTests() workspace = new WorkspaceService(NullLoggerFactory.Instance); } [Fact] - public void RefactorFunctionSingle() + public void RefactorVariableSingle() { RenameSymbolParams request = RenameVariableData.SimpleVariableAssignment; ScriptFile scriptFile = GetTestScript(request.FileName); @@ -132,7 +132,20 @@ public void RefactorVariableInScriptBlock() [Fact] public void RefactorVariableInScriptBlockScoped() { - RenameSymbolParams request = RenameVariableData.VariableInScriptblockScoped; + RenameSymbolParams request = RenameVariableData.VariablewWithinHastableExpression; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void VariableNestedFunctionScriptblock(){ + RenameSymbolParams request = RenameVariableData.VariableNestedFunctionScriptblock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( From d364d40ac99338daa0af21f4bef3c3774c7cc104 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:25:48 +0100 Subject: [PATCH 035/203] additional tests --- .../Variables/RefactorsVariablesData.cs | 14 ++++++++++ .../VariableWithinCommandAstScriptBlock.ps1 | 3 +++ ...ableWithinCommandAstScriptBlockRenamed.ps1 | 3 +++ .../Variables/VariableWithinForeachObject.ps1 | 5 ++++ .../VariableWithinForeachObjectRenamed.ps1 | 5 ++++ .../Refactoring/RefactorVariableTests.cs | 26 +++++++++++++++++++ 6 files changed, 56 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlock.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlockRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObject.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObjectRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 8047ed05d..6bd0f971e 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -72,5 +72,19 @@ internal static class RenameVariableData Line = 4, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableWithinCommandAstScriptBlock = new() + { + FileName = "VariableWithinCommandAstScriptBlock.ps1", + Column = 75, + Line = 3, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableWithinForeachObject = new() + { + FileName = "VariableWithinForeachObject.ps1", + Column = 1, + Line = 2, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlock.ps1 new file mode 100644 index 000000000..4d2f47f74 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlock.ps1 @@ -0,0 +1,3 @@ +# Not same +$var = 10 +Get-ChildItem | Rename-Item -NewName { $var = $_.FullName + (Get-Random); $var } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlockRenamed.ps1 new file mode 100644 index 000000000..56c4b4965 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinCommandAstScriptBlockRenamed.ps1 @@ -0,0 +1,3 @@ +# Not same +$var = 10 +Get-ChildItem | Rename-Item -NewName { $Renamed = $_.FullName + (Get-Random); $Renamed } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObject.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObject.ps1 new file mode 100644 index 000000000..89ab6ca1d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObject.ps1 @@ -0,0 +1,5 @@ +# Same +$var = 10 +0..10 | ForEach-Object { + $var += 5 +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObjectRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObjectRenamed.ps1 new file mode 100644 index 000000000..12f936b61 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinForeachObjectRenamed.ps1 @@ -0,0 +1,5 @@ +# Same +$Renamed = 10 +0..10 | ForEach-Object { + $Renamed += 5 +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 78092a0b0..5ec7dfa87 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -155,6 +155,32 @@ public void VariableNestedFunctionScriptblock(){ Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VariableWithinCommandAstScriptBlock(){ + RenameSymbolParams request = RenameVariableData.VariableWithinCommandAstScriptBlock; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void VariableWithinForeachObject(){ + RenameSymbolParams request = RenameVariableData.VariableWithinForeachObject; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } } } From 1f52e77946f3e428403b37e3be8a0cd0a87771ac Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:26:29 +0100 Subject: [PATCH 036/203] added break for finding the top variable assignment --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index db3a40ed0..8b8711fab 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -119,6 +119,7 @@ VarDef.Parent is AssignmentStatementAst && if (TargetParent == parent) { CorrectDefinition = element; + break; } else if (node.Parent is AssignmentStatementAst) { From 219d01ca9ad1273e8e463a30341a2cdb98927734 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:26:41 +0100 Subject: [PATCH 037/203] implemented visit command parameter --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 8b8711fab..2df6761e6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -191,7 +191,7 @@ public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) commandExpressionAst.Expression.Visit(this); return null; } - public object VisitCommandParameter(CommandParameterAst commandParameterAst) => throw new NotImplementedException(); + public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null; public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); From fe2d359d1ea00129a17103a0187c885fe012f830 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:26:55 +0100 Subject: [PATCH 038/203] implemented visit member expression --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 2df6761e6..db6f469d5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -275,7 +275,10 @@ public object VisitIfStatement(IfStatementAst ifStmtAst) } public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => throw new NotImplementedException(); public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => throw new NotImplementedException(); + public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) { + memberExpressionAst.Expression.Visit(this); + return null; + } public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => throw new NotImplementedException(); public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => throw new NotImplementedException(); public object VisitNamedBlock(NamedBlockAst namedBlockAst) From 6df0fa0d7bd0a4b7acd08a37f9d42a5744fafeaa Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:27:06 +0100 Subject: [PATCH 039/203] implemented visitparentexpression --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index db6f469d5..7fcf4ae66 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -291,7 +291,10 @@ public object VisitNamedBlock(NamedBlockAst namedBlockAst) } public object VisitParamBlock(ParamBlockAst paramBlockAst) => throw new NotImplementedException(); public object VisitParameter(ParameterAst parameterAst) => throw new NotImplementedException(); - public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => throw new NotImplementedException(); + public object VisitParenExpression(ParenExpressionAst parenExpressionAst) { + parenExpressionAst.Pipeline.Visit(this); + return null; + } public object VisitPipeline(PipelineAst pipelineAst) { foreach (Ast element in pipelineAst.PipelineElements) From 8af56bc175029e346e298c7d4e1bb5cebbb1c513 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:27:24 +0100 Subject: [PATCH 040/203] altered logic for variable renamed to check operator is equals --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 7fcf4ae66..819f14359 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -389,8 +389,10 @@ public object VisitVariableExpression(VariableExpressionAst variableExpressionAs ShouldRename = true; TargetVariableAst = variableExpressionAst; } - else if (variableExpressionAst.Parent is AssignmentStatementAst) + else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && + assignment.Operator == TokenKind.Equals) { + DuplicateVariableAst = variableExpressionAst; ShouldRename = false; } From fa563a64bf1c7001d17d021db2477263e46c730d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:31:03 +0100 Subject: [PATCH 041/203] removing examples file --- .../Refactoring/Variables/Variables.ps1 | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 deleted file mode 100644 index 7e308de45..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/Variables.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -# Not same -$var = 10 -0..10 | Select-Object @{n='SomeProperty';e={ $var = 30 * $_; $var }} - -# Not same -$var = 10 -Get-ChildItem | Rename-Item -NewName { $var = $_.FullName + (Get-Random); $var } - -# Same -$var = 10 -0..10 | ForEach-Object { - $var += 5 -} - -# Not same -$var = 10 -. (Get-Module Pester) { $var = 30 } - -# Same -$var = 10 -$sb = { $var = 30 } -. $sb - -# ??? -$var = 10 -$sb = { $var = 30 } -$shouldDotSource = Get-Random -Minimum 0 -Maximum 2 -if ($shouldDotSource) { - . $sb -} else { - & $sb -} From 160f8ba3fb2f71a0baff2929b24f06cb049f71f7 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:22:30 +0300 Subject: [PATCH 042/203] formatting and additional test --- .../Variables/VariableusedInWhileLoop.ps1 | 15 ++++++++++++++ .../VariableusedInWhileLoopRenamed.ps1 | 15 ++++++++++++++ .../Refactoring/RefactorVariableTests.cs | 20 +++++++++++++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoop.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoopRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoop.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoop.ps1 new file mode 100644 index 000000000..6ef6e2652 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoop.ps1 @@ -0,0 +1,15 @@ +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliseconds + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoopRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoopRenamed.ps1 new file mode 100644 index 000000000..7a5a46479 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableusedInWhileLoopRenamed.ps1 @@ -0,0 +1,15 @@ +function Write-Item($itemCount) { + $Renamed = 1 + + while ($Renamed -le $itemCount) { + $str = "Output $Renamed" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $Renamed = $Renamed + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliseconds + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 5ec7dfa87..62dc3dd21 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -157,7 +157,8 @@ public void VariableNestedFunctionScriptblock(){ } [Fact] - public void VariableWithinCommandAstScriptBlock(){ + public void VariableWithinCommandAstScriptBlock() + { RenameSymbolParams request = RenameVariableData.VariableWithinCommandAstScriptBlock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); @@ -170,7 +171,8 @@ public void VariableWithinCommandAstScriptBlock(){ } [Fact] - public void VariableWithinForeachObject(){ + public void VariableWithinForeachObject() + { RenameSymbolParams request = RenameVariableData.VariableWithinForeachObject; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); @@ -182,5 +184,19 @@ public void VariableWithinForeachObject(){ Assert.Equal(expectedContent.Contents, modifiedcontent); } + [Fact] + public void VariableusedInWhileLoop() + { + RenameSymbolParams request = RenameVariableData.VariableusedInWhileLoop; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } } } From 62350e697dcd73a7dd4fb4e4237502165ff56d69 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:22:56 +0300 Subject: [PATCH 043/203] formatting and proper sorting for gettestscript --- .../Refactoring/RefactorVariableTests.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 62dc3dd21..ebe21a116 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -34,9 +34,14 @@ public void Dispose() internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { - Modification.Changes.Sort((a,b) =>{ - return b.EndColumn + b.EndLine - - a.EndColumn + a.EndLine; + Modification.Changes.Sort((a, b) => + { + if (b.StartLine == a.StartLine) + { + return b.EndColumn - a.EndColumn; + } + return b.StartLine - a.StartLine; + }); string[] Lines = OriginalScript.Split( new string[] { Environment.NewLine }, @@ -144,7 +149,8 @@ public void RefactorVariableInScriptBlockScoped() } [Fact] - public void VariableNestedFunctionScriptblock(){ + public void VariableNestedFunctionScriptblock() + { RenameSymbolParams request = RenameVariableData.VariableNestedFunctionScriptblock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); From 3d85a6482f2851352854a9f54904804413715490 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:23:10 +0300 Subject: [PATCH 044/203] additional test data --- .../Refactoring/Variables/RefactorsVariablesData.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 6bd0f971e..02897d7a7 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -86,5 +86,12 @@ internal static class RenameVariableData Line = 2, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableusedInWhileLoop = new() + { + FileName = "VariableusedInWhileLoop.ps1", + Column = 5, + Line = 2, + RenameTo = "Renamed" + }; } } From 734601a6e9c68b3311c132621f84b8916ae2662e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:43:33 +0300 Subject: [PATCH 045/203] reworked class so that oldname is no longer needed --- .../PowerShell/Handlers/RenameSymbol.cs | 3 +- .../PowerShell/Refactoring/VariableVisitor.cs | 141 ++++++++++++------ 2 files changed, 93 insertions(+), 51 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index b8fef05a6..6195474ae 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -99,8 +99,7 @@ internal static ModifiedFileResponse RenameVariable(SymbolReference symbol, Ast return null; } - VariableRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, + VariableRename visitor = new(request.RenameTo, symbol.ScriptRegion.StartLineNumber, symbol.ScriptRegion.StartColumnNumber, scriptAst); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 819f14359..d3df4e287 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -9,6 +9,24 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { + + public class TargetSymbolNotFoundException : Exception + { + public TargetSymbolNotFoundException() + { + } + + public TargetSymbolNotFoundException(string message) + : base(message) + { + } + + public TargetSymbolNotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } + internal class VariableRename : ICustomAstVisitor2 { private readonly string OldName; @@ -23,72 +41,41 @@ internal class VariableRename : ICustomAstVisitor2 internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; - public VariableRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public VariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { - this.OldName = OldName.Replace("$", ""); this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - VariableExpressionAst Node = VariableRename.GetVariableTopAssignment(this.OldName, StartLineNumber, StartColumnNumber, ScriptAst); + VariableExpressionAst Node = (VariableExpressionAst)VariableRename.GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) { TargetVariableAst = Node; + OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; } } - public static VariableExpressionAst GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { - VariableExpressionAst result = null; - // Looking for a function - result = (VariableExpressionAst)ScriptAst.Find(ast => + Ast result = null; + result = ScriptAst.Find(ast => { return ast.Extent.StartLineNumber == StartLineNumber && ast.Extent.StartColumnNumber == StartColumnNumber && - ast is VariableExpressionAst VarDef && - VarDef.VariablePath.UserPath.ToLower() == OldName.ToLower(); + ast is VariableExpressionAst or CommandParameterAst; }, true); + if (result == null) + { + throw new TargetSymbolNotFoundException(); + } return result; } - public static VariableExpressionAst GetVariableTopAssignment(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { - static Ast GetAstParentScope(Ast node) - { - Ast parent = node.Parent; - // Walk backwards up the tree look - while (parent != null) - { - if (parent is ScriptBlockAst) - { - break; - } - parent = parent.Parent; - } - return parent; - } - - static bool WithinTargetsScope(Ast Target ,Ast Child){ - bool r = false; - Ast childParent = Child.Parent; - Ast TargetScope = GetAstParentScope(Target); - while (childParent != null) - { - if (childParent == TargetScope) - { - break; - } - childParent = childParent.Parent; - } - if (childParent == TargetScope) - { - r = true; - } - return r; - } // Look up the target object VariableExpressionAst node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); @@ -98,8 +85,8 @@ static bool WithinTargetsScope(Ast Target ,Ast Child){ List VariableAssignments = ScriptAst.FindAll(ast => { return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst && - VarDef.VariablePath.UserPath.ToLower() == OldName.ToLower() && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); @@ -109,7 +96,7 @@ VarDef.Parent is AssignmentStatementAst && { return node; } - VariableExpressionAst CorrectDefinition = null; + Ast CorrectDefinition = null; for (int i = VariableAssignments.Count - 1; i >= 0; i--) { VariableExpressionAst element = VariableAssignments[i]; @@ -136,15 +123,71 @@ VarDef.Parent is AssignmentStatementAst && CorrectDefinition = element; break; } - if (WithinTargetsScope(element,node)) + if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst) { - CorrectDefinition=element; + if (node.Parent is CommandAst commDef) + { + if (funcDef.Name == commDef.GetCommandName() + && funcDef.Parent.Parent == TargetParent) + { + CorrectDefinition = element; + break; + } + } + } + if (WithinTargetsScope(element, node)) + { + CorrectDefinition = element; } } - } + + } return CorrectDefinition ?? node; } + + internal static Ast GetAstParentScope(Ast node) + { + Ast parent = node; + // Walk backwards up the tree look + while (parent != null) + { + if (parent is ScriptBlockAst or FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + if (parent is ScriptBlockAst && parent.Parent != null) + { + parent = GetAstParentScope(parent.Parent); + } + return parent; + } + + internal static bool WithinTargetsScope(Ast Target, Ast Child) + { + bool r = false; + Ast childParent = Child.Parent; + Ast TargetScope = GetAstParentScope(Target); + while (childParent != null) + { + if (childParent is FunctionDefinitionAst) + { + break; + } + if (childParent == TargetScope) + { + break; + } + childParent = childParent.Parent; + } + if (childParent == TargetScope) + { + r = true; + } + return r; + } public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) From 35e834b1a2720e264d3d6e7b6aabfdab6cc75b1d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:43:55 +0300 Subject: [PATCH 046/203] implemented some new visitors --- .../PowerShell/Refactoring/VariableVisitor.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index d3df4e287..be05b13c8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -189,14 +189,25 @@ internal static bool WithinTargetsScope(Ast Target, Ast Child) return r; } public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); - public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => throw new NotImplementedException(); + public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) + { + foreach (ExpressionAst element in arrayLiteralAst.Elements) + { + element.Visit(this); + } + return null; + } public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) { assignmentStatementAst.Left.Visit(this); assignmentStatementAst.Right.Visit(this); return null; } - public object VisitAttribute(AttributeAst attributeAst) => throw new NotImplementedException(); + public object VisitAttribute(AttributeAst attributeAst) + { + attributeAst.Visit(this); + return null; + } public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) From ae6a052d57c0cdd2e9477f7fc23352b6a06420cb Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:44:11 +0300 Subject: [PATCH 047/203] early start on commandparameter renaming --- .../PowerShell/Refactoring/VariableVisitor.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index be05b13c8..a42078de9 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -245,7 +245,33 @@ public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) commandExpressionAst.Expression.Visit(this); return null; } - public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null; + public object VisitCommandParameter(CommandParameterAst commandParameterAst) + { + // TODO implement command parameter renaming + if (commandParameterAst.ParameterName == OldName) + { + if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && + commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) + { + ShouldRename = true; + } + + if (ShouldRename) + { + TextChange Change = new() + { + NewText = NewName.Contains("-") ? NewName : "-" + NewName, + StartLine = commandParameterAst.Extent.StartLineNumber - 1, + StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, + EndLine = commandParameterAst.Extent.StartLineNumber - 1, + EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, + }; + + Modifications.Add(Change); + } + } + return null; + } public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); From 9cc8eff1b0a33ee1c8c1926da3260c52f18d71f5 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:44:59 +0300 Subject: [PATCH 048/203] more implementations and some formatting --- .../PowerShell/Refactoring/VariableVisitor.cs | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index a42078de9..dc50c4738 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -275,7 +275,13 @@ public object VisitCommandParameter(CommandParameterAst commandParameterAst) public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); - public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => throw new NotImplementedException(); + public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) + { + // TODO figure out if there is a case to visit the type + //convertExpressionAst.Type.Visit(this); + convertExpressionAst.Child.Visit(this); + return null; + } public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) { @@ -325,7 +331,13 @@ public object VisitForStatement(ForStatementAst forStatementAst) public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { ScopeStack.Push(functionDefinitionAst); - + if (null != functionDefinitionAst.Parameters) + { + foreach (ParameterAst element in functionDefinitionAst.Parameters) + { + element.Visit(this); + } + } functionDefinitionAst.Body.Visit(this); ScopeStack.Pop(); @@ -353,9 +365,14 @@ public object VisitIfStatement(IfStatementAst ifStmtAst) return null; } - public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => throw new NotImplementedException(); + public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) { + indexExpressionAst.Target.Visit(this); + indexExpressionAst.Index.Visit(this); + return null; + } public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) { + public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) + { memberExpressionAst.Expression.Visit(this); return null; } @@ -369,9 +386,25 @@ public object VisitNamedBlock(NamedBlockAst namedBlockAst) } return null; } - public object VisitParamBlock(ParamBlockAst paramBlockAst) => throw new NotImplementedException(); - public object VisitParameter(ParameterAst parameterAst) => throw new NotImplementedException(); - public object VisitParenExpression(ParenExpressionAst parenExpressionAst) { + public object VisitParamBlock(ParamBlockAst paramBlockAst) + { + foreach (ParameterAst element in paramBlockAst.Parameters) + { + element.Visit(this); + } + return null; + } + public object VisitParameter(ParameterAst parameterAst) + { + parameterAst.Name.Visit(this); + foreach (AttributeBaseAst element in parameterAst.Attributes) + { + element.Visit(this); + } + return null; + } + public object VisitParenExpression(ParenExpressionAst parenExpressionAst) + { parenExpressionAst.Pipeline.Visit(this); return null; } @@ -384,7 +417,10 @@ public object VisitPipeline(PipelineAst pipelineAst) return null; } public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => throw new NotImplementedException(); - public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => throw new NotImplementedException(); + public object VisitReturnStatement(ReturnStatementAst returnStatementAst) { + returnStatementAst.Pipeline.Visit(this); + return null; + } public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) { ScopeStack.Push(scriptBlockAst); @@ -472,11 +508,14 @@ public object VisitVariableExpression(VariableExpressionAst variableExpressionAs else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && assignment.Operator == TokenKind.Equals) { - + if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) + { DuplicateVariableAst = variableExpressionAst; ShouldRename = false; } + } + if (ShouldRename) { // have some modifications to account for the dollar sign prefix powershell uses for variables @@ -494,6 +533,12 @@ public object VisitVariableExpression(VariableExpressionAst variableExpressionAs } return null; } - public object VisitWhileStatement(WhileStatementAst whileStatementAst) => throw new NotImplementedException(); + public object VisitWhileStatement(WhileStatementAst whileStatementAst) + { + whileStatementAst.Condition.Visit(this); + whileStatementAst.Body.Visit(this); + + return null; + } } } From 314a665a74548602b0cbe3e1de26c7e0581c3e59 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:46:00 +0300 Subject: [PATCH 049/203] logic to determin if we are renaming a var or parameter --- .../Services/PowerShell/Refactoring/VariableVisitor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index dc50c4738..d439f9948 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -78,7 +78,11 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN { // Look up the target object - VariableExpressionAst node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + Ast node = GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst); + + string name = node is CommandParameterAst commdef + ? commdef.ParameterName + : node is VariableExpressionAst varDef ? varDef.VariablePath.UserPath : throw new TargetSymbolNotFoundException(); Ast TargetParent = GetAstParentScope(node); From b49898e28cec24a941965a46596defbe0f8f3cdc Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:46:23 +0300 Subject: [PATCH 050/203] additional test --- .../PowerShell/Refactoring/VariableVisitor.cs | 6 +- .../Variables/RefactorsVariablesData.cs | 14 +++ .../Variables/VariableCommandParameter.ps1 | 10 ++ .../VariableCommandParameterRenamed.ps1 | 10 ++ .../Refactoring/Variables/VariableInParam.ps1 | 4 + .../Variables/VariableInParamRenamed.ps1 | 4 + .../Refactoring/RefactorVariableTests.cs | 92 ++++++++++--------- 7 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index d439f9948..3941d5f98 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -514,9 +514,9 @@ public object VisitVariableExpression(VariableExpressionAst variableExpressionAs { if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) { - DuplicateVariableAst = variableExpressionAst; - ShouldRename = false; - } + DuplicateVariableAst = variableExpressionAst; + ShouldRename = false; + } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 02897d7a7..8c999f083 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -93,5 +93,19 @@ internal static class RenameVariableData Line = 2, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableInParam = new() + { + FileName = "VariableInParam.ps1", + Column = 16, + Line = 2, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableCommandParameter = new() + { + FileName = "VariableCommandParameter.ps1", + Column = 9, + Line = 10, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 new file mode 100644 index 000000000..18eeb1e03 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 @@ -0,0 +1,10 @@ +function Get-foo { + param ( + [string]$string, + [int]$pos + ) + + return $string[$pos] + +} +Get-foo -string "Hello" -pos -1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 new file mode 100644 index 000000000..e74504a4d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 @@ -0,0 +1,10 @@ +function Get-foo { + param ( + [string]$Renamed, + [int]$pos + ) + + return $Renamed[$pos] + +} +Get-foo -Renamed "Hello" -pos -1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 new file mode 100644 index 000000000..f7eace40f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 @@ -0,0 +1,4 @@ +function Sample($var){ + write-host $var +} +Sample "Hello" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 new file mode 100644 index 000000000..569860a95 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 @@ -0,0 +1,4 @@ +function Sample($Renamed){ + write-host $Renamed +} +Sample "Hello" diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index ebe21a116..9a8c3f8c2 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -11,7 +11,6 @@ using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; -using Microsoft.PowerShell.EditorServices.Services.Symbols; using PowerShellEditorServices.Test.Shared.Refactoring.Variables; using Microsoft.PowerShell.EditorServices.Refactoring; @@ -58,13 +57,12 @@ internal static string GetModifiedScript(string OriginalScript, ModifiedFileResp return string.Join(Environment.NewLine, Lines); } - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request, SymbolReference symbol) + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request) { - VariableRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, + VariableRename visitor = new(request.RenameTo, + request.Line, + request.Column, scriptFile.ScriptAst); scriptFile.ScriptAst.Visit(visitor); ModifiedFileResponse changes = new(request.FileName) @@ -84,10 +82,8 @@ public void RefactorVariableSingle() RenameSymbolParams request = RenameVariableData.SimpleVariableAssignment; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -98,10 +94,8 @@ public void RefactorVariableNestedScopeFunction() RenameSymbolParams request = RenameVariableData.VariableNestedScopeFunction; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -112,10 +106,8 @@ public void RefactorVariableInPipeline() RenameSymbolParams request = RenameVariableData.VariableInPipeline; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -126,10 +118,8 @@ public void RefactorVariableInScriptBlock() RenameSymbolParams request = RenameVariableData.VariableInScriptblock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -140,10 +130,8 @@ public void RefactorVariableInScriptBlockScoped() RenameSymbolParams request = RenameVariableData.VariablewWithinHastableExpression; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -154,38 +142,32 @@ public void VariableNestedFunctionScriptblock() RenameSymbolParams request = RenameVariableData.VariableNestedFunctionScriptblock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] + [Fact] public void VariableWithinCommandAstScriptBlock() { RenameSymbolParams request = RenameVariableData.VariableWithinCommandAstScriptBlock; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] + [Fact] public void VariableWithinForeachObject() { RenameSymbolParams request = RenameVariableData.VariableWithinForeachObject; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); @@ -196,10 +178,32 @@ public void VariableusedInWhileLoop() RenameSymbolParams request = RenameVariableData.VariableusedInWhileLoop; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void VariableInParam() + { + RenameSymbolParams request = RenameVariableData.VariableInParam; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void VariableCommandParameter() + { + RenameSymbolParams request = RenameVariableData.VariableCommandParameter; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); From 92d429a62ecf0b9b77f53f55e1de87b7e3d68238 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:54:04 +0300 Subject: [PATCH 051/203] additional tests for parameters --- .../Variables/RefactorsVariablesData.cs | 16 +++++++++- .../Refactoring/Variables/VariableInParam.ps1 | 30 +++++++++++++++++-- .../Variables/VariableInParamRenamed.ps1 | 30 +++++++++++++++++-- .../VariableScriptWithParamBlock.ps1 | 28 +++++++++++++++++ .../VariableScriptWithParamBlockRenamed.ps1 | 28 +++++++++++++++++ .../Refactoring/RefactorVariableTests.cs | 24 +++++++++++++++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 8c999f083..5a89fe846 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -97,7 +97,7 @@ internal static class RenameVariableData { FileName = "VariableInParam.ps1", Column = 16, - Line = 2, + Line = 24, RenameTo = "Renamed" }; public static readonly RenameSymbolParams VariableCommandParameter = new() @@ -107,5 +107,19 @@ internal static class RenameVariableData Line = 10, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableCommandParameterReverse = new() + { + FileName = "VariableCommandParameter.ps1", + Column = 17, + Line = 3, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableScriptWithParamBlock = new() + { + FileName = "VariableScriptWithParamBlock.ps1", + Column = 28, + Line = 1, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 index f7eace40f..478990bfd 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 @@ -1,4 +1,28 @@ -function Sample($var){ - write-host $var +param([int]$Count=50, [int]$DelayMilliseconds=200) + +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliseconds + } } -Sample "Hello" + +# Do-Work will be underlined in green if you haven't disable script analysis. +# Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" +# doesn't use an approved verb. +function Do-Work($workCount) { + Write-Output "Doing work..." + Write-Item $workcount + Write-Host "Done!" +} + +Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 index 569860a95..2a810e887 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 @@ -1,4 +1,28 @@ -function Sample($Renamed){ - write-host $Renamed +param([int]$Count=50, [int]$DelayMilliseconds=200) + +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliseconds + } } -Sample "Hello" + +# Do-Work will be underlined in green if you haven't disable script analysis. +# Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" +# doesn't use an approved verb. +function Do-Work($Renamed) { + Write-Output "Doing work..." + Write-Item $Renamed + Write-Host "Done!" +} + +Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 new file mode 100644 index 000000000..c3175bd0d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 @@ -0,0 +1,28 @@ +param([int]$Count=50, [int]$DelayMilliSeconds=200) + +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliSeconds + } +} + +# Do-Work will be underlined in green if you haven't disable script analysis. +# Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" +# doesn't use an approved verb. +function Do-Work($workCount) { + Write-Output "Doing work..." + Write-Item $workcount + Write-Host "Done!" +} + +Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 new file mode 100644 index 000000000..4f42f891a --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 @@ -0,0 +1,28 @@ +param([int]$Count=50, [int]$Renamed=200) + +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $Renamed + } +} + +# Do-Work will be underlined in green if you haven't disable script analysis. +# Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" +# doesn't use an approved verb. +function Do-Work($workCount) { + Write-Output "Doing work..." + Write-Item $workcount + Write-Host "Done!" +} + +Do-Work $Count diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 9a8c3f8c2..930657152 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -208,5 +208,29 @@ public void VariableCommandParameter() Assert.Equal(expectedContent.Contents, modifiedcontent); } + [Fact] + public void VariableCommandParameterReverse() + { + RenameSymbolParams request = RenameVariableData.VariableCommandParameterReverse; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } + [Fact] + public void VariableScriptWithParamBlock() + { + RenameSymbolParams request = RenameVariableData.VariableScriptWithParamBlock; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + + } } } From ff09c6f1bcbb88bea96ec328e03eeb33ecc8151a Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:54:18 +0300 Subject: [PATCH 052/203] adjusting checks for parameters --- .../PowerShell/Handlers/RenameSymbol.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 6195474ae..4f8bef809 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -94,14 +94,14 @@ internal static ModifiedFileResponse RenameFunction(SymbolReference symbol, Ast } internal static ModifiedFileResponse RenameVariable(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) { - if (symbol.Type is not SymbolType.Variable) + if (symbol.Type is not (SymbolType.Variable or SymbolType.Parameter)) { return null; } VariableRename visitor = new(request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, + symbol.NameRegion.StartLineNumber, + symbol.NameRegion.StartColumnNumber, scriptAst); scriptAst.Visit(visitor); ModifiedFileResponse FileModifications = new(request.FileName) @@ -132,17 +132,12 @@ public async Task Handle(RenameSymbolParams request, Cancell Ast token = scriptFile.ScriptAst.Find(ast => { - return ast.Extent.StartLineNumber == symbol.ScriptRegion.StartLineNumber && - ast.Extent.StartColumnNumber == symbol.ScriptRegion.StartColumnNumber; + return ast.Extent.StartLineNumber == symbol.NameRegion.StartLineNumber && + ast.Extent.StartColumnNumber == symbol.NameRegion.StartColumnNumber; }, true); - ModifiedFileResponse FileModifications = null; - if (symbol.Type is SymbolType.Function) - { - FileModifications = RenameFunction(symbol, scriptFile.ScriptAst, request); - }else if(symbol.Type is SymbolType.Variable){ - FileModifications = RenameVariable(symbol, scriptFile.ScriptAst, request); - } - + ModifiedFileResponse FileModifications = symbol.Type is SymbolType.Function + ? RenameFunction(symbol, scriptFile.ScriptAst, request) + : RenameVariable(symbol, scriptFile.ScriptAst, request); RenameSymbolResult result = new(); result.Changes.Add(FileModifications); return result; From 56dd927f7cc271c993d3697de80fa8a8cfe5af6c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:55:12 +0300 Subject: [PATCH 053/203] case insensitive compare & adjustment for get ast parent scope to favour functions --- .../PowerShell/Refactoring/VariableVisitor.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 3941d5f98..3a50776d5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -162,9 +162,9 @@ internal static Ast GetAstParentScope(Ast node) } parent = parent.Parent; } - if (parent is ScriptBlockAst && parent.Parent != null) + if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { - parent = GetAstParentScope(parent.Parent); + parent = parent.Parent; } return parent; } @@ -252,7 +252,7 @@ public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) public object VisitCommandParameter(CommandParameterAst commandParameterAst) { // TODO implement command parameter renaming - if (commandParameterAst.ParameterName == OldName) + if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) { if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) @@ -429,6 +429,7 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) { ScopeStack.Push(scriptBlockAst); + scriptBlockAst.ParamBlock?.Visit(this); scriptBlockAst.BeginBlock?.Visit(this); scriptBlockAst.ProcessBlock?.Visit(this); scriptBlockAst.EndBlock?.Visit(this); @@ -493,7 +494,7 @@ public object VisitSubExpression(SubExpressionAst subExpressionAst) public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => throw new NotImplementedException(); public object VisitTrap(TrapStatementAst trapStatementAst) => throw new NotImplementedException(); public object VisitTryStatement(TryStatementAst tryStatementAst) => throw new NotImplementedException(); - public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => throw new NotImplementedException(); + public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null; public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => throw new NotImplementedException(); public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => throw new NotImplementedException(); public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => throw new NotImplementedException(); @@ -501,7 +502,7 @@ public object VisitSubExpression(SubExpressionAst subExpressionAst) public object VisitUsingStatement(UsingStatementAst usingStatement) => throw new NotImplementedException(); public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) { - if (variableExpressionAst.VariablePath.UserPath == OldName) + if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) { if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && variableExpressionAst.Extent.StartLineNumber == StartLineNumber) From b097c4bdc10902c7c647a41840196a75f84dc0f6 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:54:04 +0300 Subject: [PATCH 054/203] initial implentation of prepare rename provider --- .../Server/PsesLanguageServer.cs | 1 + .../Handlers/PrepareRenameSymbol.cs | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 5d75d4994..488f1ac07 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -123,6 +123,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithHandler() // NOTE: The OnInitialize delegate gets run when we first receive the // _Initialize_ request: diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs new file mode 100644 index 000000000..765c21c68 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using System.Management.Automation.Language; +using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Handlers +{ + [Serial, Method("powerShell/PrepareRenameSymbol")] + internal interface IPrepareRenameSymbolHandler : IJsonRpcRequestHandler { } + + internal class PrepareRenameSymbolParams : IRequest + { + public string FileName { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string RenameTo { get; set; } + } + internal class PrepareRenameSymbolResult + { + public string Message; + } + + internal class PrepareRenameSymbolHandler : IPrepareRenameSymbolHandler + { + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + + public PrepareRenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService workspaceService) + { + _logger = loggerFactory.CreateLogger(); + _workspaceService = workspaceService; + } + public async Task Handle(PrepareRenameSymbolParams request, CancellationToken cancellationToken) + { + if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) + { + _logger.LogDebug("Failed to open file!"); + return await Task.FromResult(null).ConfigureAwait(false); + } + return await Task.Run(() => + { + PrepareRenameSymbolResult result = new(); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line + 1, + request.Column + 1); + + if (symbol == null) { result.Message="Unable to Find Symbol"; return result; } + + Ast token = scriptFile.ScriptAst.Find(ast => + { + return ast.Extent.StartLineNumber == symbol.NameRegion.StartLineNumber && + ast.Extent.StartColumnNumber == symbol.NameRegion.StartColumnNumber; + }, true); + + + + result.Message = "Nope cannot do"; + + return result; + }).ConfigureAwait(false); + } + } +} From 849920ed32ccfacc42bf91242066b5414c305c5c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:57:45 +0300 Subject: [PATCH 055/203] new test to handle detection for if the target function is a param ast --- .../PowerShell/Refactoring/VariableVisitor.cs | 8 ++++++-- .../Refactoring/Variables/RefactorsVariablesData.cs | 7 +++++++ .../Refactoring/Variables/VariableNonParam.ps1 | 8 ++++++++ .../Variables/VariableNonParamRenamed.ps1 | 8 ++++++++ .../Refactoring/RefactorVariableTests.cs | 12 ++++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 3a50776d5..642981647 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -40,6 +40,7 @@ internal class VariableRename : ICustomAstVisitor2 internal VariableExpressionAst DuplicateVariableAst; internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; + internal bool isParam; public VariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { @@ -51,7 +52,10 @@ public VariableRename(string NewName, int StartLineNumber, int StartColumnNumber VariableExpressionAst Node = (VariableExpressionAst)VariableRename.GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) { - + if (Node.Parent is ParameterAst) + { + isParam = true; + } TargetVariableAst = Node; OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; @@ -260,7 +264,7 @@ public object VisitCommandParameter(CommandParameterAst commandParameterAst) ShouldRename = true; } - if (ShouldRename) + if (ShouldRename && isParam) { TextChange Change = new() { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 5a89fe846..ddf1a1f25 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -121,5 +121,12 @@ internal static class RenameVariableData Line = 1, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableNonParam = new() + { + FileName = "VariableNonParam.ps1", + Column = 1, + Line = 7, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 new file mode 100644 index 000000000..78119ac37 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 @@ -0,0 +1,8 @@ +$params = @{ + HtmlBodyContent = "Testing JavaScript and CSS paths..." + JavaScriptPaths = ".\Assets\script.js" + StyleSheetPaths = ".\Assets\style.css" +} + +$view = New-VSCodeHtmlContentView -Title "Test View" -ShowInColumn Two +Set-VSCodeHtmlContentView -View $view @params diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 new file mode 100644 index 000000000..e6858827b --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 @@ -0,0 +1,8 @@ +$params = @{ + HtmlBodyContent = "Testing JavaScript and CSS paths..." + JavaScriptPaths = ".\Assets\script.js" + StyleSheetPaths = ".\Assets\style.css" +} + +$Renamed = New-VSCodeHtmlContentView -Title "Test View" -ShowInColumn Two +Set-VSCodeHtmlContentView -View $Renamed @params diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 930657152..d48a8cb54 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -231,6 +231,18 @@ public void VariableScriptWithParamBlock() Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VariableNonParam() + { + RenameSymbolParams request = RenameVariableData.VariableNonParam; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } } } From 9384b3a08c7d6c4c04f4c7651f334d41c0d64eba Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:58:05 +0300 Subject: [PATCH 056/203] new exception for when dot sourcing is detected --- .../PowerShell/Refactoring/VariableVisitor.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 642981647..11f405f20 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -27,6 +27,23 @@ public TargetSymbolNotFoundException(string message, Exception inner) } } + public class TargetVariableIsDotSourcedException : Exception + { + public TargetVariableIsDotSourcedException() + { + } + + public TargetVariableIsDotSourcedException(string message) + : base(message) + { + } + + public TargetVariableIsDotSourcedException(string message, Exception inner) + : base(message, inner) + { + } + } + internal class VariableRename : ICustomAstVisitor2 { private readonly string OldName; @@ -238,6 +255,7 @@ public object VisitCommand(CommandAst commandAst) if (commandAst.CommandElements[1] is StringConstantExpressionAst scriptPath) { dotSourcedScripts.Add(scriptPath.Value); + throw new TargetVariableIsDotSourcedException(); } } From 8e9fffe4e9a27bdb37ae026d3d304995b51f9a7c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:59:06 +0300 Subject: [PATCH 057/203] added more detection and errors for prepare rename symbol --- .../Handlers/PrepareRenameSymbol.cs | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 765c21c68..87705a76e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -10,6 +10,7 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Refactoring; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -25,7 +26,7 @@ internal class PrepareRenameSymbolParams : IRequest } internal class PrepareRenameSymbolResult { - public string Message; + public string message; } internal class PrepareRenameSymbolHandler : IPrepareRenameSymbolHandler @@ -38,6 +39,9 @@ public PrepareRenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService _logger = loggerFactory.CreateLogger(); _workspaceService = workspaceService; } + + + public async Task Handle(PrepareRenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) @@ -47,22 +51,54 @@ public async Task Handle(PrepareRenameSymbolParams re } return await Task.Run(() => { - PrepareRenameSymbolResult result = new(); + PrepareRenameSymbolResult result = new() + { + message = "" + }; SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( request.Line + 1, request.Column + 1); - if (symbol == null) { result.Message="Unable to Find Symbol"; return result; } + if (symbol == null) { result.message = "Unable to Find Symbol"; return result; } Ast token = scriptFile.ScriptAst.Find(ast => { return ast.Extent.StartLineNumber == symbol.NameRegion.StartLineNumber && ast.Extent.StartColumnNumber == symbol.NameRegion.StartColumnNumber; }, true); + if (symbol.Type is SymbolType.Function) + { + FunctionRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); + if (visitor.TargetFunctionAst == null) + { + result.message = "Failed to Find function definition within current file"; + } + } + else if (symbol.Type is SymbolType.Variable or SymbolType.Parameter) + { + try + { + VariableRename visitor = new(request.RenameTo, + symbol.NameRegion.StartLineNumber, + symbol.NameRegion.StartColumnNumber, + scriptFile.ScriptAst); + if (visitor.TargetVariableAst == null) + { + result.message = "Failed to find variable definition within the current file"; + } + } + catch (TargetVariableIsDotSourcedException) + { + result.message = "Variable is dot sourced which is currently not supported unable to perform a rename"; + } - result.Message = "Nope cannot do"; + } return result; }).ConfigureAwait(false); From 682be29ca1f811a8deb636aa727da7147e99356b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 21:08:26 +0300 Subject: [PATCH 058/203] new exception for when the function definition cannot be found --- .../Handlers/PrepareRenameSymbol.cs | 17 +++++++++----- .../PowerShell/Refactoring/FunctionVistor.cs | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 87705a76e..b4b55aff6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -68,13 +68,18 @@ public async Task Handle(PrepareRenameSymbolParams re }, true); if (symbol.Type is SymbolType.Function) { - FunctionRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, - scriptFile.ScriptAst); - if (visitor.TargetFunctionAst == null) + try { + + FunctionRename visitor = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); + } + catch (FunctionDefinitionNotFoundException) + { + result.message = "Failed to Find function definition within current file"; } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs index fc73034b0..fcc491256 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs @@ -9,6 +9,26 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { + + + public class FunctionDefinitionNotFoundException : Exception + { + public FunctionDefinitionNotFoundException() + { + } + + public FunctionDefinitionNotFoundException(string message) + : base(message) + { + } + + public FunctionDefinitionNotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } + + internal class FunctionRename : ICustomAstVisitor2 { private readonly string OldName; @@ -16,7 +36,6 @@ internal class FunctionRename : ICustomAstVisitor2 internal Stack ScopeStack = new(); internal bool ShouldRename; public List Modifications = new(); - private readonly List Log = new(); internal int StartLineNumber; internal int StartColumnNumber; internal FunctionDefinitionAst TargetFunctionAst; @@ -43,7 +62,7 @@ public FunctionRename(string OldName, string NewName, int StartLineNumber, int S TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) { - Log.Add("Failed to get the Commands Function Definition"); + throw new FunctionDefinitionNotFoundException(); } this.StartColumnNumber = TargetFunctionAst.Extent.StartColumnNumber; this.StartLineNumber = TargetFunctionAst.Extent.StartLineNumber; From 8def8bcfaa9348ae553046d46fb1b53079a15350 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 21:58:12 +0300 Subject: [PATCH 059/203] no longer using trygetsymbolatposition as it doesnt detect parameterAst tokents --- .../Handlers/PrepareRenameSymbol.cs | 29 +++---- .../PowerShell/Handlers/RenameSymbol.cs | 83 +++++++++---------- 2 files changed, 50 insertions(+), 62 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index b4b55aff6..d485e747e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -6,7 +6,6 @@ using MediatR; using System.Management.Automation.Language; using OmniSharp.Extensions.JsonRpc; -using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; @@ -40,8 +39,6 @@ public PrepareRenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService _workspaceService = workspaceService; } - - public async Task Handle(PrepareRenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) @@ -55,26 +52,24 @@ public async Task Handle(PrepareRenameSymbolParams re { message = "" }; - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - - if (symbol == null) { result.message = "Unable to Find Symbol"; return result; } Ast token = scriptFile.ScriptAst.Find(ast => { - return ast.Extent.StartLineNumber == symbol.NameRegion.StartLineNumber && - ast.Extent.StartColumnNumber == symbol.NameRegion.StartColumnNumber; + return request.Line == ast.Extent.StartLineNumber && + request.Column >= ast.Extent.StartColumnNumber && request.Column <= ast.Extent.EndColumnNumber; }, true); - if (symbol.Type is SymbolType.Function) + + if (token == null) { result.message = "Unable to Find Symbol"; return result; } + + if (token is FunctionDefinitionAst funcDef) { try { - FunctionRename visitor = new(symbol.NameRegion.Text, + FunctionRename visitor = new(funcDef.Name, request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, + funcDef.Extent.StartLineNumber, + funcDef.Extent.StartColumnNumber, scriptFile.ScriptAst); } catch (FunctionDefinitionNotFoundException) @@ -83,14 +78,14 @@ public async Task Handle(PrepareRenameSymbolParams re result.message = "Failed to Find function definition within current file"; } } - else if (symbol.Type is SymbolType.Variable or SymbolType.Parameter) + else if (token is VariableExpressionAst or CommandAst) { try { VariableRename visitor = new(request.RenameTo, - symbol.NameRegion.StartLineNumber, - symbol.NameRegion.StartColumnNumber, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, scriptFile.ScriptAst); if (visitor.TargetVariableAst == null) { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 4f8bef809..226d6b549 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -7,7 +7,6 @@ using MediatR; using System.Management.Automation.Language; using OmniSharp.Extensions.JsonRpc; -using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; @@ -68,47 +67,49 @@ internal class RenameSymbolHandler : IRenameSymbolHandler private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; - public RenameSymbolHandler(ILoggerFactory loggerFactory,WorkspaceService workspaceService) + public RenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService workspaceService) { _logger = loggerFactory.CreateLogger(); _workspaceService = workspaceService; } - internal static ModifiedFileResponse RenameFunction(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) + internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameSymbolParams request) { - if (symbol.Type is not SymbolType.Function) + if (token is FunctionDefinitionAst funcDef) { - return null; + FunctionRename visitor = new(funcDef.Name, + request.RenameTo, + funcDef.Extent.StartLineNumber, + funcDef.Extent.StartColumnNumber, + scriptAst); + scriptAst.Visit(visitor); + ModifiedFileResponse FileModifications = new(request.FileName) + { + Changes = visitor.Modifications + }; + return FileModifications; + } + return null; - FunctionRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, - scriptAst); - scriptAst.Visit(visitor); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; } - internal static ModifiedFileResponse RenameVariable(SymbolReference symbol, Ast scriptAst, RenameSymbolParams request) + internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameSymbolParams request) { - if (symbol.Type is not (SymbolType.Variable or SymbolType.Parameter)) + if (symbol is VariableExpressionAst or ParameterAst) { - return null; + VariableRename visitor = new(request.RenameTo, + symbol.Extent.StartLineNumber, + symbol.Extent.StartColumnNumber, + scriptAst); + scriptAst.Visit(visitor); + ModifiedFileResponse FileModifications = new(request.FileName) + { + Changes = visitor.Modifications + }; + return FileModifications; + } + return null; - VariableRename visitor = new(request.RenameTo, - symbol.NameRegion.StartLineNumber, - symbol.NameRegion.StartColumnNumber, - scriptAst); - scriptAst.Visit(visitor); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; } public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { @@ -117,27 +118,19 @@ public async Task Handle(RenameSymbolParams request, Cancell _logger.LogDebug("Failed to open file!"); return await Task.FromResult(null).ConfigureAwait(false); } - // Locate the Symbol in the file - // Look at its parent to find its script scope - // I.E In a function - // Lookup all other occurances of the symbol - // replace symbols that fall in the same scope as the initial symbol + return await Task.Run(() => { - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line + 1, - request.Column + 1); - - if (symbol == null) { return null; } - Ast token = scriptFile.ScriptAst.Find(ast => { - return ast.Extent.StartLineNumber == symbol.NameRegion.StartLineNumber && - ast.Extent.StartColumnNumber == symbol.NameRegion.StartColumnNumber; + return request.Line >= ast.Extent.StartLineNumber && request.Line <= ast.Extent.EndLineNumber && + request.Column >= ast.Extent.StartColumnNumber && request.Column <= ast.Extent.EndColumnNumber; }, true); - ModifiedFileResponse FileModifications = symbol.Type is SymbolType.Function - ? RenameFunction(symbol, scriptFile.ScriptAst, request) - : RenameVariable(symbol, scriptFile.ScriptAst, request); + + if (token == null) { return null; } + ModifiedFileResponse FileModifications = token is FunctionDefinitionAst + ? RenameFunction(token, scriptFile.ScriptAst, request) + : RenameVariable(token, scriptFile.ScriptAst, request); RenameSymbolResult result = new(); result.Changes.Add(FileModifications); return result; From 7a9aa033df8f833c1ef65c24aff93d09c243825e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 13 Oct 2023 22:38:45 +0300 Subject: [PATCH 060/203] further adjustments to detection --- .../PowerShell/Handlers/PrepareRenameSymbol.cs | 10 +++++++--- .../Services/PowerShell/Handlers/RenameSymbol.cs | 9 ++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index d485e747e..7db89f78c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -10,6 +10,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -53,12 +55,14 @@ public async Task Handle(PrepareRenameSymbolParams re message = "" }; - Ast token = scriptFile.ScriptAst.Find(ast => + IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => { - return request.Line == ast.Extent.StartLineNumber && - request.Column >= ast.Extent.StartColumnNumber && request.Column <= ast.Extent.EndColumnNumber; + return request.Line+1 == ast.Extent.StartLineNumber && + request.Column+1 >= ast.Extent.StartColumnNumber && request.Column+1 <= ast.Extent.EndColumnNumber; }, true); + Ast token = tokens.Last(); + if (token == null) { result.message = "Unable to Find Symbol"; return result; } if (token is FunctionDefinitionAst funcDef) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 226d6b549..c809fed5d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; +using System.Linq; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -121,12 +122,14 @@ public async Task Handle(RenameSymbolParams request, Cancell return await Task.Run(() => { - Ast token = scriptFile.ScriptAst.Find(ast => + IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => { - return request.Line >= ast.Extent.StartLineNumber && request.Line <= ast.Extent.EndLineNumber && - request.Column >= ast.Extent.StartColumnNumber && request.Column <= ast.Extent.EndColumnNumber; + return request.Line+1 == ast.Extent.StartLineNumber && + request.Column+1 >= ast.Extent.StartColumnNumber && request.Column+1 <= ast.Extent.EndColumnNumber; }, true); + Ast token = tokens.Last(); + if (token == null) { return null; } ModifiedFileResponse FileModifications = token is FunctionDefinitionAst ? RenameFunction(token, scriptFile.ScriptAst, request) From e9f990f3ba68a4795803fcb00d532bb3ae69c468 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 17:00:23 +0300 Subject: [PATCH 061/203] switched to processing using iteration to avoid stack overflow --- package-lock.json | 6 + .../PowerShell/Handlers/RenameSymbol.cs | 4 +- .../Refactoring/IterativeFunctionVistor.cs | 288 ++++++++++++++++++ .../Refactoring/RefactorFunctionTests.cs | 13 +- 4 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 package-lock.json create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..a839281bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "PowerShellEditorServices", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index c809fed5d..b7aa1288a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -77,12 +77,12 @@ internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, Re { if (token is FunctionDefinitionAst funcDef) { - FunctionRename visitor = new(funcDef.Name, + FunctionRenameIterative visitor = new(funcDef.Name, request.RenameTo, funcDef.Extent.StartLineNumber, funcDef.Extent.StartColumnNumber, scriptAst); - scriptAst.Visit(visitor); + visitor.Visit(scriptAst) ModifiedFileResponse FileModifications = new(request.FileName) { Changes = visitor.Modifications diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs new file mode 100644 index 000000000..169638868 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + + internal class FunctionRenameIterative + { + private readonly string OldName; + private readonly string NewName; + internal Queue queue = new(); + internal bool ShouldRename; + public List Modifications = new(); + public List Log = new(); + internal int StartLineNumber; + internal int StartColumnNumber; + internal FunctionDefinitionAst TargetFunctionAst; + internal FunctionDefinitionAst DuplicateFunctionAst; + internal readonly Ast ScriptAst; + + public FunctionRenameIterative(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + this.OldName = OldName; + this.NewName = NewName; + this.StartLineNumber = StartLineNumber; + this.StartColumnNumber = StartColumnNumber; + this.ScriptAst = ScriptAst; + + Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + if (Node != null) + { + if (Node is FunctionDefinitionAst FuncDef) + { + TargetFunctionAst = FuncDef; + } + if (Node is CommandAst) + { + TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + if (TargetFunctionAst == null) + { + throw new FunctionDefinitionNotFoundException(); + } + this.StartColumnNumber = TargetFunctionAst.Extent.StartColumnNumber; + this.StartLineNumber = TargetFunctionAst.Extent.StartLineNumber; + } + } + } + + public class NodeProcessingState + { + public Ast Node { get; set; } + public bool ShouldRename { get; set; } + public IEnumerator ChildrenEnumerator { get; set; } + } + public bool DetermineChildShouldRenameState(NodeProcessingState currentState, Ast child) + { + // The Child Has the name we are looking for + if (child is FunctionDefinitionAst funcDef && funcDef.Name.ToLower() == OldName.ToLower()) + { + // The Child is the function we are looking for + if (child.Extent.StartLineNumber == StartLineNumber && + child.Extent.StartColumnNumber == StartColumnNumber) + { + return true; + + } + // Otherwise its a duplicate named function + else + { + DuplicateFunctionAst = funcDef; + return false; + } + + } + else if (child?.Parent?.Parent is ScriptBlockAst) + { + // The Child is in the same scriptblock as the Target Function + if (TargetFunctionAst.Parent.Parent == child?.Parent?.Parent) + { + return true; + } + // The Child is in the same ScriptBlock as the Duplicate Function + if (DuplicateFunctionAst?.Parent?.Parent == child?.Parent?.Parent) + { + return false; + } + } + else if (child?.Parent is StatementBlockAst) + { + + if (child?.Parent == TargetFunctionAst?.Parent) + { + return true; + } + + if (DuplicateFunctionAst?.Parent == child?.Parent) + { + return false; + } + } + return currentState.ShouldRename; + } + public void Visit(Ast root) + { + Stack processingStack = new(); + + processingStack.Push(new NodeProcessingState { Node = root, ShouldRename = false }); + + while (processingStack.Count > 0) + { + NodeProcessingState currentState = processingStack.Peek(); + + if (currentState.ChildrenEnumerator == null) + { + // First time processing this node. Do the initial processing. + ProcessNode(currentState.Node, currentState.ShouldRename); // This line is crucial. + + // Get the children and set up the enumerator. + IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); + currentState.ChildrenEnumerator = children.GetEnumerator(); + } + + // Process the next child. + if (currentState.ChildrenEnumerator.MoveNext()) + { + Ast child = currentState.ChildrenEnumerator.Current; + bool childShouldRename = DetermineChildShouldRenameState(currentState, child); + processingStack.Push(new NodeProcessingState { Node = child, ShouldRename = childShouldRename }); + } + else + { + // All children have been processed, we're done with this node. + processingStack.Pop(); + } + } + } + + public void ProcessNode(Ast node, bool shouldRename) + { + Log.Add($"Proc node: {node.GetType().Name}, " + + $"SL: {node.Extent.StartLineNumber}, " + + $"SC: {node.Extent.StartColumnNumber}"); + + switch (node) + { + case FunctionDefinitionAst ast: + if (ast.Name.ToLower() == OldName.ToLower()) + { + if (ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber) + { + TargetFunctionAst = ast; + TextChange Change = new() + { + NewText = NewName, + StartLine = ast.Extent.StartLineNumber - 1, + StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, + EndLine = ast.Extent.StartLineNumber - 1, + EndColumn = ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1, + }; + + Modifications.Add(Change); + //node.ShouldRename = true; + } + else + { + // Entering a duplicate functions scope and shouldnt rename + //node.ShouldRename = false; + DuplicateFunctionAst = ast; + } + } + break; + case CommandAst ast: + if (ast.GetCommandName()?.ToLower() == OldName.ToLower()) + { + if (shouldRename) + { + TextChange Change = new() + { + NewText = NewName, + StartLine = ast.Extent.StartLineNumber - 1, + StartColumn = ast.Extent.StartColumnNumber - 1, + EndLine = ast.Extent.StartLineNumber - 1, + EndColumn = ast.Extent.StartColumnNumber + OldName.Length - 1, + }; + Modifications.Add(Change); + } + } + break; + } + Log.Add($"ShouldRename after proc: {shouldRename}"); + } + + public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + Ast result = null; + // Looking for a function + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower(); + }, true); + // Looking for a a Command call + if (null == result) + { + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is CommandAst CommDef && + CommDef.GetCommandName().ToLower() == OldName.ToLower(); + }, true); + } + + return result; + } + + public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + // Look up the targetted object + CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => + { + return ast is CommandAst CommDef && + CommDef.GetCommandName().ToLower() == OldName.ToLower() && + CommDef.Extent.StartLineNumber == StartLineNumber && + CommDef.Extent.StartColumnNumber == StartColumnNumber; + }, true); + + string FunctionName = TargetCommand.GetCommandName(); + + List FunctionDefinitions = ScriptFile.FindAll(ast => + { + return ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower() && + (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || + (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && + FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Sort function definitions + //FunctionDefinitions.Sort((a, b) => + //{ + // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - + // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; + //}); + // Determine which function definition is the right one + FunctionDefinitionAst CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetCommand.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index c7ba82774..edbce30e0 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -53,15 +53,22 @@ internal static string GetModifiedScript(string OriginalScript, ModifiedFileResp internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request, SymbolReference symbol) { - FunctionRename visitor = new(symbol.NameRegion.Text, + //FunctionRename visitor = new(symbol.NameRegion.Text, + // request.RenameTo, + // symbol.ScriptRegion.StartLineNumber, + // symbol.ScriptRegion.StartColumnNumber, + // scriptFile.ScriptAst); + // scriptFile.ScriptAst.Visit(visitor); + FunctionRenameIterative iterative = new(symbol.NameRegion.Text, request.RenameTo, symbol.ScriptRegion.StartLineNumber, symbol.ScriptRegion.StartColumnNumber, scriptFile.ScriptAst); - scriptFile.ScriptAst.Visit(visitor); + iterative.Visit(scriptFile.ScriptAst); + //scriptFile.ScriptAst.Visit(visitor); ModifiedFileResponse changes = new(request.FileName) { - Changes = visitor.Modifications + Changes = iterative.Modifications }; return GetModifiedScript(scriptFile.Contents, changes); } From 5ee72cc7c811b45c57f597a17d7e06b88cf30ff1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:23:31 +0300 Subject: [PATCH 062/203] Fixing typo --- .../PowerShell/Refactoring/IterativeFunctionVistor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 169638868..201a01abe 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -9,7 +9,7 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { - internal class FunctionRenameIterative + internal class IterativeFunctionRename { private readonly string OldName; private readonly string NewName; @@ -23,7 +23,7 @@ internal class FunctionRenameIterative internal FunctionDefinitionAst DuplicateFunctionAst; internal readonly Ast ScriptAst; - public FunctionRenameIterative(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public IterativeFunctionRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { this.OldName = OldName; this.NewName = NewName; From 65dc4e79336d01a2905433c655fa9dfb177a2057 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:24:00 +0300 Subject: [PATCH 063/203] Switching tests to use iterative class --- .../Refactoring/RefactorFunctionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index edbce30e0..22e1a4bc5 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -59,7 +59,7 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams re // symbol.ScriptRegion.StartColumnNumber, // scriptFile.ScriptAst); // scriptFile.ScriptAst.Visit(visitor); - FunctionRenameIterative iterative = new(symbol.NameRegion.Text, + IterativeFunctionRename iterative = new(symbol.NameRegion.Text, request.RenameTo, symbol.ScriptRegion.StartLineNumber, symbol.ScriptRegion.StartColumnNumber, From 8b1de2986b19ef62f0fe1ed69e76f77e8714fcdd Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:24:22 +0300 Subject: [PATCH 064/203] init version of the variable rename iterative --- .../Refactoring/IterativeVariableVisitor.cs | 393 ++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs new file mode 100644 index 000000000..b76c381fa --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; +using System.Linq; + +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + + internal class VariableRenameIterative + { + private readonly string OldName; + private readonly string NewName; + internal Stack ScopeStack = new(); + internal bool ShouldRename; + public List Modifications = new(); + internal int StartLineNumber; + internal int StartColumnNumber; + internal VariableExpressionAst TargetVariableAst; + internal VariableExpressionAst DuplicateVariableAst; + internal List dotSourcedScripts = new(); + internal readonly Ast ScriptAst; + internal bool isParam; + internal List Log = new(); + + public VariableRenameIterative(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + this.NewName = NewName; + this.StartLineNumber = StartLineNumber; + this.StartColumnNumber = StartColumnNumber; + this.ScriptAst = ScriptAst; + + VariableExpressionAst Node = (VariableExpressionAst)VariableRename.GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); + if (Node != null) + { + if (Node.Parent is ParameterAst) + { + isParam = true; + } + TargetVariableAst = Node; + OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); + this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; + this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; + } + } + + public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + Ast result = null; + result = ScriptAst.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is VariableExpressionAst or CommandParameterAst; + }, true); + if (result == null) + { + throw new TargetSymbolNotFoundException(); + } + return result; + } + + public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + + // Look up the target object + Ast node = GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst); + + string name = node is CommandParameterAst commdef + ? commdef.ParameterName + : node is VariableExpressionAst varDef ? varDef.VariablePath.UserPath : throw new TargetSymbolNotFoundException(); + + Ast TargetParent = GetAstParentScope(node); + + List VariableAssignments = ScriptAst.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the def if we have no matches + if (VariableAssignments.Count == 0) + { + return node; + } + Ast CorrectDefinition = null; + for (int i = VariableAssignments.Count - 1; i >= 0; i--) + { + VariableExpressionAst element = VariableAssignments[i]; + + Ast parent = GetAstParentScope(element); + // closest assignment statement is within the scope of the node + if (TargetParent == parent) + { + CorrectDefinition = element; + break; + } + else if (node.Parent is AssignmentStatementAst) + { + // the node is probably the first assignment statement within the scope + CorrectDefinition = node; + break; + } + // node is proably just a reference of an assignment statement within the global scope or higher + if (node.Parent is not AssignmentStatementAst) + { + if (null == parent || null == parent.Parent) + { + // we have hit the global scope of the script file + CorrectDefinition = element; + break; + } + if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst) + { + if (node.Parent is CommandAst commDef) + { + if (funcDef.Name == commDef.GetCommandName() + && funcDef.Parent.Parent == TargetParent) + { + CorrectDefinition = element; + break; + } + } + } + if (WithinTargetsScope(element, node)) + { + CorrectDefinition = element; + } + } + + + } + return CorrectDefinition ?? node; + } + + internal static Ast GetAstParentScope(Ast node) + { + Ast parent = node; + // Walk backwards up the tree look + while (parent != null) + { + if (parent is ScriptBlockAst or FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) + { + parent = parent.Parent; + } + return parent; + } + + internal static bool WithinTargetsScope(Ast Target, Ast Child) + { + bool r = false; + Ast childParent = Child.Parent; + Ast TargetScope = GetAstParentScope(Target); + while (childParent != null) + { + if (childParent is FunctionDefinitionAst) + { + break; + } + if (childParent == TargetScope) + { + break; + } + childParent = childParent.Parent; + } + if (childParent == TargetScope) + { + r = true; + } + return r; + } + + public class NodeProcessingState + { + public Ast Node { get; set; } + public IEnumerator ChildrenEnumerator { get; set; } + } + + public void Visit(Ast root) + { + Stack processingStack = new(); + + processingStack.Push(new NodeProcessingState { Node = root}); + + while (processingStack.Count > 0) + { + NodeProcessingState currentState = processingStack.Peek(); + + if (currentState.ChildrenEnumerator == null) + { + // First time processing this node. Do the initial processing. + ProcessNode(currentState.Node); // This line is crucial. + + // Get the children and set up the enumerator. + IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); + currentState.ChildrenEnumerator = children.GetEnumerator(); + } + + // Process the next child. + if (currentState.ChildrenEnumerator.MoveNext()) + { + Ast child = currentState.ChildrenEnumerator.Current; + processingStack.Push(new NodeProcessingState { Node = child}); + } + else + { + // All children have been processed, we're done with this node. + processingStack.Pop(); + } + } + } + + public void ProcessNode(Ast node) + { + Log.Add($"Proc node: {node.GetType().Name}, " + + $"SL: {node.Extent.StartLineNumber}, " + + $"SC: {node.Extent.StartColumnNumber}"); + + switch (node) + { + case CommandParameterAst commandParameterAst: + + if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) + { + if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && + commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) + { + ShouldRename = true; + } + + if (ShouldRename && isParam) + { + TextChange Change = new() + { + NewText = NewName.Contains("-") ? NewName : "-" + NewName, + StartLine = commandParameterAst.Extent.StartLineNumber - 1, + StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, + EndLine = commandParameterAst.Extent.StartLineNumber - 1, + EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, + }; + + Modifications.Add(Change); + } + } + break; + case VariableExpressionAst variableExpressionAst: + if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) + { + if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && + variableExpressionAst.Extent.StartLineNumber == StartLineNumber) + { + ShouldRename = true; + TargetVariableAst = variableExpressionAst; + }else if (variableExpressionAst.Parent is CommandAst commandAst) + { + if(WithinTargetsScope(TargetVariableAst, commandAst)) + { + ShouldRename = true; + } + } + else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && + assignment.Operator == TokenKind.Equals) + { + if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) + { + DuplicateVariableAst = variableExpressionAst; + ShouldRename = false; + } + + } + + if (ShouldRename) + { + // have some modifications to account for the dollar sign prefix powershell uses for variables + TextChange Change = new() + { + NewText = NewName.Contains("$") ? NewName : "$" + NewName, + StartLine = variableExpressionAst.Extent.StartLineNumber - 1, + StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, + EndLine = variableExpressionAst.Extent.StartLineNumber - 1, + EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, + }; + + Modifications.Add(Change); + } + } + break; + + } + Log.Add($"ShouldRename after proc: {ShouldRename}"); + } + + public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + Ast result = null; + // Looking for a function + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower(); + }, true); + // Looking for a a Command call + if (null == result) + { + result = ScriptFile.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + ast is CommandAst CommDef && + CommDef.GetCommandName().ToLower() == OldName.ToLower(); + }, true); + } + + return result; + } + + public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + // Look up the targetted object + CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => + { + return ast is CommandAst CommDef && + CommDef.GetCommandName().ToLower() == OldName.ToLower() && + CommDef.Extent.StartLineNumber == StartLineNumber && + CommDef.Extent.StartColumnNumber == StartColumnNumber; + }, true); + + string FunctionName = TargetCommand.GetCommandName(); + + List FunctionDefinitions = ScriptFile.FindAll(ast => + { + return ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower() && + (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || + (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && + FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Sort function definitions + //FunctionDefinitions.Sort((a, b) => + //{ + // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - + // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; + //}); + // Determine which function definition is the right one + FunctionDefinitionAst CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetCommand.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + } +} From eea9369180e9c290205e66b07a5b59f8ccd5db70 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:24:39 +0300 Subject: [PATCH 065/203] switched tests and vscode to use iterative class --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 8 ++++---- .../Refactoring/RefactorVariableTests.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index b7aa1288a..3c7c2c999 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -77,12 +77,12 @@ internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, Re { if (token is FunctionDefinitionAst funcDef) { - FunctionRenameIterative visitor = new(funcDef.Name, + IterativeFunctionRename visitor = new(funcDef.Name, request.RenameTo, funcDef.Extent.StartLineNumber, funcDef.Extent.StartColumnNumber, scriptAst); - visitor.Visit(scriptAst) + visitor.Visit(scriptAst); ModifiedFileResponse FileModifications = new(request.FileName) { Changes = visitor.Modifications @@ -97,11 +97,11 @@ internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, R { if (symbol is VariableExpressionAst or ParameterAst) { - VariableRename visitor = new(request.RenameTo, + VariableRenameIterative visitor = new(request.RenameTo, symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst); - scriptAst.Visit(visitor); + visitor.Visit(scriptAst); ModifiedFileResponse FileModifications = new(request.FileName) { Changes = visitor.Modifications diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index d48a8cb54..2035d7bc6 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -60,14 +60,14 @@ internal static string GetModifiedScript(string OriginalScript, ModifiedFileResp internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request) { - VariableRename visitor = new(request.RenameTo, + VariableRenameIterative iterative = new(request.RenameTo, request.Line, request.Column, scriptFile.ScriptAst); - scriptFile.ScriptAst.Visit(visitor); + iterative.Visit(scriptFile.ScriptAst); ModifiedFileResponse changes = new(request.FileName) { - Changes = visitor.Modifications + Changes = iterative.Modifications }; return GetModifiedScript(scriptFile.Contents, changes); } @@ -220,7 +220,7 @@ public void VariableCommandParameterReverse() Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] + [Fact] public void VariableScriptWithParamBlock() { RenameSymbolParams request = RenameVariableData.VariableScriptWithParamBlock; @@ -232,7 +232,7 @@ public void VariableScriptWithParamBlock() Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] + [Fact] public void VariableNonParam() { RenameSymbolParams request = RenameVariableData.VariableNonParam; From 8b55eac553b6e750d3f78572bc46f07bb64fe100 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:34:25 +0300 Subject: [PATCH 066/203] new test to check for method with the same parameter name --- .../Variables/RefactorsVariablesData.cs | 7 ++++++ .../VariableParameterCommndWithSameName.ps1 | 22 +++++++++++++++++++ ...ableParameterCommndWithSameNameRenamed.ps1 | 22 +++++++++++++++++++ .../Refactoring/RefactorVariableTests.cs | 13 +++++++++++ 4 files changed, 64 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index ddf1a1f25..ec9b7fe7d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -128,5 +128,12 @@ internal static class RenameVariableData Line = 7, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableParameterCommndWithSameName = new() + { + FileName = "VariableParameterCommndWithSameName.ps1", + Column = 13, + Line = 9, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 new file mode 100644 index 000000000..86d9c6c75 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 @@ -0,0 +1,22 @@ +function Test-AADConnected { + + param ( + [Parameter(Mandatory = $false)][String]$UserPrincipalName + ) + Begin {} + Process { + [HashTable]$ConnectAADSplat = @{} + if ($UserPrincipalName) { + $ConnectAADSplat = @{ + AccountId = $UserPrincipalName + ErrorAction = 'Stop' + } + } + } +} + +Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop +$UserPrincipalName = "Bob" +if ($UserPrincipalName) { + $SplatTestAADConnected.Add('UserPrincipalName', $UserPrincipalName) +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 new file mode 100644 index 000000000..7ce2e4f92 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 @@ -0,0 +1,22 @@ +function Test-AADConnected { + + param ( + [Parameter(Mandatory = $false)][String]$Renamed + ) + Begin {} + Process { + [HashTable]$ConnectAADSplat = @{} + if ($Renamed) { + $ConnectAADSplat = @{ + AccountId = $Renamed + ErrorAction = 'Stop' + } + } + } +} + +Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop +$UserPrincipalName = "Bob" +if ($UserPrincipalName) { + $SplatTestAADConnected.Add('UserPrincipalName', $UserPrincipalName) +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 2035d7bc6..2976cd0b8 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -244,5 +244,18 @@ public void VariableNonParam() Assert.Equal(expectedContent.Contents, modifiedcontent); } + [Fact] + public void VariableParameterCommndWithSameName() + { + RenameSymbolParams request = RefactorsFunctionData.VariableParameterCommndWithSameName; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } } } From 3c0364e5c44bb12315e2002a081ba08bd40524ac Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:59:50 +0300 Subject: [PATCH 067/203] fixing up tests for VariableParameterCommandWithSameName --- .../Refactoring/IterativeVariableVisitor.cs | 27 +++++++++++++++---- .../Variables/RefactorsVariablesData.cs | 2 +- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index b76c381fa..6c6f51290 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -23,6 +23,7 @@ internal class VariableRenameIterative internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; internal bool isParam; + internal FunctionDefinitionAst TargetFunction; internal List Log = new(); public VariableRenameIterative(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) @@ -38,6 +39,20 @@ public VariableRenameIterative(string NewName, int StartLineNumber, int StartCol if (Node.Parent is ParameterAst) { isParam = true; + Ast parent = Node; + // Look for a target function that the parameterAst will be within if it exists + while (parent != null) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + if (parent != null) + { + TargetFunction = (FunctionDefinitionAst)parent; + } } TargetVariableAst = Node; OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); @@ -191,7 +206,7 @@ public void Visit(Ast root) { Stack processingStack = new(); - processingStack.Push(new NodeProcessingState { Node = root}); + processingStack.Push(new NodeProcessingState { Node = root }); while (processingStack.Count > 0) { @@ -211,7 +226,7 @@ public void Visit(Ast root) if (currentState.ChildrenEnumerator.MoveNext()) { Ast child = currentState.ChildrenEnumerator.Current; - processingStack.Push(new NodeProcessingState { Node = child}); + processingStack.Push(new NodeProcessingState { Node = child }); } else { @@ -239,7 +254,8 @@ public void ProcessNode(Ast node) ShouldRename = true; } - if (ShouldRename && isParam) + if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && + commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && ShouldRename && isParam) { TextChange Change = new() { @@ -262,9 +278,10 @@ public void ProcessNode(Ast node) { ShouldRename = true; TargetVariableAst = variableExpressionAst; - }else if (variableExpressionAst.Parent is CommandAst commandAst) + } + else if (variableExpressionAst.Parent is CommandAst commandAst) { - if(WithinTargetsScope(TargetVariableAst, commandAst)) + if (WithinTargetsScope(TargetVariableAst, commandAst)) { ShouldRename = true; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index ec9b7fe7d..7705bfe7a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -128,7 +128,7 @@ internal static class RenameVariableData Line = 7, RenameTo = "Renamed" }; - public static readonly RenameSymbolParams VariableParameterCommndWithSameName = new() + public static readonly RenameSymbolParams VariableParameterCommandWithSameName = new() { FileName = "VariableParameterCommndWithSameName.ps1", Column = 13, From 4a59385205e96a52f763de6fec37595a6d0c0737 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:00:02 +0300 Subject: [PATCH 068/203] fixing up tests --- .../Refactoring/RefactorVariableTests.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 2976cd0b8..327a0b337 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -245,15 +245,13 @@ public void VariableNonParam() } [Fact] - public void VariableParameterCommndWithSameName() + public void VariableParameterCommandWithSameName() { - RenameSymbolParams request = RefactorsFunctionData.VariableParameterCommndWithSameName; + RenameSymbolParams request = RenameVariableData.VariableParameterCommandWithSameName; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + string modifiedcontent = TestRenaming(scriptFile, request); Assert.Equal(expectedContent.Contents, modifiedcontent); } From 0fd2601f47e81bd591a123e6fda60af06b0f89ca Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:06:16 +0300 Subject: [PATCH 069/203] adjusting tests for more complexity --- .../Refactoring/IterativeVariableVisitor.cs | 1 - .../VariableParameterCommndWithSameName.ps1 | 34 +++++++++++++++++++ ...ableParameterCommndWithSameNameRenamed.ps1 | 34 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 6c6f51290..cd945637b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -313,7 +313,6 @@ public void ProcessNode(Ast node) } } break; - } Log.Add($"ShouldRename after proc: {ShouldRename}"); } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 index 86d9c6c75..3f3ef39b0 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 @@ -15,6 +15,40 @@ function Test-AADConnected { } } +function Set-MSolUMFA{ + [CmdletBinding(SupportsShouldProcess=$true)] + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string]$UserPrincipalName, + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateSet('Enabled','Disabled','Enforced')][String]$StrongAuthenticationRequiremets + ) + begin{ + # Check if connected to Msol Session already + if (!(Test-MSolConnected)) { + Write-Verbose('No existing Msol session detected') + try { + Write-Verbose('Initiating connection to Msol') + Connect-MsolService -ErrorAction Stop + Write-Verbose('Connected to Msol successfully') + }catch{ + return Write-Error($_.Exception.Message) + } + } + if(!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ + return Write-Error('Insufficient permissions to set MFA') + } + } + Process{ + # Get the time and calc 2 min to the future + $TimeStart = Get-Date + $TimeEnd = $timeStart.addminutes(1) + $Finished=$false + #Loop to check if the user exists already + if ($PSCmdlet.ShouldProcess($UserPrincipalName, "StrongAuthenticationRequiremets = "+$StrongAuthenticationRequiremets)) { + } + } + End{} +} + Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop $UserPrincipalName = "Bob" if ($UserPrincipalName) { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 index 7ce2e4f92..1f5bcc598 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 @@ -15,6 +15,40 @@ function Test-AADConnected { } } +function Set-MSolUMFA{ + [CmdletBinding(SupportsShouldProcess=$true)] + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string]$UserPrincipalName, + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateSet('Enabled','Disabled','Enforced')][String]$StrongAuthenticationRequiremets + ) + begin{ + # Check if connected to Msol Session already + if (!(Test-MSolConnected)) { + Write-Verbose('No existing Msol session detected') + try { + Write-Verbose('Initiating connection to Msol') + Connect-MsolService -ErrorAction Stop + Write-Verbose('Connected to Msol successfully') + }catch{ + return Write-Error($_.Exception.Message) + } + } + if(!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ + return Write-Error('Insufficient permissions to set MFA') + } + } + Process{ + # Get the time and calc 2 min to the future + $TimeStart = Get-Date + $TimeEnd = $timeStart.addminutes(1) + $Finished=$false + #Loop to check if the user exists already + if ($PSCmdlet.ShouldProcess($UserPrincipalName, "StrongAuthenticationRequiremets = "+$StrongAuthenticationRequiremets)) { + } + } + End{} +} + Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop $UserPrincipalName = "Bob" if ($UserPrincipalName) { From 8cc9ba0907fb3e5df75de1b4506b5c6c6c00e72e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sat, 14 Oct 2023 22:54:08 +0300 Subject: [PATCH 070/203] now adds Alias on commandParameterRenaming --- .../Refactoring/IterativeVariableVisitor.cs | 79 ++++++++++++++++--- .../VariableCommandParameterRenamed.ps1 | 2 +- .../Variables/VariableInParamRenamed.ps1 | 2 +- .../VariableParameterCommndWithSameName.ps1 | 2 +- ...ableParameterCommndWithSameNameRenamed.ps1 | 2 +- .../VariableScriptWithParamBlockRenamed.ps1 | 2 +- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index cd945637b..b2d56d121 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -23,6 +23,7 @@ internal class VariableRenameIterative internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; internal bool isParam; + internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; internal List Log = new(); @@ -156,7 +157,7 @@ VarDef.Parent is AssignmentStatementAst or ParameterAst && internal static Ast GetAstParentScope(Ast node) { Ast parent = node; - // Walk backwards up the tree look + // Walk backwards up the tree lookinf for a ScriptBLock of a FunctionDefinition while (parent != null) { if (parent is ScriptBlockAst or FunctionDefinitionAst) @@ -255,18 +256,25 @@ public void ProcessNode(Ast node) } if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && - commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && ShouldRename && isParam) + commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam) { - TextChange Change = new() + if (ShouldRename) { - NewText = NewName.Contains("-") ? NewName : "-" + NewName, - StartLine = commandParameterAst.Extent.StartLineNumber - 1, - StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, - EndLine = commandParameterAst.Extent.StartLineNumber - 1, - EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, - }; - - Modifications.Add(Change); + TextChange Change = new() + { + NewText = NewName.Contains("-") ? NewName : "-" + NewName, + StartLine = commandParameterAst.Extent.StartLineNumber - 1, + StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, + EndLine = commandParameterAst.Extent.StartLineNumber - 1, + EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, + }; + + Modifications.Add(Change); + } + } + else + { + ShouldRename = false; } } break; @@ -296,9 +304,15 @@ public void ProcessNode(Ast node) } } - + else + { + ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); + } if (ShouldRename) { + // If the variables parent is a parameterAst Add a modification + //to add an Alias to the parameter so that any other scripts out of context calling it will still work + // have some modifications to account for the dollar sign prefix powershell uses for variables TextChange Change = new() { @@ -308,8 +322,47 @@ public void ProcessNode(Ast node) EndLine = variableExpressionAst.Extent.StartLineNumber - 1, EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, }; - + // If the variables parent is a parameterAst Add a modification + //to add an Alias to the parameter so that any other scripts out of context calling it will still work + if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet) + { + TextChange aliasChange = new(); + foreach (Ast Attr in paramAst.Attributes) + { + if (Attr is AttributeAst AttrAst) + { + // Alias Already Exists + if (AttrAst.TypeName.FullName == "Alias") + { + string existingEntries = AttrAst.Extent.Text + .Substring("[Alias(".Length); + existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); + string nentries = existingEntries + $", \"{OldName}\""; + + aliasChange.NewText = $"[Alias({nentries})]"; + aliasChange.StartLine = Attr.Extent.StartLineNumber - 1; + aliasChange.StartColumn = Attr.Extent.StartColumnNumber - 1; + aliasChange.EndLine = Attr.Extent.StartLineNumber - 1; + aliasChange.EndColumn = Attr.Extent.EndColumnNumber - 1; + + break; + } + + } + } + if (aliasChange.NewText == null) + { + aliasChange.NewText = $"[Alias(\"{OldName}\")]"; + aliasChange.StartLine = variableExpressionAst.Extent.StartLineNumber - 1; + aliasChange.StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1; + aliasChange.EndLine = variableExpressionAst.Extent.StartLineNumber - 1; + aliasChange.EndColumn = variableExpressionAst.Extent.StartColumnNumber - 1; + } + Modifications.Add(aliasChange); + AliasSet = true; + } Modifications.Add(Change); + } } break; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 index e74504a4d..1e6ac9d0f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 @@ -1,6 +1,6 @@ function Get-foo { param ( - [string]$Renamed, + [string][Alias("string")]$Renamed, [int]$pos ) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 index 2a810e887..4f567188c 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 @@ -19,7 +19,7 @@ function Write-Item($itemCount) { # Do-Work will be underlined in green if you haven't disable script analysis. # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. -function Do-Work($Renamed) { +function Do-Work([Alias("workCount")]$Renamed) { Write-Output "Doing work..." Write-Item $Renamed Write-Host "Done!" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 index 3f3ef39b0..650271316 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 @@ -1,7 +1,7 @@ function Test-AADConnected { param ( - [Parameter(Mandatory = $false)][String]$UserPrincipalName + [Parameter(Mandatory = $false)][Alias("UPName")][String]$UserPrincipalName ) Begin {} Process { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 index 1f5bcc598..9c88a44d4 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 @@ -1,7 +1,7 @@ function Test-AADConnected { param ( - [Parameter(Mandatory = $false)][String]$Renamed + [Parameter(Mandatory = $false)][Alias("UPName", "UserPrincipalName")][String]$Renamed ) Begin {} Process { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 index 4f42f891a..e218fce9f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 @@ -1,4 +1,4 @@ -param([int]$Count=50, [int]$Renamed=200) +param([int]$Count=50, [int][Alias("DelayMilliSeconds")]$Renamed=200) function Write-Item($itemCount) { $i = 1 From d6da7996e8e308d2c989046823f9b12071541459 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:17:47 +0300 Subject: [PATCH 071/203] refactored alias creation for readability --- .../Refactoring/IterativeVariableVisitor.cs | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index b2d56d121..b99b9ef36 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -281,12 +281,14 @@ public void ProcessNode(Ast node) case VariableExpressionAst variableExpressionAst: if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) { + // Is this the Target Variable if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && variableExpressionAst.Extent.StartLineNumber == StartLineNumber) { ShouldRename = true; TargetVariableAst = variableExpressionAst; } + // Is this a Command Ast within scope else if (variableExpressionAst.Parent is CommandAst commandAst) { if (WithinTargetsScope(TargetVariableAst, commandAst)) @@ -294,6 +296,7 @@ public void ProcessNode(Ast node) ShouldRename = true; } } + // Is this a Variable Assignment thats not within scope else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && assignment.Operator == TokenKind.Equals) { @@ -304,15 +307,13 @@ public void ProcessNode(Ast node) } } + // Else is the variable within scope else { ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); } if (ShouldRename) { - // If the variables parent is a parameterAst Add a modification - //to add an Alias to the parameter so that any other scripts out of context calling it will still work - // have some modifications to account for the dollar sign prefix powershell uses for variables TextChange Change = new() { @@ -323,41 +324,9 @@ public void ProcessNode(Ast node) EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, }; // If the variables parent is a parameterAst Add a modification - //to add an Alias to the parameter so that any other scripts out of context calling it will still work if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet) { - TextChange aliasChange = new(); - foreach (Ast Attr in paramAst.Attributes) - { - if (Attr is AttributeAst AttrAst) - { - // Alias Already Exists - if (AttrAst.TypeName.FullName == "Alias") - { - string existingEntries = AttrAst.Extent.Text - .Substring("[Alias(".Length); - existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); - string nentries = existingEntries + $", \"{OldName}\""; - - aliasChange.NewText = $"[Alias({nentries})]"; - aliasChange.StartLine = Attr.Extent.StartLineNumber - 1; - aliasChange.StartColumn = Attr.Extent.StartColumnNumber - 1; - aliasChange.EndLine = Attr.Extent.StartLineNumber - 1; - aliasChange.EndColumn = Attr.Extent.EndColumnNumber - 1; - - break; - } - - } - } - if (aliasChange.NewText == null) - { - aliasChange.NewText = $"[Alias(\"{OldName}\")]"; - aliasChange.StartLine = variableExpressionAst.Extent.StartLineNumber - 1; - aliasChange.StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1; - aliasChange.EndLine = variableExpressionAst.Extent.StartLineNumber - 1; - aliasChange.EndColumn = variableExpressionAst.Extent.StartColumnNumber - 1; - } + TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); AliasSet = true; } @@ -370,6 +339,48 @@ public void ProcessNode(Ast node) Log.Add($"ShouldRename after proc: {ShouldRename}"); } + internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) + { + // Check if an Alias AttributeAst already exists and append the new Alias to the existing list + // Otherwise Create a new Alias Attribute + // Add the modidifcations to the changes + // the Attribute will be appended before the variable or in the existing location of the Original Alias + TextChange aliasChange = new(); + foreach (Ast Attr in paramAst.Attributes) + { + if (Attr is AttributeAst AttrAst) + { + // Alias Already Exists + if (AttrAst.TypeName.FullName == "Alias") + { + string existingEntries = AttrAst.Extent.Text + .Substring("[Alias(".Length); + existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); + string nentries = existingEntries + $", \"{OldName}\""; + + aliasChange.NewText = $"[Alias({nentries})]"; + aliasChange.StartLine = Attr.Extent.StartLineNumber - 1; + aliasChange.StartColumn = Attr.Extent.StartColumnNumber - 1; + aliasChange.EndLine = Attr.Extent.StartLineNumber - 1; + aliasChange.EndColumn = Attr.Extent.EndColumnNumber - 1; + + break; + } + + } + } + if (aliasChange.NewText == null) + { + aliasChange.NewText = $"[Alias(\"{OldName}\")]"; + aliasChange.StartLine = variableExpressionAst.Extent.StartLineNumber - 1; + aliasChange.StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1; + aliasChange.EndLine = variableExpressionAst.Extent.StartLineNumber - 1; + aliasChange.EndColumn = variableExpressionAst.Extent.StartColumnNumber - 1; + } + + return aliasChange; + } + public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) { Ast result = null; From 828a1f50b07fc1505abe15b2b3f1bdfe5cc73857 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:48:08 +0300 Subject: [PATCH 072/203] updated prepeare rename symbol to use iterative and added msg for if symbol isnt found --- .../PowerShell/Handlers/PrepareRenameSymbol.cs | 16 ++++++++++------ .../Services/PowerShell/Handlers/RenameSymbol.cs | 9 +++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 7db89f78c..c6adc081f 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -58,19 +58,23 @@ public async Task Handle(PrepareRenameSymbolParams re IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => { return request.Line+1 == ast.Extent.StartLineNumber && - request.Column+1 >= ast.Extent.StartColumnNumber && request.Column+1 <= ast.Extent.EndColumnNumber; - }, true); + request.Column+1 >= ast.Extent.StartColumnNumber; + }, false); - Ast token = tokens.Last(); + Ast token = tokens.LastOrDefault(); - if (token == null) { result.message = "Unable to Find Symbol"; return result; } + if (token == null) + { + result.message = "Unable to find symbol"; + return result; + } if (token is FunctionDefinitionAst funcDef) { try { - FunctionRename visitor = new(funcDef.Name, + IterativeFunctionRename visitor = new(funcDef.Name, request.RenameTo, funcDef.Extent.StartLineNumber, funcDef.Extent.StartColumnNumber, @@ -87,7 +91,7 @@ public async Task Handle(PrepareRenameSymbolParams re try { - VariableRename visitor = new(request.RenameTo, + IterativeVariableRename visitor = new(request.RenameTo, token.Extent.StartLineNumber, token.Extent.StartColumnNumber, scriptFile.ScriptAst); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 3c7c2c999..684d703ce 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -97,7 +97,7 @@ internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, R { if (symbol is VariableExpressionAst or ParameterAst) { - VariableRenameIterative visitor = new(request.RenameTo, + IterativeVariableRename visitor = new(request.RenameTo, symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst); @@ -122,13 +122,14 @@ public async Task Handle(RenameSymbolParams request, Cancell return await Task.Run(() => { + IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => { return request.Line+1 == ast.Extent.StartLineNumber && - request.Column+1 >= ast.Extent.StartColumnNumber && request.Column+1 <= ast.Extent.EndColumnNumber; - }, true); + request.Column+1 >= ast.Extent.StartColumnNumber; + }, false); - Ast token = tokens.Last(); + Ast token = tokens.LastOrDefault(); if (token == null) { return null; } ModifiedFileResponse FileModifications = token is FunctionDefinitionAst From 332849705376f1afe31d328c8e20f7efc7928ddf Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:48:35 +0300 Subject: [PATCH 073/203] renamed renamevariableiterative to IterativeVariableRename --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 4 ++-- .../Refactoring/RefactorVariableTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index b99b9ef36..3261afe98 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -9,7 +9,7 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { - internal class VariableRenameIterative + internal class IterativeVariableRename { private readonly string OldName; private readonly string NewName; @@ -27,7 +27,7 @@ internal class VariableRenameIterative internal FunctionDefinitionAst TargetFunction; internal List Log = new(); - public VariableRenameIterative(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 327a0b337..ff6cc401f 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -60,7 +60,7 @@ internal static string GetModifiedScript(string OriginalScript, ModifiedFileResp internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request) { - VariableRenameIterative iterative = new(request.RenameTo, + IterativeVariableRename iterative = new(request.RenameTo, request.Line, request.Column, scriptFile.ScriptAst); From d90a5eaca1b206e5f7b88848fa5a4844afc3a479 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:50:54 +0300 Subject: [PATCH 074/203] using switch instead of else if --- .../Handlers/PrepareRenameSymbol.cs | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index c6adc081f..630eec4b0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -57,8 +57,8 @@ public async Task Handle(PrepareRenameSymbolParams re IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => { - return request.Line+1 == ast.Extent.StartLineNumber && - request.Column+1 >= ast.Extent.StartColumnNumber; + return request.Line + 1 == ast.Extent.StartLineNumber && + request.Column + 1 >= ast.Extent.StartColumnNumber; }, false); Ast token = tokens.LastOrDefault(); @@ -69,43 +69,50 @@ public async Task Handle(PrepareRenameSymbolParams re return result; } - if (token is FunctionDefinitionAst funcDef) - { - try - { - - IterativeFunctionRename visitor = new(funcDef.Name, - request.RenameTo, - funcDef.Extent.StartLineNumber, - funcDef.Extent.StartColumnNumber, - scriptFile.ScriptAst); - } - catch (FunctionDefinitionNotFoundException) - { - - result.message = "Failed to Find function definition within current file"; - } - } - else if (token is VariableExpressionAst or CommandAst) + switch (token) { + case FunctionDefinitionAst funcDef: + { + try + { - try - { - IterativeVariableRename visitor = new(request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, + IterativeFunctionRename visitor = new(funcDef.Name, + request.RenameTo, + funcDef.Extent.StartLineNumber, + funcDef.Extent.StartColumnNumber, scriptFile.ScriptAst); - if (visitor.TargetVariableAst == null) - { - result.message = "Failed to find variable definition within the current file"; + } + catch (FunctionDefinitionNotFoundException) + { + + result.message = "Failed to Find function definition within current file"; + } + + break; } - } - catch (TargetVariableIsDotSourcedException) - { - result.message = "Variable is dot sourced which is currently not supported unable to perform a rename"; - } + case VariableExpressionAst or CommandAst: + { + try + { + IterativeVariableRename visitor = new(request.RenameTo, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptFile.ScriptAst); + if (visitor.TargetVariableAst == null) + { + result.message = "Failed to find variable definition within the current file"; + } + } + catch (TargetVariableIsDotSourcedException) + { + + result.message = "Variable is dot sourced which is currently not supported unable to perform a rename"; + } + + break; + } } return result; From 8cc30d66be869ba78020accc36c284001ccbe25a Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:51:03 +0300 Subject: [PATCH 075/203] formatting for rename symbol --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 684d703ce..3309160ec 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -132,11 +132,15 @@ public async Task Handle(RenameSymbolParams request, Cancell Ast token = tokens.LastOrDefault(); if (token == null) { return null; } + ModifiedFileResponse FileModifications = token is FunctionDefinitionAst ? RenameFunction(token, scriptFile.ScriptAst, request) : RenameVariable(token, scriptFile.ScriptAst, request); + RenameSymbolResult result = new(); + result.Changes.Add(FileModifications); + return result; }).ConfigureAwait(false); } From 80370824d78e0b295f1853227ae9262eeaae0ac9 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:58:44 +0300 Subject: [PATCH 076/203] moved Function shared test content into its own folder --- .../Refactoring/{ => Functions}/BasicFunction.ps1 | 0 .../Refactoring/{ => Functions}/BasicFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/CmdletFunction.ps1 | 0 .../Refactoring/{ => Functions}/CmdletFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/ForeachFunction.ps1 | 0 .../Refactoring/{ => Functions}/ForeachFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/ForeachObjectFunction.ps1 | 0 .../{ => Functions}/ForeachObjectFunctionRenamed.ps1 | 0 .../{ => Functions}/FunctionCallWIthinStringExpression.ps1 | 0 .../FunctionCallWIthinStringExpressionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/InnerFunction.ps1 | 0 .../Refactoring/{ => Functions}/InnerFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/InternalCalls.ps1 | 0 .../Refactoring/{ => Functions}/InternalCallsRenamed.ps1 | 0 .../Refactoring/{ => Functions}/LoopFunction.ps1 | 0 .../Refactoring/{ => Functions}/LoopFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/MultipleOccurrences.ps1 | 0 .../{ => Functions}/MultipleOccurrencesRenamed.ps1 | 0 .../Refactoring/{ => Functions}/NestedFunctions.ps1 | 0 .../Refactoring/{ => Functions}/NestedFunctionsRenamed.ps1 | 0 .../Refactoring/{ => Functions}/OuterFunction.ps1 | 0 .../Refactoring/{ => Functions}/OuterFunctionRenamed.ps1 | 0 .../Refactoring/{ => Functions}/RefactorsFunctionData.cs | 2 +- .../Refactoring/{ => Functions}/SamenameFunctions.ps1 | 0 .../Refactoring/{ => Functions}/SamenameFunctionsRenamed.ps1 | 0 .../Refactoring/{ => Functions}/ScriptblockFunction.ps1 | 0 .../{ => Functions}/ScriptblockFunctionRenamed.ps1 | 0 .../Refactoring/RefactorFunctionTests.cs | 4 ++-- 28 files changed, 3 insertions(+), 3 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/BasicFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/BasicFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/CmdletFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/CmdletFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ForeachFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ForeachFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ForeachObjectFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ForeachObjectFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/FunctionCallWIthinStringExpression.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/FunctionCallWIthinStringExpressionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/InnerFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/InnerFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/InternalCalls.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/InternalCallsRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/LoopFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/LoopFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/MultipleOccurrences.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/MultipleOccurrencesRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/NestedFunctions.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/NestedFunctionsRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/OuterFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/OuterFunctionRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/RefactorsFunctionData.cs (97%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/SamenameFunctions.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/SamenameFunctionsRenamed.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ScriptblockFunction.ps1 (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/{ => Functions}/ScriptblockFunctionRenamed.ps1 (100%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/BasicFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/CmdletFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ForeachObjectFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCallWIthinStringExpression.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpression.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCallWIthinStringExpression.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCallWIthinStringExpressionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/FunctionCallWIthinStringExpressionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCallWIthinStringExpressionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/InnerFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCalls.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCalls.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCalls.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCallsRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/InternalCallsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCallsRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/LoopFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrences.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrences.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrences.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrencesRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/MultipleOccurrencesRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrencesRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctions.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctions.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctions.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctionsRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/NestedFunctionsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctionsRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/OuterFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs similarity index 97% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs index 8c06f0d14..7b6918795 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using Microsoft.PowerShell.EditorServices.Handlers; -namespace PowerShellEditorServices.Test.Shared.Refactoring +namespace PowerShellEditorServices.Test.Shared.Refactoring.Functions { internal static class RefactorsFunctionData { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctions.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctions.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctions.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctionsRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/SamenameFunctionsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctionsRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/ScriptblockFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 22e1a4bc5..0d8a5df4c 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -12,8 +12,8 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; using Microsoft.PowerShell.EditorServices.Services.Symbols; -using PowerShellEditorServices.Test.Shared.Refactoring; using Microsoft.PowerShell.EditorServices.Refactoring; +using PowerShellEditorServices.Test.Shared.Refactoring.Functions; namespace PowerShellEditorServices.Test.Refactoring { @@ -30,7 +30,7 @@ public void Dispose() #pragma warning restore VSTHRD002 GC.SuppressFinalize(this); } - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", fileName))); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Functions", fileName))); internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { From 22f0eb7741920f1b06639591fa009f0613462f8d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:29:35 +0300 Subject: [PATCH 077/203] New Test for splatted variable parameter renaming --- .../Variables/RefactorsVariablesData.cs | 14 ++++++++++++ .../VarableCommandParameterSplatted.ps1 | 15 +++++++++++++ ...VarableCommandParameterSplattedRenamed.ps1 | 15 +++++++++++++ .../Refactoring/RefactorVariableTests.cs | 22 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 7705bfe7a..2fcd2d3b5 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -135,5 +135,19 @@ internal static class RenameVariableData Line = 9, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VarableCommandParameterSplattedFromCommandAst = new() + { + FileName = "VarableCommandParameterSplatted.ps1", + Column = 10, + Line = 15, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VarableCommandParameterSplattedFromSplat = new() + { + FileName = "VarableCommandParameterSplatted.ps1", + Column = 5, + Line = 10, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 new file mode 100644 index 000000000..1bbbcc6bd --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 @@ -0,0 +1,15 @@ +function New-User { + param ( + [string]$Username, + [string]$password + ) + write-host $username + $password +} + +$UserDetailsSplat= @{ + Username = "JohnDoe" + Password = "SomePassword" +} +New-User @UserDetailsSplat + +New-User -Username "JohnDoe" -Password "SomePassword" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 new file mode 100644 index 000000000..a63fde3e5 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 @@ -0,0 +1,15 @@ +function New-User { + param ( + [string][Alias("Username")]$Renamed, + [string]$password + ) + write-host $Renamed + $password +} + +$UserDetailsSplat= @{ + Renamed = "JohnDoe" + Password = "SomePassword" +} +New-User @UserDetailsSplat + +New-User -Renamed "JohnDoe" -Password "SomePassword" diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index ff6cc401f..bd0c2c0de 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -253,6 +253,28 @@ public void VariableParameterCommandWithSameName() string modifiedcontent = TestRenaming(scriptFile, request); + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VarableCommandParameterSplattedFromCommandAst() + { + RenameSymbolParams request = RenameVariableData.VarableCommandParameterSplattedFromCommandAst; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VarableCommandParameterSplattedFromSplat() + { + RenameSymbolParams request = RenameVariableData.VarableCommandParameterSplattedFromSplat; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + Assert.Equal(expectedContent.Contents, modifiedcontent); } } From de4145435c02f842b2500d7842c43615836132da Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:29:56 +0300 Subject: [PATCH 078/203] first stage of supporting symbol renaming for splatted command Ast calls --- .../Refactoring/IterativeVariableVisitor.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 3261afe98..d307a1ca1 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -5,6 +5,7 @@ using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Handlers; using System.Linq; +using System; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -245,6 +246,31 @@ public void ProcessNode(Ast node) switch (node) { + case CommandAst commandAst: + // Is the Target Variable a Parameter and is this commandAst the target function + if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) + { + // Check to see if this is a splatted call to the target function. + Ast Splatted = null; + foreach (Ast element in commandAst.CommandElements) + { + if (element is VariableExpressionAst varAst && varAst.Splatted) + { + Splatted = varAst; + break; + } + } + if (Splatted != null) + { + NewSplattedModification(Splatted); + } + else + { + // The Target Variable is a Parameter and the commandAst is the Target Function + ShouldRename = true; + } + } + break; case CommandParameterAst commandParameterAst: if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) @@ -339,6 +365,41 @@ public void ProcessNode(Ast node) Log.Add($"ShouldRename after proc: {ShouldRename}"); } + internal void NewSplattedModification(Ast Splatted) + { + // Find the Splats Top Assignment / Definition + Ast SplatAssignment = GetVariableTopAssignment( + Splatted.Extent.StartLineNumber, + Splatted.Extent.StartColumnNumber, + ScriptAst); + // Look for the Parameter within the Splats HashTable + if (SplatAssignment.Parent is AssignmentStatementAst assignmentStatementAst && + assignmentStatementAst.Right is CommandExpressionAst commExpAst && + commExpAst.Expression is HashtableAst hashTableAst) + { + foreach (Tuple element in hashTableAst.KeyValuePairs) + { + if (element.Item1 is StringConstantExpressionAst strConstAst && + strConstAst.Value.ToLower() == OldName.ToLower()) + { + TextChange Change = new() + { + NewText = NewName, + StartLine = strConstAst.Extent.StartLineNumber - 1, + StartColumn = strConstAst.Extent.StartColumnNumber - 1, + EndLine = strConstAst.Extent.StartLineNumber - 1, + EndColumn = strConstAst.Extent.EndColumnNumber - 1, + }; + + Modifications.Add(Change); + break; + } + + } + + } + } + internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) { // Check if an Alias AttributeAst already exists and append the new Alias to the existing list From 56187f6124b8115f168227386a0c7a37c57d9d15 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:35:00 +0300 Subject: [PATCH 079/203] split out exceptions into generic file --- .../PowerShell/Refactoring/Exceptions.cs | 41 +++++++++++++++++++ .../PowerShell/Refactoring/VariableVisitor.cs | 34 --------------- 2 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs new file mode 100644 index 000000000..39a3fb1c0 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + + public class TargetSymbolNotFoundException : Exception + { + public TargetSymbolNotFoundException() + { + } + + public TargetSymbolNotFoundException(string message) + : base(message) + { + } + + public TargetSymbolNotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } + + public class TargetVariableIsDotSourcedException : Exception + { + public TargetVariableIsDotSourcedException() + { + } + + public TargetVariableIsDotSourcedException(string message) + : base(message) + { + } + + public TargetVariableIsDotSourcedException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs index 11f405f20..675960d24 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs @@ -10,40 +10,6 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { - public class TargetSymbolNotFoundException : Exception - { - public TargetSymbolNotFoundException() - { - } - - public TargetSymbolNotFoundException(string message) - : base(message) - { - } - - public TargetSymbolNotFoundException(string message, Exception inner) - : base(message, inner) - { - } - } - - public class TargetVariableIsDotSourcedException : Exception - { - public TargetVariableIsDotSourcedException() - { - } - - public TargetVariableIsDotSourcedException(string message) - : base(message) - { - } - - public TargetVariableIsDotSourcedException(string message, Exception inner) - : base(message, inner) - { - } - } - internal class VariableRename : ICustomAstVisitor2 { private readonly string OldName; From b119d5adec9260cd0521014ccc1f3ede494ff4de Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:35:16 +0300 Subject: [PATCH 080/203] updated to use its own internal version --- .../Services/PowerShell/Refactoring/IterativeFunctionVistor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 201a01abe..3981aa569 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -40,7 +40,7 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb } if (Node is CommandAst) { - TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + TargetFunctionAst = GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) { throw new FunctionDefinitionNotFoundException(); From 219599868e2eaabbb45bde4af0bfe1da47ffacea Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:35:34 +0300 Subject: [PATCH 081/203] added functionality to reverse lookup the top variable from a splat --- .../Refactoring/IterativeVariableVisitor.cs | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index d307a1ca1..801c9bf1a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -35,7 +35,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - VariableExpressionAst Node = (VariableExpressionAst)VariableRename.GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); + VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) { if (Node.Parent is ParameterAst) @@ -70,7 +70,7 @@ public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumn { return ast.Extent.StartLineNumber == StartLineNumber && ast.Extent.StartColumnNumber == StartColumnNumber && - ast is VariableExpressionAst or CommandParameterAst; + ast is VariableExpressionAst or CommandParameterAst or StringConstantExpressionAst; }, true); if (result == null) { @@ -85,17 +85,40 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN // Look up the target object Ast node = GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst); - string name = node is CommandParameterAst commdef - ? commdef.ParameterName - : node is VariableExpressionAst varDef ? varDef.VariablePath.UserPath : throw new TargetSymbolNotFoundException(); + string name = node switch + { + CommandParameterAst commdef => commdef.ParameterName, + VariableExpressionAst varDef => varDef.VariablePath.UserPath, + // Key within a Hashtable + StringConstantExpressionAst strExp => strExp.Value, + _ => throw new TargetSymbolNotFoundException() + }; + + VariableExpressionAst splatAssignment = null; + if (node is StringConstantExpressionAst) + { + Ast parent = node; + while (parent != null) + { + if (parent is AssignmentStatementAst assignmentStatementAst) + { + splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find(ast => ast is VariableExpressionAst, false); - Ast TargetParent = GetAstParentScope(node); + break; + } + parent = parent.Parent; + } + } + Ast TargetParent = GetAstParentScope(node); + // Find All Variables and Parameter Assignments with the same name before + // The node found above List VariableAssignments = ScriptAst.FindAll(ast => { return ast is VariableExpressionAst VarDef && VarDef.Parent is AssignmentStatementAst or ParameterAst && VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && + // Look Backwards from the node above (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); @@ -123,7 +146,7 @@ VarDef.Parent is AssignmentStatementAst or ParameterAst && CorrectDefinition = node; break; } - // node is proably just a reference of an assignment statement within the global scope or higher + // node is proably just a reference to an assignment statement or Parameter within the global scope or higher if (node.Parent is not AssignmentStatementAst) { if (null == parent || null == parent.Parent) @@ -132,8 +155,32 @@ VarDef.Parent is AssignmentStatementAst or ParameterAst && CorrectDefinition = element; break; } - if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst) + + if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst or StringConstantExpressionAst) { + if (node is StringConstantExpressionAst) + { + List SplatReferences = ScriptAst.FindAll(ast => + { + return ast is VariableExpressionAst varDef && + varDef.Splatted && + varDef.Parent is CommandAst && + varDef.VariablePath.UserPath.ToLower() == splatAssignment.VariablePath.UserPath.ToLower(); + }, true).Cast().ToList(); + + if (SplatReferences.Count >= 1) + { + CommandAst splatFirstRefComm = (CommandAst)SplatReferences.First().Parent; + if (funcDef.Name == splatFirstRefComm.GetCommandName() + && funcDef.Parent.Parent == TargetParent) + { + CorrectDefinition = element; + break; + } + } + } + + if (node.Parent is CommandAst commDef) { if (funcDef.Name == commDef.GetCommandName() From 664ffdb7a505e892624421755e168535def0086b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 17:15:00 +0300 Subject: [PATCH 082/203] Detter symbol detection was timing out on larger files --- .../PowerShell/Handlers/PrepareRenameSymbol.cs | 14 ++++++++------ .../Services/PowerShell/Handlers/RenameSymbol.cs | 16 +++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 630eec4b0..dcffa0950 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -54,15 +54,17 @@ public async Task Handle(PrepareRenameSymbolParams re { message = "" }; - - IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => + // ast is FunctionDefinitionAst or CommandAst or VariableExpressionAst or StringConstantExpressionAst && + Ast token = scriptFile.ScriptAst.Find(ast => { return request.Line + 1 == ast.Extent.StartLineNumber && request.Column + 1 >= ast.Extent.StartColumnNumber; - }, false); - - Ast token = tokens.LastOrDefault(); - + }, true); + IEnumerable tokens = token.FindAll(ast =>{ + return ast.Extent.StartColumnNumber <= request.Column && + ast.Extent.EndColumnNumber >= request.Column; + },true); + token = tokens.LastOrDefault(); if (token == null) { result.message = "Unable to find symbol"; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 3309160ec..135d29e97 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -12,7 +12,6 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; using System.Linq; - namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/renameSymbol")] @@ -123,13 +122,16 @@ public async Task Handle(RenameSymbolParams request, Cancell return await Task.Run(() => { - IEnumerable tokens = scriptFile.ScriptAst.FindAll(ast => + Ast token = scriptFile.ScriptAst.Find(ast => { - return request.Line+1 == ast.Extent.StartLineNumber && - request.Column+1 >= ast.Extent.StartColumnNumber; - }, false); - - Ast token = tokens.LastOrDefault(); + return request.Line + 1 == ast.Extent.StartLineNumber && + request.Column + 1 >= ast.Extent.StartColumnNumber; + }, true); + IEnumerable tokens = token.FindAll(ast =>{ + return ast.Extent.StartColumnNumber <= request.Column && + ast.Extent.EndColumnNumber >= request.Column; + },true); + token = tokens.LastOrDefault(); if (token == null) { return null; } From c33295502d14689e9633e1779138cdebc70bfc85 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:13:29 +0300 Subject: [PATCH 083/203] added utilities for common renaming functions updated tests --- .../PowerShell/Refactoring/Utilities.cs | 35 +++++ .../Refactoring/Utilities/TestDetection.ps1 | 21 +++ .../Variables/RefactorsVariablesData.cs | 2 +- .../VarableCommandParameterSplatted.ps1 | 6 + ...VarableCommandParameterSplattedRenamed.ps1 | 6 + .../Refactoring/RefactorUtilitiesTests.cs | 137 ++++++++++++++++++ 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetection.ps1 create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs new file mode 100644 index 000000000..b3e260e70 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Refactoring +{ + internal class Utilities + { + public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) + { + Ast token = null; + + token = Ast.Find(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + + IEnumerable tokens = token.FindAll(ast => + { + return ast.Extent.EndColumnNumber >= StartColumnNumber + && StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + if (tokens.Count() > 1) + { + token = tokens.LastOrDefault(); + } + return token; + } + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetection.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetection.ps1 new file mode 100644 index 000000000..d12a8652f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetection.ps1 @@ -0,0 +1,21 @@ +function New-User { + param ( + [string]$Username, + [string]$password + ) + write-host $username + $password + + $splat= @{ + Username = "JohnDeer" + Password = "SomePassword" + } + New-User @splat +} + +$UserDetailsSplat= @{ + Username = "JohnDoe" + Password = "SomePassword" +} +New-User @UserDetailsSplat + +New-User -Username "JohnDoe" -Password "SomePassword" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index 2fcd2d3b5..bb3bfdb09 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -146,7 +146,7 @@ internal static class RenameVariableData { FileName = "VarableCommandParameterSplatted.ps1", Column = 5, - Line = 10, + Line = 16, RenameTo = "Renamed" }; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 index 1bbbcc6bd..d12a8652f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 @@ -4,6 +4,12 @@ function New-User { [string]$password ) write-host $username + $password + + $splat= @{ + Username = "JohnDeer" + Password = "SomePassword" + } + New-User @splat } $UserDetailsSplat= @{ diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 index a63fde3e5..c799fd852 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 @@ -4,6 +4,12 @@ function New-User { [string]$password ) write-host $Renamed + $password + + $splat= @{ + Renamed = "JohnDeer" + Password = "SomePassword" + } + New-User @splat } $UserDetailsSplat= @{ diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs new file mode 100644 index 000000000..62bd60e56 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using Microsoft.PowerShell.EditorServices.Refactoring; +using System.Management.Automation.Language; + +namespace PowerShellEditorServices.Test.Refactoring +{ + [Trait("Category", "RefactorUtilities")] + public class RefactorUtilitiesTests : IDisposable + { + private readonly PsesInternalHost psesHost; + private readonly WorkspaceService workspace; + + public RefactorUtilitiesTests() + { + psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); + } + + public void Dispose() + { +#pragma warning disable VSTHRD002 + psesHost.StopAsync().Wait(); +#pragma warning restore VSTHRD002 + GC.SuppressFinalize(this); + } + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Utilities", fileName))); + + [Fact] + public void GetVariableExpressionAst() + { + RenameSymbolParams request = new(){ + Column=11, + Line=15, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(15,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetVariableExpressionStartAst() + { + RenameSymbolParams request = new(){ + Column=1, + Line=15, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(15,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetVariableWithinParameterAst() + { + RenameSymbolParams request = new(){ + Column=21, + Line=3, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(3,symbol.Extent.StartLineNumber); + Assert.Equal(17,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetHashTableKey() + { + RenameSymbolParams request = new(){ + Column=9, + Line=16, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(16,symbol.Extent.StartLineNumber); + Assert.Equal(5,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetVariableWithinCommandAst() + { + RenameSymbolParams request = new(){ + Column=29, + Line=6, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(6,symbol.Extent.StartLineNumber); + Assert.Equal(28,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetCommandParameterAst() + { + RenameSymbolParams request = new(){ + Column=12, + Line=21, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(21,symbol.Extent.StartLineNumber); + Assert.Equal(10,symbol.Extent.StartColumnNumber); + + } + } +} From acb9e0ac9ecd3c144f9d03c4d3246c306cb4ae20 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:13:49 +0300 Subject: [PATCH 084/203] adjusted rename to use utilities --- .../PowerShell/Handlers/PrepareRenameSymbol.cs | 18 +++++------------- .../PowerShell/Handlers/RenameSymbol.cs | 14 ++------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index dcffa0950..4733689ed 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -10,8 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; -using System.Collections.Generic; -using System.Linq; +using Microsoft.PowerShell.EditorServices.Services.Symbols; namespace Microsoft.PowerShell.EditorServices.Handlers { @@ -55,16 +54,9 @@ public async Task Handle(PrepareRenameSymbolParams re message = "" }; // ast is FunctionDefinitionAst or CommandAst or VariableExpressionAst or StringConstantExpressionAst && - Ast token = scriptFile.ScriptAst.Find(ast => - { - return request.Line + 1 == ast.Extent.StartLineNumber && - request.Column + 1 >= ast.Extent.StartColumnNumber; - }, true); - IEnumerable tokens = token.FindAll(ast =>{ - return ast.Extent.StartColumnNumber <= request.Column && - ast.Extent.EndColumnNumber >= request.Column; - },true); - token = tokens.LastOrDefault(); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); + Ast token = Utilities.GetAst(request.Line + 1,request.Column + 1,scriptFile.ScriptAst); + if (token == null) { result.message = "Unable to find symbol"; @@ -93,7 +85,7 @@ public async Task Handle(PrepareRenameSymbolParams re break; } - case VariableExpressionAst or CommandAst: + case VariableExpressionAst or CommandAst or CommandParameterAst or ParameterAst or StringConstantExpressionAst: { try diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 135d29e97..603e6a761 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; -using System.Linq; namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/renameSymbol")] @@ -94,7 +93,7 @@ internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, Re } internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameSymbolParams request) { - if (symbol is VariableExpressionAst or ParameterAst) + if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { IterativeVariableRename visitor = new(request.RenameTo, symbol.Extent.StartLineNumber, @@ -122,16 +121,7 @@ public async Task Handle(RenameSymbolParams request, Cancell return await Task.Run(() => { - Ast token = scriptFile.ScriptAst.Find(ast => - { - return request.Line + 1 == ast.Extent.StartLineNumber && - request.Column + 1 >= ast.Extent.StartColumnNumber; - }, true); - IEnumerable tokens = token.FindAll(ast =>{ - return ast.Extent.StartColumnNumber <= request.Column && - ast.Extent.EndColumnNumber >= request.Column; - },true); - token = tokens.LastOrDefault(); + Ast token = Utilities.GetAst(request.Line + 1,request.Column + 1,scriptFile.ScriptAst); if (token == null) { return null; } From 44a0499d60866bc4cf5c7fe46a97c33abe847697 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:38:33 +1100 Subject: [PATCH 085/203] added comments to NewSplattedModification --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 801c9bf1a..5f2ee2dc0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -414,6 +414,9 @@ public void ProcessNode(Ast node) internal void NewSplattedModification(Ast Splatted) { + // This Function should be passed a Splatted VariableExpressionAst which + // is used by a CommandAst that is the TargetFunction. + // Find the Splats Top Assignment / Definition Ast SplatAssignment = GetVariableTopAssignment( Splatted.Extent.StartLineNumber, @@ -443,7 +446,6 @@ assignmentStatementAst.Right is CommandExpressionAst commExpAst && } } - } } From 95c4540431c2832a2fd4ce5742c480e4aa789eb2 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:38:58 +1100 Subject: [PATCH 086/203] adjusted test to use -username param instead of -password due to Alias creation --- .../Refactoring/Variables/RefactorsVariablesData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs index bb3bfdb09..eab1415d1 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs @@ -139,7 +139,7 @@ internal static class RenameVariableData { FileName = "VarableCommandParameterSplatted.ps1", Column = 10, - Line = 15, + Line = 21, RenameTo = "Renamed" }; public static readonly RenameSymbolParams VarableCommandParameterSplattedFromSplat = new() From 03207232e8b6315189e7b9b94771d44a4e04b4c8 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:01:39 +1100 Subject: [PATCH 087/203] extracted method of LookForParentOfType from GetFuncDecFromCommAst --- .../Refactoring/IterativeFunctionVistor.cs | 20 ++++--------------- .../PowerShell/Refactoring/Utilities.cs | 16 +++++++++++++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 3981aa569..001984a2d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -31,7 +31,7 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + Ast Node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) { if (Node is FunctionDefinitionAst FuncDef) @@ -218,7 +218,6 @@ ast is CommandAst CommDef && CommDef.GetCommandName().ToLower() == OldName.ToLower(); }, true); } - return result; } @@ -248,12 +247,7 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i { return FunctionDefinitions[0]; } - // Sort function definitions - //FunctionDefinitions.Sort((a, b) => - //{ - // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - - // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; - //}); + // Determine which function definition is the right one FunctionDefinitionAst CorrectDefinition = null; for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) @@ -262,14 +256,8 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i Ast parent = element.Parent; // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } + parent = Utilities.LookForParentOfType(parent); + // we have hit the global scope of the script file if (null == parent) { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index b3e260e70..765774cb2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -9,6 +9,22 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring { internal class Utilities { + + public static Ast LookForParentOfType(Ast ast) + { + Ast parent = ast.Parent; + // walk backwards till we hit a parent of the specified type or return null + while (null != parent) + { + if (typeof(T) == parent.GetType()) + { + return parent; + } + parent = parent.Parent; + } + return null; + + } public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { Ast token = null; From 5ca399c7f647b181e63b10ffd1f2098c04880229 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:18:04 +1100 Subject: [PATCH 088/203] adjusted LookForParent so it accepts multiple types to look for --- .../Services/PowerShell/Refactoring/Utilities.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 765774cb2..d72e964e1 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; @@ -10,13 +11,13 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring internal class Utilities { - public static Ast LookForParentOfType(Ast ast) + public static Ast LookForParentOfType(Ast ast, params Type[] type) { - Ast parent = ast.Parent; + Ast parent = ast; // walk backwards till we hit a parent of the specified type or return null while (null != parent) { - if (typeof(T) == parent.GetType()) + if (type.Contains(parent.GetType())) { return parent; } From 4b2678034eeaf10261c0568f0a44b8e6f7dfbe2c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:18:21 +1100 Subject: [PATCH 089/203] adjusting iterative functions to use LookForParentOfType method --- .../Refactoring/IterativeFunctionVistor.cs | 2 +- .../Refactoring/IterativeVariableVisitor.cs | 33 +++++-------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 001984a2d..f44237333 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -256,7 +256,7 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i Ast parent = element.Parent; // walk backwards till we hit a functiondefinition if any - parent = Utilities.LookForParentOfType(parent); + parent = Utilities.LookForParentOfType(parent,typeof(FunctionDefinitionAst)); // we have hit the global scope of the script file if (null == parent) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 5f2ee2dc0..8f35d3c03 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -43,14 +43,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol isParam = true; Ast parent = Node; // Look for a target function that the parameterAst will be within if it exists - while (parent != null) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } + parent = Utilities.LookForParentOfType(parent,typeof(FunctionDefinitionAst)); if (parent != null) { TargetFunction = (FunctionDefinitionAst)parent; @@ -95,18 +88,15 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN }; VariableExpressionAst splatAssignment = null; + // A rename of a parameter has been initiated from a splat if (node is StringConstantExpressionAst) { Ast parent = node; - while (parent != null) + parent = Utilities.LookForParentOfType(parent,typeof(AssignmentStatementAst)); + if (parent is not null and AssignmentStatementAst assignmentStatementAst) { - if (parent is AssignmentStatementAst assignmentStatementAst) - { - splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find(ast => ast is VariableExpressionAst, false); - - break; - } - parent = parent.Parent; + splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( + ast => ast is VariableExpressionAst, false); } } @@ -205,15 +195,8 @@ varDef.Parent is CommandAst && internal static Ast GetAstParentScope(Ast node) { Ast parent = node; - // Walk backwards up the tree lookinf for a ScriptBLock of a FunctionDefinition - while (parent != null) - { - if (parent is ScriptBlockAst or FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } + // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition + parent = Utilities.LookForParentOfType(parent,typeof(ScriptBlockAst),typeof(FunctionDefinitionAst)); if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { parent = parent.Parent; From 2e704d2654067e14abdbe7a171f59a45ffbbb0d8 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:31:56 +1100 Subject: [PATCH 090/203] Moved GetAstNodeByLineAndColumn to a generic method --- .../Services/PowerShell/Refactoring/Utilities.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index d72e964e1..b78b4c5c5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -11,6 +11,22 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring internal class Utilities { + public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst,params Type[] type) + { + Ast result = null; + result = ScriptAst.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + type.Contains(ast.GetType()); + }, true); + if (result == null) + { + throw new TargetSymbolNotFoundException(); + } + return result; + } + public static Ast LookForParentOfType(Ast ast, params Type[] type) { Ast parent = ast; From d683f70fe65da29a55a89b7c681a18b1b1ae0aab Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:32:09 +1100 Subject: [PATCH 091/203] updated visitors to use generic GetAstNodeByLineAndColumn --- .../Refactoring/IterativeFunctionVistor.cs | 32 +++---------------- .../Refactoring/IterativeVariableVisitor.cs | 19 ++--------- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index f44237333..014f1c244 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -31,14 +31,15 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - Ast Node = GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + Ast Node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst, typeof(FunctionDefinitionAst), typeof(CommandAst)); + if (Node != null) { - if (Node is FunctionDefinitionAst FuncDef) + if (Node is FunctionDefinitionAst FuncDef && FuncDef.Name.ToLower() == OldName.ToLower()) { TargetFunctionAst = FuncDef; } - if (Node is CommandAst) + if (Node is CommandAst commdef && commdef.GetCommandName().ToLower() == OldName.ToLower()) { TargetFunctionAst = GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) @@ -196,31 +197,6 @@ public void ProcessNode(Ast node, bool shouldRename) Log.Add($"ShouldRename after proc: {shouldRename}"); } - public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - Ast result = null; - // Looking for a function - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower(); - }, true); - // Looking for a a Command call - if (null == result) - { - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower(); - }, true); - } - return result; - } - public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) { // Look up the targetted object diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 8f35d3c03..ba6801861 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -56,27 +56,12 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol } } - public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - Ast result = null; - result = ScriptAst.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is VariableExpressionAst or CommandParameterAst or StringConstantExpressionAst; - }, true); - if (result == null) - { - throw new TargetSymbolNotFoundException(); - } - return result; - } - public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { // Look up the target object - Ast node = GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst); + Ast node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, + ScriptAst,typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); string name = node switch { From 869414dbf4a1227024cf7b72db5d719d370b6c05 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:43:48 +1100 Subject: [PATCH 092/203] formatting moved GetFunctionDefByCommandAst to Utilities --- .../Refactoring/IterativeFunctionVistor.cs | 52 ----------- .../Refactoring/IterativeVariableVisitor.cs | 88 ------------------- .../PowerShell/Refactoring/Utilities.cs | 66 +++++++++++++- 3 files changed, 65 insertions(+), 141 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 014f1c244..f8b939c3a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -196,57 +196,5 @@ public void ProcessNode(Ast node, bool shouldRename) } Log.Add($"ShouldRename after proc: {shouldRename}"); } - - public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - // Look up the targetted object - CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => - { - return ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower() && - CommDef.Extent.StartLineNumber == StartLineNumber && - CommDef.Extent.StartColumnNumber == StartColumnNumber; - }, true); - - string FunctionName = TargetCommand.GetCommandName(); - - List FunctionDefinitions = ScriptFile.FindAll(ast => - { - return ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower() && - (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || - (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && - FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - - // Determine which function definition is the right one - FunctionDefinitionAst CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - parent = Utilities.LookForParentOfType(parent,typeof(FunctionDefinitionAst)); - - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (TargetCommand.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; - } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index ba6801861..5c3658012 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -459,93 +459,5 @@ internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpres return aliasChange; } - public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - Ast result = null; - // Looking for a function - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower(); - }, true); - // Looking for a a Command call - if (null == result) - { - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower(); - }, true); - } - - return result; - } - - public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - // Look up the targetted object - CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => - { - return ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower() && - CommDef.Extent.StartLineNumber == StartLineNumber && - CommDef.Extent.StartColumnNumber == StartColumnNumber; - }, true); - - string FunctionName = TargetCommand.GetCommandName(); - - List FunctionDefinitions = ScriptFile.FindAll(ast => - { - return ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower() && - (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || - (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && - FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Sort function definitions - //FunctionDefinitions.Sort((a, b) => - //{ - // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - - // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; - //}); - // Determine which function definition is the right one - FunctionDefinitionAst CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (TargetCommand.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; - } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index b78b4c5c5..3adcc8239 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -11,7 +11,7 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring internal class Utilities { - public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst,params Type[] type) + public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) { Ast result = null; result = ScriptAst.Find(ast => @@ -42,6 +42,70 @@ public static Ast LookForParentOfType(Ast ast, params Type[] type) return null; } + + public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + // Look up the targetted object + CommandAst TargetCommand = (CommandAst)Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptFile + , typeof(CommandAst)); + + if (TargetCommand.GetCommandName().ToLower() != OldName.ToLower()) + { + TargetCommand = null; + } + + string FunctionName = TargetCommand.GetCommandName(); + + List FunctionDefinitions = ScriptFile.FindAll(ast => + { + return ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower() && + (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || + (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && + FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Sort function definitions + //FunctionDefinitions.Sort((a, b) => + //{ + // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - + // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; + //}); + // Determine which function definition is the right one + FunctionDefinitionAst CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetCommand.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { Ast token = null; From 0dd2f1663430737e599f20610964afba3a51125c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:44:18 +1100 Subject: [PATCH 093/203] Refactoring to use Utilities class for generic methods --- .../PowerShell/Refactoring/IterativeFunctionVistor.cs | 6 +++--- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index f8b939c3a..5f2e7bebc 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Handlers; -using System.Linq; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -31,7 +30,8 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - Ast Node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst, typeof(FunctionDefinitionAst), typeof(CommandAst)); + Ast Node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst, + typeof(FunctionDefinitionAst), typeof(CommandAst)); if (Node != null) { @@ -41,7 +41,7 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb } if (Node is CommandAst commdef && commdef.GetCommandName().ToLower() == OldName.ToLower()) { - TargetFunctionAst = GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); + TargetFunctionAst = Utilities.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) { throw new FunctionDefinitionNotFoundException(); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 5c3658012..28f19c1f6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -43,7 +43,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol isParam = true; Ast parent = Node; // Look for a target function that the parameterAst will be within if it exists - parent = Utilities.LookForParentOfType(parent,typeof(FunctionDefinitionAst)); + parent = Utilities.LookForParentOfType(parent, typeof(FunctionDefinitionAst)); if (parent != null) { TargetFunction = (FunctionDefinitionAst)parent; @@ -61,7 +61,7 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN // Look up the target object Ast node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, - ScriptAst,typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); + ScriptAst, typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); string name = node switch { @@ -77,7 +77,7 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN if (node is StringConstantExpressionAst) { Ast parent = node; - parent = Utilities.LookForParentOfType(parent,typeof(AssignmentStatementAst)); + parent = Utilities.LookForParentOfType(parent, typeof(AssignmentStatementAst)); if (parent is not null and AssignmentStatementAst assignmentStatementAst) { splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( @@ -181,7 +181,7 @@ internal static Ast GetAstParentScope(Ast node) { Ast parent = node; // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.LookForParentOfType(parent,typeof(ScriptBlockAst),typeof(FunctionDefinitionAst)); + parent = Utilities.LookForParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst)); if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { parent = parent.Parent; From 5311641b6e95967377e8eb7eb41072685206da47 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:48:49 +1100 Subject: [PATCH 094/203] Renaming methods to be clearer --- .../PowerShell/Refactoring/IterativeFunctionVistor.cs | 2 +- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 8 ++++---- .../Services/PowerShell/Refactoring/Utilities.cs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 5f2e7bebc..21caa24d0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -30,7 +30,7 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - Ast Node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst, + Ast Node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptAst, typeof(FunctionDefinitionAst), typeof(CommandAst)); if (Node != null) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 28f19c1f6..1181cd159 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -43,7 +43,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol isParam = true; Ast parent = Node; // Look for a target function that the parameterAst will be within if it exists - parent = Utilities.LookForParentOfType(parent, typeof(FunctionDefinitionAst)); + parent = Utilities.GetAstParentOfType(parent, typeof(FunctionDefinitionAst)); if (parent != null) { TargetFunction = (FunctionDefinitionAst)parent; @@ -60,7 +60,7 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN { // Look up the target object - Ast node = Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, + Ast node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptAst, typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); string name = node switch @@ -77,7 +77,7 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN if (node is StringConstantExpressionAst) { Ast parent = node; - parent = Utilities.LookForParentOfType(parent, typeof(AssignmentStatementAst)); + parent = Utilities.GetAstParentOfType(parent, typeof(AssignmentStatementAst)); if (parent is not null and AssignmentStatementAst assignmentStatementAst) { splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( @@ -181,7 +181,7 @@ internal static Ast GetAstParentScope(Ast node) { Ast parent = node; // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.LookForParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst)); + parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst)); if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { parent = parent.Parent; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 3adcc8239..e791c51cc 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -11,7 +11,7 @@ namespace Microsoft.PowerShell.EditorServices.Refactoring internal class Utilities { - public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) + public static Ast GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) { Ast result = null; result = ScriptAst.Find(ast => @@ -27,7 +27,7 @@ public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumn return result; } - public static Ast LookForParentOfType(Ast ast, params Type[] type) + public static Ast GetAstParentOfType(Ast ast, params Type[] type) { Ast parent = ast; // walk backwards till we hit a parent of the specified type or return null @@ -46,7 +46,7 @@ public static Ast LookForParentOfType(Ast ast, params Type[] type) public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) { // Look up the targetted object - CommandAst TargetCommand = (CommandAst)Utilities.GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptFile + CommandAst TargetCommand = (CommandAst)Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptFile , typeof(CommandAst)); if (TargetCommand.GetCommandName().ToLower() != OldName.ToLower()) From ea75f46b0de090ffd1be989d03ce0e11efe4322c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:20:57 +1100 Subject: [PATCH 095/203] Added a test to get a function definition ast --- .../Refactoring/RefactorUtilitiesTests.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 62bd60e56..19a626597 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -11,8 +11,8 @@ using Microsoft.PowerShell.EditorServices.Test.Shared; using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; -using Microsoft.PowerShell.EditorServices.Refactoring; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Refactoring; namespace PowerShellEditorServices.Test.Refactoring { @@ -133,5 +133,21 @@ public void GetCommandParameterAst() Assert.Equal(10,symbol.Extent.StartColumnNumber); } + [Fact] + public void GetFunctionDefinitionAst() + { + RenameSymbolParams request = new(){ + Column=12, + Line=1, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(1,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); + + } } } From b8859fe827b44f62c49c90c8f1327b6dc7093c81 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:21:19 +1100 Subject: [PATCH 096/203] additional checks in getast for namedblock detection --- .../Services/PowerShell/Refactoring/Utilities.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index e791c51cc..81440121c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -117,6 +117,21 @@ public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) StartColumnNumber >= ast.Extent.StartColumnNumber; }, true); + if (token is NamedBlockAst) + { + return token.Parent; + } + + if (null == token) + { + IEnumerable LineT = Ast.FindAll(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + return LineT.OfType()?.LastOrDefault(); + } + IEnumerable tokens = token.FindAll(ast => { return ast.Extent.EndColumnNumber >= StartColumnNumber From 2ae405af37a3481ab1c128ab9dca7e3bf433b30e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 27 Oct 2023 22:40:04 +1100 Subject: [PATCH 097/203] formatting an new test for finding a function --- .../Refactoring/RefactorUtilitiesTests.cs | 136 ++++++++++-------- 1 file changed, 78 insertions(+), 58 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 19a626597..ab684ea2c 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -13,6 +13,9 @@ using Xunit; using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Refactoring; +using System.Management.Automation.Language; +using System.Collections.Generic; +using System.Linq; namespace PowerShellEditorServices.Test.Refactoring { @@ -40,114 +43,131 @@ public void Dispose() [Fact] public void GetVariableExpressionAst() { - RenameSymbolParams request = new(){ - Column=11, - Line=15, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 11, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(15,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(15, symbol.Extent.StartLineNumber); + Assert.Equal(1, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableExpressionStartAst() { - RenameSymbolParams request = new(){ - Column=1, - Line=15, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 1, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(15,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(15, symbol.Extent.StartLineNumber); + Assert.Equal(1, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinParameterAst() { - RenameSymbolParams request = new(){ - Column=21, - Line=3, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 21, + Line = 3, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(3,symbol.Extent.StartLineNumber); - Assert.Equal(17,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(3, symbol.Extent.StartLineNumber); + Assert.Equal(17, symbol.Extent.StartColumnNumber); } [Fact] public void GetHashTableKey() { - RenameSymbolParams request = new(){ - Column=9, - Line=16, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 9, + Line = 16, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(16,symbol.Extent.StartLineNumber); - Assert.Equal(5,symbol.Extent.StartColumnNumber); + List Tokens = scriptFile.ScriptTokens.Cast().ToList(); + IEnumerable Found = Tokens.FindAll(e => + { + return e.Extent.StartLineNumber == request.Line && + e.Extent.StartColumnNumber <= request.Column && + e.Extent.EndColumnNumber >= request.Column; + }); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(16, symbol.Extent.StartLineNumber); + Assert.Equal(5, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinCommandAst() { - RenameSymbolParams request = new(){ - Column=29, - Line=6, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 29, + Line = 6, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(6,symbol.Extent.StartLineNumber); - Assert.Equal(28,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(6, symbol.Extent.StartLineNumber); + Assert.Equal(28, symbol.Extent.StartColumnNumber); } [Fact] public void GetCommandParameterAst() { - RenameSymbolParams request = new(){ - Column=12, - Line=21, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 12, + Line = 21, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(21,symbol.Extent.StartLineNumber); - Assert.Equal(10,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(21, symbol.Extent.StartLineNumber); + Assert.Equal(10, symbol.Extent.StartColumnNumber); } [Fact] public void GetFunctionDefinitionAst() { - RenameSymbolParams request = new(){ - Column=12, - Line=1, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 16, + Line = 1, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(1,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); - + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(1, symbol.Extent.StartLineNumber); + Assert.Equal(1, symbol.Extent.StartColumnNumber); } } } From dc9005ae374191a2d086fcc06b8cd58d7f8b01f9 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 27 Oct 2023 22:41:15 +1100 Subject: [PATCH 098/203] reworked GetAst for better detection --- .../PowerShell/Refactoring/Utilities.cs | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 81440121c..7e67246e8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -108,40 +108,42 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { - Ast token = null; - token = Ast.Find(ast => + // Get all the tokens on the startline so we can look for an appropriate Ast to return + IEnumerable tokens = Ast.FindAll(ast => { - return StartLineNumber == ast.Extent.StartLineNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; + return StartLineNumber == ast.Extent.StartLineNumber; }, true); - - if (token is NamedBlockAst) - { - return token.Parent; - } - - if (null == token) + // Check if the Ast is a FunctionDefinitionAst + IEnumerable Functions = tokens.OfType(); + if (Functions.Any()) { - IEnumerable LineT = Ast.FindAll(ast => + foreach (FunctionDefinitionAst Function in Functions) { - return StartLineNumber == ast.Extent.StartLineNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - return LineT.OfType()?.LastOrDefault(); + if (Function.Extent.StartLineNumber != Function.Extent.EndLineNumber) + { + return Function; + } + } } - IEnumerable tokens = token.FindAll(ast => + IEnumerable token = null; + token = Ast.FindAll(ast => { - return ast.Extent.EndColumnNumber >= StartColumnNumber - && StartColumnNumber >= ast.Extent.StartColumnNumber; + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber <= StartColumnNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber; }, true); - if (tokens.Count() > 1) + if (token != null) { - token = tokens.LastOrDefault(); + if (token.First() is AssignmentStatementAst Assignment) + { + return Assignment.Left; + } + return token.Last(); } - return token; + + return token.First(); } } } From e817c0e43a7f17b4fe1de18d075886f2e087efd6 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:30:10 +1100 Subject: [PATCH 099/203] additional changes to getast utility method --- .../PowerShell/Refactoring/Utilities.cs | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 7e67246e8..cba8ce3e1 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -108,42 +108,26 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { + Ast token = null; - // Get all the tokens on the startline so we can look for an appropriate Ast to return - IEnumerable tokens = Ast.FindAll(ast => + token = Ast.Find(ast => { - return StartLineNumber == ast.Extent.StartLineNumber; + return StartLineNumber == ast.Extent.StartLineNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; }, true); - // Check if the Ast is a FunctionDefinitionAst - IEnumerable Functions = tokens.OfType(); - if (Functions.Any()) - { - foreach (FunctionDefinitionAst Function in Functions) - { - if (Function.Extent.StartLineNumber != Function.Extent.EndLineNumber) - { - return Function; - } - } - } IEnumerable token = null; token = Ast.FindAll(ast => { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber <= StartColumnNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber; + return ast.Extent.EndColumnNumber >= StartColumnNumber + && StartColumnNumber >= ast.Extent.StartColumnNumber; }, true); - if (token != null) + if (tokens.Count() > 1) { - if (token.First() is AssignmentStatementAst Assignment) - { - return Assignment.Left; - } - return token.Last(); + token = tokens.LastOrDefault(); } - - return token.First(); + return token; } } } From e3ce34364be720e7d630e40c41bee79f56a11871 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 21 Mar 2024 13:34:33 +1100 Subject: [PATCH 100/203] reverting changes from bad merge request pull --- .../Refactoring/RefactorUtilitiesTests.cs | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index ab684ea2c..78af55904 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -43,113 +43,113 @@ public void Dispose() [Fact] public void GetVariableExpressionAst() { - RenameSymbolParams request = new() - { - Column = 11, - Line = 15, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=11, + Line=15, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); - Assert.Equal(15, symbol.Extent.StartLineNumber); - Assert.Equal(1, symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(15,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableExpressionStartAst() { - RenameSymbolParams request = new() - { - Column = 1, - Line = 15, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=1, + Line=15, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); - Assert.Equal(15, symbol.Extent.StartLineNumber); - Assert.Equal(1, symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(15,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinParameterAst() { - RenameSymbolParams request = new() - { - Column = 21, - Line = 3, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=21, + Line=3, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); - Assert.Equal(3, symbol.Extent.StartLineNumber); - Assert.Equal(17, symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(3,symbol.Extent.StartLineNumber); + Assert.Equal(17,symbol.Extent.StartColumnNumber); } [Fact] public void GetHashTableKey() { - RenameSymbolParams request = new() - { - Column = 9, - Line = 16, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=9, + Line=16, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - List Tokens = scriptFile.ScriptTokens.Cast().ToList(); - IEnumerable Found = Tokens.FindAll(e => - { - return e.Extent.StartLineNumber == request.Line && - e.Extent.StartColumnNumber <= request.Column && - e.Extent.EndColumnNumber >= request.Column; - }); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(16, symbol.Extent.StartLineNumber); - Assert.Equal(5, symbol.Extent.StartColumnNumber); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(16,symbol.Extent.StartLineNumber); + Assert.Equal(5,symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinCommandAst() { - RenameSymbolParams request = new() - { - Column = 29, - Line = 6, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=29, + Line=6, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(6, symbol.Extent.StartLineNumber); - Assert.Equal(28, symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(6,symbol.Extent.StartLineNumber); + Assert.Equal(28,symbol.Extent.StartColumnNumber); } [Fact] public void GetCommandParameterAst() { - RenameSymbolParams request = new() - { - Column = 12, - Line = 21, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" + RenameSymbolParams request = new(){ + Column=12, + Line=21, + RenameTo="Renamed", + FileName="TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); - Assert.Equal(21, symbol.Extent.StartLineNumber); - Assert.Equal(10, symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(21,symbol.Extent.StartLineNumber); + Assert.Equal(10,symbol.Extent.StartColumnNumber); + + } + [Fact] + public void GetFunctionDefinitionAst() + { + RenameSymbolParams request = new(){ + Column=12, + Line=1, + RenameTo="Renamed", + FileName="TestDetection.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.Equal(1,symbol.Extent.StartLineNumber); + Assert.Equal(1,symbol.Extent.StartColumnNumber); } [Fact] From 6daf20ef6b47c168ab5ab51c52b960cf034555a0 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:33:58 +1100 Subject: [PATCH 101/203] removing unused properties of the class --- .../Refactoring/IterativeFunctionVistor.cs | 5 ----- .../Refactoring/IterativeVariableVisitor.cs | 16 ++++------------ 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 21caa24d0..45545493a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -15,7 +15,6 @@ internal class IterativeFunctionRename internal Queue queue = new(); internal bool ShouldRename; public List Modifications = new(); - public List Log = new(); internal int StartLineNumber; internal int StartColumnNumber; internal FunctionDefinitionAst TargetFunctionAst; @@ -143,9 +142,6 @@ public void Visit(Ast root) public void ProcessNode(Ast node, bool shouldRename) { - Log.Add($"Proc node: {node.GetType().Name}, " + - $"SL: {node.Extent.StartLineNumber}, " + - $"SC: {node.Extent.StartColumnNumber}"); switch (node) { @@ -194,7 +190,6 @@ public void ProcessNode(Ast node, bool shouldRename) } break; } - Log.Add($"ShouldRename after proc: {shouldRename}"); } } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 1181cd159..c60bdd363 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -14,19 +14,16 @@ internal class IterativeVariableRename { private readonly string OldName; private readonly string NewName; - internal Stack ScopeStack = new(); internal bool ShouldRename; public List Modifications = new(); internal int StartLineNumber; internal int StartColumnNumber; internal VariableExpressionAst TargetVariableAst; - internal VariableExpressionAst DuplicateVariableAst; internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; internal bool isParam; internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; - internal List Log = new(); public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) { @@ -255,9 +252,6 @@ public void Visit(Ast root) public void ProcessNode(Ast node) { - Log.Add($"Proc node: {node.GetType().Name}, " + - $"SL: {node.Extent.StartLineNumber}, " + - $"SC: {node.Extent.StartColumnNumber}"); switch (node) { @@ -343,7 +337,6 @@ public void ProcessNode(Ast node) { if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) { - DuplicateVariableAst = variableExpressionAst; ShouldRename = false; } @@ -377,15 +370,14 @@ public void ProcessNode(Ast node) } break; } - Log.Add($"ShouldRename after proc: {ShouldRename}"); } internal void NewSplattedModification(Ast Splatted) { - // This Function should be passed a Splatted VariableExpressionAst which + // This Function should be passed a splatted VariableExpressionAst which // is used by a CommandAst that is the TargetFunction. - // Find the Splats Top Assignment / Definition + // Find the splats top assignment / definition Ast SplatAssignment = GetVariableTopAssignment( Splatted.Extent.StartLineNumber, Splatted.Extent.StartColumnNumber, @@ -421,8 +413,8 @@ internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpres { // Check if an Alias AttributeAst already exists and append the new Alias to the existing list // Otherwise Create a new Alias Attribute - // Add the modidifcations to the changes - // the Attribute will be appended before the variable or in the existing location of the Original Alias + // Add the modifications to the changes + // The Attribute will be appended before the variable or in the existing location of the original alias TextChange aliasChange = new(); foreach (Ast Attr in paramAst.Attributes) { From 828c3a586c18ceac98bd52eb0acd1ec09fba563d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:34:28 +1100 Subject: [PATCH 102/203] migrated FunctionDefinitionNotFoundException to exceptions.cs --- .../PowerShell/Refactoring/Exceptions.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs index 39a3fb1c0..230e136b7 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs @@ -38,4 +38,21 @@ public TargetVariableIsDotSourcedException(string message, Exception inner) { } } + + public class FunctionDefinitionNotFoundException : Exception + { + public FunctionDefinitionNotFoundException() + { + } + + public FunctionDefinitionNotFoundException(string message) + : base(message) + { + } + + public FunctionDefinitionNotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } } From 700c5fed85065613ff7b242c2e31a309bac651e1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:34:39 +1100 Subject: [PATCH 103/203] removed original recursive visitor classes --- .../PowerShell/Refactoring/FunctionVistor.cs | 385 ------------- .../PowerShell/Refactoring/VariableVisitor.cs | 537 ------------------ 2 files changed, 922 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs deleted file mode 100644 index fcc491256..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/FunctionVistor.cs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; -using System; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Refactoring -{ - - - public class FunctionDefinitionNotFoundException : Exception - { - public FunctionDefinitionNotFoundException() - { - } - - public FunctionDefinitionNotFoundException(string message) - : base(message) - { - } - - public FunctionDefinitionNotFoundException(string message, Exception inner) - : base(message, inner) - { - } - } - - - internal class FunctionRename : ICustomAstVisitor2 - { - private readonly string OldName; - private readonly string NewName; - internal Stack ScopeStack = new(); - internal bool ShouldRename; - public List Modifications = new(); - internal int StartLineNumber; - internal int StartColumnNumber; - internal FunctionDefinitionAst TargetFunctionAst; - internal FunctionDefinitionAst DuplicateFunctionAst; - internal readonly Ast ScriptAst; - - public FunctionRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - this.OldName = OldName; - this.NewName = NewName; - this.StartLineNumber = StartLineNumber; - this.StartColumnNumber = StartColumnNumber; - this.ScriptAst = ScriptAst; - - Ast Node = FunctionRename.GetAstNodeByLineAndColumn(OldName, StartLineNumber, StartColumnNumber, ScriptAst); - if (Node != null) - { - if (Node is FunctionDefinitionAst FuncDef) - { - TargetFunctionAst = FuncDef; - } - if (Node is CommandAst) - { - TargetFunctionAst = FunctionRename.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); - if (TargetFunctionAst == null) - { - throw new FunctionDefinitionNotFoundException(); - } - this.StartColumnNumber = TargetFunctionAst.Extent.StartColumnNumber; - this.StartLineNumber = TargetFunctionAst.Extent.StartLineNumber; - } - } - } - public static Ast GetAstNodeByLineAndColumn(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - Ast result = null; - // Looking for a function - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower(); - }, true); - // Looking for a a Command call - if (null == result) - { - result = ScriptFile.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower(); - }, true); - } - - return result; - } - - public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - // Look up the targetted object - CommandAst TargetCommand = (CommandAst)ScriptFile.Find(ast => - { - return ast is CommandAst CommDef && - CommDef.GetCommandName().ToLower() == OldName.ToLower() && - CommDef.Extent.StartLineNumber == StartLineNumber && - CommDef.Extent.StartColumnNumber == StartColumnNumber; - }, true); - - string FunctionName = TargetCommand.GetCommandName(); - - List FunctionDefinitions = ScriptFile.FindAll(ast => - { - return ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower() && - (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || - (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && - FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Sort function definitions - //FunctionDefinitions.Sort((a, b) => - //{ - // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - - // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; - //}); - // Determine which function definition is the right one - FunctionDefinitionAst CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (TargetCommand.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; - } - - public object VisitFunctionDefinition(FunctionDefinitionAst ast) - { - ScopeStack.Push("function_" + ast.Name); - - if (ast.Name == OldName) - { - if (ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber) - { - TargetFunctionAst = ast; - TextChange Change = new() - { - NewText = NewName, - StartLine = ast.Extent.StartLineNumber - 1, - StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, - EndLine = ast.Extent.StartLineNumber - 1, - EndColumn = ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1, - }; - - Modifications.Add(Change); - ShouldRename = true; - } - else - { - // Entering a duplicate functions scope and shouldnt rename - ShouldRename = false; - DuplicateFunctionAst = ast; - } - } - ast.Body.Visit(this); - - ScopeStack.Pop(); - return null; - } - - public object VisitLoopStatement(LoopStatementAst ast) - { - - ScopeStack.Push("Loop"); - - ast.Body.Visit(this); - - ScopeStack.Pop(); - return null; - } - - public object VisitScriptBlock(ScriptBlockAst ast) - { - ScopeStack.Push("scriptblock"); - - ast.BeginBlock?.Visit(this); - ast.ProcessBlock?.Visit(this); - ast.EndBlock?.Visit(this); - ast.DynamicParamBlock?.Visit(this); - - if (ShouldRename && TargetFunctionAst.Parent.Parent == ast) - { - ShouldRename = false; - } - - if (DuplicateFunctionAst?.Parent.Parent == ast) - { - ShouldRename = true; - } - ScopeStack.Pop(); - - return null; - } - - public object VisitPipeline(PipelineAst ast) - { - foreach (Ast element in ast.PipelineElements) - { - element.Visit(this); - } - return null; - } - public object VisitAssignmentStatement(AssignmentStatementAst ast) - { - ast.Right.Visit(this); - ast.Left.Visit(this); - return null; - } - public object VisitStatementBlock(StatementBlockAst ast) - { - foreach (StatementAst element in ast.Statements) - { - element.Visit(this); - } - - if (DuplicateFunctionAst?.Parent == ast) - { - ShouldRename = true; - } - - return null; - } - public object VisitForStatement(ForStatementAst ast) - { - ast.Body.Visit(this); - return null; - } - public object VisitIfStatement(IfStatementAst ast) - { - foreach (Tuple element in ast.Clauses) - { - element.Item1.Visit(this); - element.Item2.Visit(this); - } - - ast.ElseClause?.Visit(this); - - return null; - } - public object VisitForEachStatement(ForEachStatementAst ast) - { - ast.Body.Visit(this); - return null; - } - public object VisitCommandExpression(CommandExpressionAst ast) - { - ast.Expression.Visit(this); - return null; - } - public object VisitScriptBlockExpression(ScriptBlockExpressionAst ast) - { - ast.ScriptBlock.Visit(this); - return null; - } - public object VisitNamedBlock(NamedBlockAst ast) - { - foreach (StatementAst element in ast.Statements) - { - element.Visit(this); - } - return null; - } - public object VisitCommand(CommandAst ast) - { - if (ast.GetCommandName() == OldName) - { - if (ShouldRename) - { - TextChange Change = new() - { - NewText = NewName, - StartLine = ast.Extent.StartLineNumber - 1, - StartColumn = ast.Extent.StartColumnNumber - 1, - EndLine = ast.Extent.StartLineNumber - 1, - EndColumn = ast.Extent.StartColumnNumber + OldName.Length - 1, - }; - Modifications.Add(Change); - } - } - foreach (CommandElementAst element in ast.CommandElements) - { - element.Visit(this); - } - - return null; - } - - public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => null; - public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => null; - public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => null; - public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => null; - public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => null; - public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => null; - public object VisitUsingStatement(UsingStatementAst usingStatement) => null; - public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => null; - public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) => null; - public object VisitAttribute(AttributeAst attributeAst) => null; - public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => null; - public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) => null; - public object VisitBlockStatement(BlockStatementAst blockStatementAst) => null; - public object VisitBreakStatement(BreakStatementAst breakStatementAst) => null; - public object VisitCatchClause(CatchClauseAst catchClauseAst) => null; - public object VisitCommandParameter(CommandParameterAst commandParameterAst) => null; - public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; - public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => null; - public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) => null; - public object VisitDataStatement(DataStatementAst dataStatementAst) => null; - public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) => null; - public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) => null; - public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => null; - public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => null; - public object VisitExitStatement(ExitStatementAst exitStatementAst) => null; - public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) - { - - foreach (ExpressionAst element in expandableStringExpressionAst.NestedExpressions) - { - element.Visit(this); - } - return null; - } - public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => null; - public object VisitHashtable(HashtableAst hashtableAst) => null; - public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) => null; - public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => null; - public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) => null; - public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => null; - public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => null; - public object VisitParamBlock(ParamBlockAst paramBlockAst) => null; - public object VisitParameter(ParameterAst parameterAst) => null; - public object VisitParenExpression(ParenExpressionAst parenExpressionAst) => null; - public object VisitReturnStatement(ReturnStatementAst returnStatementAst) => null; - public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; - public object VisitSubExpression(SubExpressionAst subExpressionAst) - { - subExpressionAst.SubExpression.Visit(this); - return null; - } - public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => null; - public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => null; - public object VisitTrap(TrapStatementAst trapStatementAst) => null; - public object VisitTryStatement(TryStatementAst tryStatementAst) => null; - public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null; - public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => null; - public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => null; - public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => null; - public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) => null; - public object VisitWhileStatement(WhileStatementAst whileStatementAst) => null; - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs deleted file mode 100644 index 675960d24..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/VariableVisitor.cs +++ /dev/null @@ -1,537 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; -using System; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Refactoring -{ - - internal class VariableRename : ICustomAstVisitor2 - { - private readonly string OldName; - private readonly string NewName; - internal Stack ScopeStack = new(); - internal bool ShouldRename; - public List Modifications = new(); - internal int StartLineNumber; - internal int StartColumnNumber; - internal VariableExpressionAst TargetVariableAst; - internal VariableExpressionAst DuplicateVariableAst; - internal List dotSourcedScripts = new(); - internal readonly Ast ScriptAst; - internal bool isParam; - - public VariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - this.NewName = NewName; - this.StartLineNumber = StartLineNumber; - this.StartColumnNumber = StartColumnNumber; - this.ScriptAst = ScriptAst; - - VariableExpressionAst Node = (VariableExpressionAst)VariableRename.GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); - if (Node != null) - { - if (Node.Parent is ParameterAst) - { - isParam = true; - } - TargetVariableAst = Node; - OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); - this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; - this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; - } - } - - public static Ast GetAstNodeByLineAndColumn(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - Ast result = null; - result = ScriptAst.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - ast is VariableExpressionAst or CommandParameterAst; - }, true); - if (result == null) - { - throw new TargetSymbolNotFoundException(); - } - return result; - } - public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - - // Look up the target object - Ast node = GetAstNodeByLineAndColumn(StartLineNumber, StartColumnNumber, ScriptAst); - - string name = node is CommandParameterAst commdef - ? commdef.ParameterName - : node is VariableExpressionAst varDef ? varDef.VariablePath.UserPath : throw new TargetSymbolNotFoundException(); - - Ast TargetParent = GetAstParentScope(node); - - List VariableAssignments = ScriptAst.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the def if we have no matches - if (VariableAssignments.Count == 0) - { - return node; - } - Ast CorrectDefinition = null; - for (int i = VariableAssignments.Count - 1; i >= 0; i--) - { - VariableExpressionAst element = VariableAssignments[i]; - - Ast parent = GetAstParentScope(element); - // closest assignment statement is within the scope of the node - if (TargetParent == parent) - { - CorrectDefinition = element; - break; - } - else if (node.Parent is AssignmentStatementAst) - { - // the node is probably the first assignment statement within the scope - CorrectDefinition = node; - break; - } - // node is proably just a reference of an assignment statement within the global scope or higher - if (node.Parent is not AssignmentStatementAst) - { - if (null == parent || null == parent.Parent) - { - // we have hit the global scope of the script file - CorrectDefinition = element; - break; - } - if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst) - { - if (node.Parent is CommandAst commDef) - { - if (funcDef.Name == commDef.GetCommandName() - && funcDef.Parent.Parent == TargetParent) - { - CorrectDefinition = element; - break; - } - } - } - if (WithinTargetsScope(element, node)) - { - CorrectDefinition = element; - } - } - - - } - return CorrectDefinition ?? node; - } - - internal static Ast GetAstParentScope(Ast node) - { - Ast parent = node; - // Walk backwards up the tree look - while (parent != null) - { - if (parent is ScriptBlockAst or FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) - { - parent = parent.Parent; - } - return parent; - } - - internal static bool WithinTargetsScope(Ast Target, Ast Child) - { - bool r = false; - Ast childParent = Child.Parent; - Ast TargetScope = GetAstParentScope(Target); - while (childParent != null) - { - if (childParent is FunctionDefinitionAst) - { - break; - } - if (childParent == TargetScope) - { - break; - } - childParent = childParent.Parent; - } - if (childParent == TargetScope) - { - r = true; - } - return r; - } - public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) => throw new NotImplementedException(); - public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) - { - foreach (ExpressionAst element in arrayLiteralAst.Elements) - { - element.Visit(this); - } - return null; - } - public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) - { - assignmentStatementAst.Left.Visit(this); - assignmentStatementAst.Right.Visit(this); - return null; - } - public object VisitAttribute(AttributeAst attributeAst) - { - attributeAst.Visit(this); - return null; - } - public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) => throw new NotImplementedException(); - public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) - { - binaryExpressionAst.Left.Visit(this); - binaryExpressionAst.Right.Visit(this); - - return null; - } - public object VisitBlockStatement(BlockStatementAst blockStatementAst) => throw new NotImplementedException(); - public object VisitBreakStatement(BreakStatementAst breakStatementAst) => throw new NotImplementedException(); - public object VisitCatchClause(CatchClauseAst catchClauseAst) => throw new NotImplementedException(); - public object VisitCommand(CommandAst commandAst) - { - - // Check for dot sourcing - // TODO Handle the dot sourcing after detection - if (commandAst.InvocationOperator == TokenKind.Dot && commandAst.CommandElements.Count > 1) - { - if (commandAst.CommandElements[1] is StringConstantExpressionAst scriptPath) - { - dotSourcedScripts.Add(scriptPath.Value); - throw new TargetVariableIsDotSourcedException(); - } - } - - foreach (CommandElementAst element in commandAst.CommandElements) - { - element.Visit(this); - } - - return null; - } - public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) - { - commandExpressionAst.Expression.Visit(this); - return null; - } - public object VisitCommandParameter(CommandParameterAst commandParameterAst) - { - // TODO implement command parameter renaming - if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) - { - if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && - commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) - { - ShouldRename = true; - } - - if (ShouldRename && isParam) - { - TextChange Change = new() - { - NewText = NewName.Contains("-") ? NewName : "-" + NewName, - StartLine = commandParameterAst.Extent.StartLineNumber - 1, - StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, - EndLine = commandParameterAst.Extent.StartLineNumber - 1, - EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, - }; - - Modifications.Add(Change); - } - } - return null; - } - public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) => throw new NotImplementedException(); - public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) => null; - public object VisitContinueStatement(ContinueStatementAst continueStatementAst) => throw new NotImplementedException(); - public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) - { - // TODO figure out if there is a case to visit the type - //convertExpressionAst.Type.Visit(this); - convertExpressionAst.Child.Visit(this); - return null; - } - public object VisitDataStatement(DataStatementAst dataStatementAst) => throw new NotImplementedException(); - public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) - { - doUntilStatementAst.Condition.Visit(this); - ScopeStack.Push(doUntilStatementAst); - doUntilStatementAst.Body.Visit(this); - ScopeStack.Pop(); - return null; - } - public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) - { - doWhileStatementAst.Condition.Visit(this); - ScopeStack.Push(doWhileStatementAst); - doWhileStatementAst.Body.Visit(this); - ScopeStack.Pop(); - return null; - } - public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordAst) => throw new NotImplementedException(); - public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) => throw new NotImplementedException(); - public object VisitErrorStatement(ErrorStatementAst errorStatementAst) => throw new NotImplementedException(); - public object VisitExitStatement(ExitStatementAst exitStatementAst) => throw new NotImplementedException(); - public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) - { - - foreach (ExpressionAst element in expandableStringExpressionAst.NestedExpressions) - { - element.Visit(this); - } - return null; - } - public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) => throw new NotImplementedException(); - public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) - { - ScopeStack.Push(forEachStatementAst); - forEachStatementAst.Body.Visit(this); - ScopeStack.Pop(); - return null; - } - public object VisitForStatement(ForStatementAst forStatementAst) - { - forStatementAst.Condition.Visit(this); - ScopeStack.Push(forStatementAst); - forStatementAst.Body.Visit(this); - ScopeStack.Pop(); - return null; - } - public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - ScopeStack.Push(functionDefinitionAst); - if (null != functionDefinitionAst.Parameters) - { - foreach (ParameterAst element in functionDefinitionAst.Parameters) - { - element.Visit(this); - } - } - functionDefinitionAst.Body.Visit(this); - - ScopeStack.Pop(); - return null; - } - public object VisitFunctionMember(FunctionMemberAst functionMemberAst) => throw new NotImplementedException(); - public object VisitHashtable(HashtableAst hashtableAst) - { - foreach (Tuple element in hashtableAst.KeyValuePairs) - { - element.Item1.Visit(this); - element.Item2.Visit(this); - } - return null; - } - public object VisitIfStatement(IfStatementAst ifStmtAst) - { - foreach (Tuple element in ifStmtAst.Clauses) - { - element.Item1.Visit(this); - element.Item2.Visit(this); - } - - ifStmtAst.ElseClause?.Visit(this); - - return null; - } - public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) { - indexExpressionAst.Target.Visit(this); - indexExpressionAst.Index.Visit(this); - return null; - } - public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) => throw new NotImplementedException(); - public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) - { - memberExpressionAst.Expression.Visit(this); - return null; - } - public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) => throw new NotImplementedException(); - public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) => throw new NotImplementedException(); - public object VisitNamedBlock(NamedBlockAst namedBlockAst) - { - foreach (StatementAst element in namedBlockAst.Statements) - { - element.Visit(this); - } - return null; - } - public object VisitParamBlock(ParamBlockAst paramBlockAst) - { - foreach (ParameterAst element in paramBlockAst.Parameters) - { - element.Visit(this); - } - return null; - } - public object VisitParameter(ParameterAst parameterAst) - { - parameterAst.Name.Visit(this); - foreach (AttributeBaseAst element in parameterAst.Attributes) - { - element.Visit(this); - } - return null; - } - public object VisitParenExpression(ParenExpressionAst parenExpressionAst) - { - parenExpressionAst.Pipeline.Visit(this); - return null; - } - public object VisitPipeline(PipelineAst pipelineAst) - { - foreach (Ast element in pipelineAst.PipelineElements) - { - element.Visit(this); - } - return null; - } - public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) => throw new NotImplementedException(); - public object VisitReturnStatement(ReturnStatementAst returnStatementAst) { - returnStatementAst.Pipeline.Visit(this); - return null; - } - public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) - { - ScopeStack.Push(scriptBlockAst); - - scriptBlockAst.ParamBlock?.Visit(this); - scriptBlockAst.BeginBlock?.Visit(this); - scriptBlockAst.ProcessBlock?.Visit(this); - scriptBlockAst.EndBlock?.Visit(this); - scriptBlockAst.DynamicParamBlock?.Visit(this); - - if (ShouldRename && TargetVariableAst.Parent.Parent == scriptBlockAst) - { - ShouldRename = false; - } - - if (DuplicateVariableAst?.Parent.Parent.Parent == scriptBlockAst) - { - ShouldRename = true; - DuplicateVariableAst = null; - } - - if (TargetVariableAst?.Parent.Parent == scriptBlockAst) - { - ShouldRename = true; - } - - ScopeStack.Pop(); - - return null; - } - public object VisitLoopStatement(LoopStatementAst loopAst) - { - - ScopeStack.Push(loopAst); - - loopAst.Body.Visit(this); - - ScopeStack.Pop(); - return null; - } - public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) - { - scriptBlockExpressionAst.ScriptBlock.Visit(this); - return null; - } - public object VisitStatementBlock(StatementBlockAst statementBlockAst) - { - foreach (StatementAst element in statementBlockAst.Statements) - { - element.Visit(this); - } - - if (DuplicateVariableAst?.Parent == statementBlockAst) - { - ShouldRename = true; - } - - return null; - } - public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) => null; - public object VisitSubExpression(SubExpressionAst subExpressionAst) - { - subExpressionAst.SubExpression.Visit(this); - return null; - } - public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) => throw new NotImplementedException(); - public object VisitThrowStatement(ThrowStatementAst throwStatementAst) => throw new NotImplementedException(); - public object VisitTrap(TrapStatementAst trapStatementAst) => throw new NotImplementedException(); - public object VisitTryStatement(TryStatementAst tryStatementAst) => throw new NotImplementedException(); - public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) => null; - public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) => throw new NotImplementedException(); - public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) => throw new NotImplementedException(); - public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) => throw new NotImplementedException(); - public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) => throw new NotImplementedException(); - public object VisitUsingStatement(UsingStatementAst usingStatement) => throw new NotImplementedException(); - public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) - { - if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && - variableExpressionAst.Extent.StartLineNumber == StartLineNumber) - { - ShouldRename = true; - TargetVariableAst = variableExpressionAst; - } - else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && - assignment.Operator == TokenKind.Equals) - { - if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) - { - DuplicateVariableAst = variableExpressionAst; - ShouldRename = false; - } - - } - - if (ShouldRename) - { - // have some modifications to account for the dollar sign prefix powershell uses for variables - TextChange Change = new() - { - NewText = NewName.Contains("$") ? NewName : "$" + NewName, - StartLine = variableExpressionAst.Extent.StartLineNumber - 1, - StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, - EndLine = variableExpressionAst.Extent.StartLineNumber - 1, - EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, - }; - - Modifications.Add(Change); - } - } - return null; - } - public object VisitWhileStatement(WhileStatementAst whileStatementAst) - { - whileStatementAst.Condition.Visit(this); - whileStatementAst.Body.Visit(this); - - return null; - } - } -} From b04a20e887462f516b1f1ba437627a6f0d3ea396 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:36:02 +1100 Subject: [PATCH 104/203] removed unsued class properties from function visitor --- .../Services/PowerShell/Refactoring/IterativeFunctionVistor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 45545493a..87ad9cc6a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -12,8 +12,6 @@ internal class IterativeFunctionRename { private readonly string OldName; private readonly string NewName; - internal Queue queue = new(); - internal bool ShouldRename; public List Modifications = new(); internal int StartLineNumber; internal int StartColumnNumber; From 8a9ede2e3295a501798676c6ef8b48e699ffdfd2 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:52:35 +1100 Subject: [PATCH 105/203] condensing if statements --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index c60bdd363..8591add68 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -291,10 +291,8 @@ public void ProcessNode(Ast node) } if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && - commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam) + commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) { - if (ShouldRename) - { TextChange Change = new() { NewText = NewName.Contains("-") ? NewName : "-" + NewName, @@ -303,9 +301,7 @@ public void ProcessNode(Ast node) EndLine = commandParameterAst.Extent.StartLineNumber - 1, EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, }; - Modifications.Add(Change); - } } else { From ab115225f77bd47a8204c470af34afb42a04b09f Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:54:58 +1100 Subject: [PATCH 106/203] Broke up Process node into 3 sub functions for readability --- .../Refactoring/IterativeVariableVisitor.cs | 214 ++++++++++-------- 1 file changed, 114 insertions(+), 100 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 8591add68..234078b98 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -256,115 +256,129 @@ public void ProcessNode(Ast node) switch (node) { case CommandAst commandAst: - // Is the Target Variable a Parameter and is this commandAst the target function - if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) - { - // Check to see if this is a splatted call to the target function. - Ast Splatted = null; - foreach (Ast element in commandAst.CommandElements) - { - if (element is VariableExpressionAst varAst && varAst.Splatted) - { - Splatted = varAst; - break; - } - } - if (Splatted != null) - { - NewSplattedModification(Splatted); - } - else - { - // The Target Variable is a Parameter and the commandAst is the Target Function - ShouldRename = true; - } - } + ProcessCommandAst(commandAst); break; case CommandParameterAst commandParameterAst: + ProcessCommandParameterAst(commandParameterAst); + break; + case VariableExpressionAst variableExpressionAst: + ProcessVariableExpressionAst(variableExpressionAst); + break; + } + } - if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) + private void ProcessCommandAst(CommandAst commandAst) + { + // Is the Target Variable a Parameter and is this commandAst the target function + if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) + { + // Check to see if this is a splatted call to the target function. + Ast Splatted = null; + foreach (Ast element in commandAst.CommandElements) + { + if (element is VariableExpressionAst varAst && varAst.Splatted) { - if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && - commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) - { - ShouldRename = true; - } + Splatted = varAst; + break; + } + } + if (Splatted != null) + { + NewSplattedModification(Splatted); + } + else + { + // The Target Variable is a Parameter and the commandAst is the Target Function + ShouldRename = true; + } + } + } - if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && - commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) - { - TextChange Change = new() - { - NewText = NewName.Contains("-") ? NewName : "-" + NewName, - StartLine = commandParameterAst.Extent.StartLineNumber - 1, - StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, - EndLine = commandParameterAst.Extent.StartLineNumber - 1, - EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, - }; - Modifications.Add(Change); - } - else - { - ShouldRename = false; - } + private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressionAst) + { + if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) + { + // Is this the Target Variable + if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && + variableExpressionAst.Extent.StartLineNumber == StartLineNumber) + { + ShouldRename = true; + TargetVariableAst = variableExpressionAst; + } + // Is this a Command Ast within scope + else if (variableExpressionAst.Parent is CommandAst commandAst) + { + if (WithinTargetsScope(TargetVariableAst, commandAst)) + { + ShouldRename = true; } - break; - case VariableExpressionAst variableExpressionAst: - if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) + } + // Is this a Variable Assignment thats not within scope + else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && + assignment.Operator == TokenKind.Equals) + { + if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) { - // Is this the Target Variable - if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && - variableExpressionAst.Extent.StartLineNumber == StartLineNumber) - { - ShouldRename = true; - TargetVariableAst = variableExpressionAst; - } - // Is this a Command Ast within scope - else if (variableExpressionAst.Parent is CommandAst commandAst) - { - if (WithinTargetsScope(TargetVariableAst, commandAst)) - { - ShouldRename = true; - } - } - // Is this a Variable Assignment thats not within scope - else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && - assignment.Operator == TokenKind.Equals) - { - if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) - { - ShouldRename = false; - } - - } - // Else is the variable within scope - else - { - ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); - } - if (ShouldRename) - { - // have some modifications to account for the dollar sign prefix powershell uses for variables - TextChange Change = new() - { - NewText = NewName.Contains("$") ? NewName : "$" + NewName, - StartLine = variableExpressionAst.Extent.StartLineNumber - 1, - StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, - EndLine = variableExpressionAst.Extent.StartLineNumber - 1, - EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, - }; - // If the variables parent is a parameterAst Add a modification - if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet) - { - TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); - Modifications.Add(aliasChange); - AliasSet = true; - } - Modifications.Add(Change); + ShouldRename = false; + } - } + } + // Else is the variable within scope + else + { + ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); + } + if (ShouldRename) + { + // have some modifications to account for the dollar sign prefix powershell uses for variables + TextChange Change = new() + { + NewText = NewName.Contains("$") ? NewName : "$" + NewName, + StartLine = variableExpressionAst.Extent.StartLineNumber - 1, + StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, + EndLine = variableExpressionAst.Extent.StartLineNumber - 1, + EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, + }; + // If the variables parent is a parameterAst Add a modification + if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet) + { + TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); + Modifications.Add(aliasChange); + AliasSet = true; } - break; + Modifications.Add(Change); + + } + } + } + + private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) + { + if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) + { + if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && + commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) + { + ShouldRename = true; + } + + if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && + commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) + { + TextChange Change = new() + { + NewText = NewName.Contains("-") ? NewName : "-" + NewName, + StartLine = commandParameterAst.Extent.StartLineNumber - 1, + StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, + EndLine = commandParameterAst.Extent.StartLineNumber - 1, + EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, + }; + Modifications.Add(Change); + } + else + { + ShouldRename = false; + } } } From 0c94c658f811bfe08f774f1f42bcc245b06cb75d Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:14:56 +1100 Subject: [PATCH 107/203] fixing comment grammar --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 234078b98..663e627e8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -83,7 +83,7 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN } Ast TargetParent = GetAstParentScope(node); - // Find All Variables and Parameter Assignments with the same name before + // Find all variables and parameter assignments with the same name before // The node found above List VariableAssignments = ScriptAst.FindAll(ast => { @@ -152,7 +152,6 @@ varDef.Parent is CommandAst && } } - if (node.Parent is CommandAst commDef) { if (funcDef.Name == commDef.GetCommandName() @@ -168,8 +167,6 @@ varDef.Parent is CommandAst && CorrectDefinition = element; } } - - } return CorrectDefinition ?? node; } From d10d9000c065de4054aa52e7e93f4a217ad3aea6 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:15:38 +1100 Subject: [PATCH 108/203] New Method and tests to check if a script ast contains dot sourcing --- .../PowerShell/Refactoring/Utilities.cs | 10 ++ .../Utilities/TestDotSourcingFalse.ps1 | 21 +++ .../Utilities/TestDotSourcingTrue.ps1 | 7 + .../Refactoring/RefactorUtilitiesTests.cs | 134 +++++++++--------- 4 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingFalse.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingTrue.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index cba8ce3e1..7f621f208 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -106,6 +106,16 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i return CorrectDefinition; } + public static bool AssertContainsDotSourced(Ast ScriptAst){ + Ast dotsourced = ScriptAst.Find(ast =>{ + return ast is CommandAst commandAst && commandAst.InvocationOperator == TokenKind.Dot; + },true); + if (dotsourced != null) + { + return true; + } + return false; + } public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { Ast token = null; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingFalse.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingFalse.ps1 new file mode 100644 index 000000000..d12a8652f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingFalse.ps1 @@ -0,0 +1,21 @@ +function New-User { + param ( + [string]$Username, + [string]$password + ) + write-host $username + $password + + $splat= @{ + Username = "JohnDeer" + Password = "SomePassword" + } + New-User @splat +} + +$UserDetailsSplat= @{ + Username = "JohnDoe" + Password = "SomePassword" +} +New-User @UserDetailsSplat + +New-User -Username "JohnDoe" -Password "SomePassword" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingTrue.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingTrue.ps1 new file mode 100644 index 000000000..b1cd25e65 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDotSourcingTrue.ps1 @@ -0,0 +1,7 @@ +$sb = { $var = 30 } +$shouldDotSource = Get-Random -Minimum 0 -Maximum 2 +if ($shouldDotSource) { + . $sb +} else { + & $sb +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 78af55904..6fd2e4fa4 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -43,113 +43,103 @@ public void Dispose() [Fact] public void GetVariableExpressionAst() { - RenameSymbolParams request = new(){ - Column=11, - Line=15, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 11, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(15,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(15, symbol.Extent.StartLineNumber); + Assert.Equal(1, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableExpressionStartAst() { - RenameSymbolParams request = new(){ - Column=1, - Line=15, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 1, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(15,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(15, symbol.Extent.StartLineNumber); + Assert.Equal(1, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinParameterAst() { - RenameSymbolParams request = new(){ - Column=21, - Line=3, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 21, + Line = 3, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(3,symbol.Extent.StartLineNumber); - Assert.Equal(17,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(3, symbol.Extent.StartLineNumber); + Assert.Equal(17, symbol.Extent.StartColumnNumber); } [Fact] public void GetHashTableKey() { - RenameSymbolParams request = new(){ - Column=9, - Line=16, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 9, + Line = 16, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(16,symbol.Extent.StartLineNumber); - Assert.Equal(5,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(16, symbol.Extent.StartLineNumber); + Assert.Equal(5, symbol.Extent.StartColumnNumber); } [Fact] public void GetVariableWithinCommandAst() { - RenameSymbolParams request = new(){ - Column=29, - Line=6, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 29, + Line = 6, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(6,symbol.Extent.StartLineNumber); - Assert.Equal(28,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(6, symbol.Extent.StartLineNumber); + Assert.Equal(28, symbol.Extent.StartColumnNumber); } [Fact] public void GetCommandParameterAst() { - RenameSymbolParams request = new(){ - Column=12, - Line=21, - RenameTo="Renamed", - FileName="TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(21,symbol.Extent.StartLineNumber); - Assert.Equal(10,symbol.Extent.StartColumnNumber); - - } - [Fact] - public void GetFunctionDefinitionAst() - { - RenameSymbolParams request = new(){ - Column=12, - Line=1, - RenameTo="Renamed", - FileName="TestDetection.ps1" + RenameSymbolParams request = new() + { + Column = 12, + Line = 21, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); - Assert.Equal(1,symbol.Extent.StartLineNumber); - Assert.Equal(1,symbol.Extent.StartColumnNumber); + Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); + Assert.Equal(21, symbol.Extent.StartLineNumber); + Assert.Equal(10, symbol.Extent.StartColumnNumber); } [Fact] @@ -157,7 +147,7 @@ public void GetFunctionDefinitionAst() { RenameSymbolParams request = new() { - Column = 16, + Column = 12, Line = 1, RenameTo = "Renamed", FileName = "TestDetection.ps1" @@ -165,9 +155,21 @@ public void GetFunctionDefinitionAst() ScriptFile scriptFile = GetTestScript(request.FileName); Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); Assert.Equal(1, symbol.Extent.StartLineNumber); Assert.Equal(1, symbol.Extent.StartColumnNumber); + + } + [Fact] + public void AssertContainsDotSourcingTrue() + { + ScriptFile scriptFile = GetTestScript("TestDotSourcingTrue.ps1"); + Assert.True(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); + } + [Fact] + public void AssertContainsDotSourcingFalse() + { + ScriptFile scriptFile = GetTestScript("TestDotSourcingFalse.ps1"); + Assert.False(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); } } } From 1adec8b46fa4b5b38b22142a583e8ed3cebae9c3 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:42:34 +1100 Subject: [PATCH 109/203] finalised dot source detection and notification --- .../Handlers/PrepareRenameSymbol.cs | 31 +++++++------------ .../PowerShell/Refactoring/Exceptions.cs | 17 ---------- .../Refactoring/IterativeVariableVisitor.cs | 1 - 3 files changed, 12 insertions(+), 37 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 4733689ed..6e9e23343 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -55,14 +55,18 @@ public async Task Handle(PrepareRenameSymbolParams re }; // ast is FunctionDefinitionAst or CommandAst or VariableExpressionAst or StringConstantExpressionAst && SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); - Ast token = Utilities.GetAst(request.Line + 1,request.Column + 1,scriptFile.ScriptAst); + Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); if (token == null) { result.message = "Unable to find symbol"; return result; } - + if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) + { + result.message = "Dot Source detected, this is currently not supported operation aborted"; + return result; + } switch (token) { case FunctionDefinitionAst funcDef: @@ -87,28 +91,17 @@ public async Task Handle(PrepareRenameSymbolParams re case VariableExpressionAst or CommandAst or CommandParameterAst or ParameterAst or StringConstantExpressionAst: { - - try + IterativeVariableRename visitor = new(request.RenameTo, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptFile.ScriptAst); + if (visitor.TargetVariableAst == null) { - IterativeVariableRename visitor = new(request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptFile.ScriptAst); - if (visitor.TargetVariableAst == null) - { - result.message = "Failed to find variable definition within the current file"; - } + result.message = "Failed to find variable definition within the current file"; } - catch (TargetVariableIsDotSourcedException) - { - - result.message = "Variable is dot sourced which is currently not supported unable to perform a rename"; - } - break; } } - return result; }).ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs index 230e136b7..e447556cf 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Exceptions.cs @@ -22,23 +22,6 @@ public TargetSymbolNotFoundException(string message, Exception inner) } } - public class TargetVariableIsDotSourcedException : Exception - { - public TargetVariableIsDotSourcedException() - { - } - - public TargetVariableIsDotSourcedException(string message) - : base(message) - { - } - - public TargetVariableIsDotSourcedException(string message, Exception inner) - : base(message, inner) - { - } - } - public class FunctionDefinitionNotFoundException : Exception { public FunctionDefinitionNotFoundException() diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 663e627e8..c5935afa8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -19,7 +19,6 @@ internal class IterativeVariableRename internal int StartLineNumber; internal int StartColumnNumber; internal VariableExpressionAst TargetVariableAst; - internal List dotSourcedScripts = new(); internal readonly Ast ScriptAst; internal bool isParam; internal bool AliasSet; From 7f5eb44b10e01e7aff9dcf760b984c984ea8d512 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:01:01 +1100 Subject: [PATCH 110/203] fixing spelling / naming mistakes --- ...RefactorsVariablesData.cs => RefactorVariablesData.cs} | 8 ++++---- ...rSplatted.ps1 => VariableCommandParameterSplatted.ps1} | 0 ...ed.ps1 => VariableCommandParameterSplattedRenamed.ps1} | 0 .../Refactoring/RefactorVariableTests.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{RefactorsVariablesData.cs => RefactorVariablesData.cs} (93%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VarableCommandParameterSplatted.ps1 => VariableCommandParameterSplatted.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VarableCommandParameterSplattedRenamed.ps1 => VariableCommandParameterSplattedRenamed.ps1} (100%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs similarity index 93% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index eab1415d1..78e4145a6 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorsVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -135,16 +135,16 @@ internal static class RenameVariableData Line = 9, RenameTo = "Renamed" }; - public static readonly RenameSymbolParams VarableCommandParameterSplattedFromCommandAst = new() + public static readonly RenameSymbolParams VariableCommandParameterSplattedFromCommandAst = new() { - FileName = "VarableCommandParameterSplatted.ps1", + FileName = "VariableCommandParameterSplatted.ps1", Column = 10, Line = 21, RenameTo = "Renamed" }; - public static readonly RenameSymbolParams VarableCommandParameterSplattedFromSplat = new() + public static readonly RenameSymbolParams VariableCommandParameterSplattedFromSplat = new() { - FileName = "VarableCommandParameterSplatted.ps1", + FileName = "VariableCommandParameterSplatted.ps1", Column = 5, Line = 16, RenameTo = "Renamed" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplatted.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VarableCommandParameterSplattedRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index bd0c2c0de..7fca178c8 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -258,7 +258,7 @@ public void VariableParameterCommandWithSameName() [Fact] public void VarableCommandParameterSplattedFromCommandAst() { - RenameSymbolParams request = RenameVariableData.VarableCommandParameterSplattedFromCommandAst; + RenameSymbolParams request = RenameVariableData.VariableCommandParameterSplattedFromCommandAst; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); @@ -269,7 +269,7 @@ public void VarableCommandParameterSplattedFromCommandAst() [Fact] public void VarableCommandParameterSplattedFromSplat() { - RenameSymbolParams request = RenameVariableData.VarableCommandParameterSplattedFromSplat; + RenameSymbolParams request = RenameVariableData.VariableCommandParameterSplattedFromSplat; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); From 434805b76536c1fc298f34de13c45d85235a9e25 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:32:11 +1100 Subject: [PATCH 111/203] removing .vscode files --- .vscode/launch.json | 26 -------------------------- .vscode/tasks.json | 41 ----------------------------------------- 2 files changed, 67 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 69f85c365..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/test/PowerShellEditorServices.Test.E2E/bin/Debug/net7.0/PowerShellEditorServices.Test.E2E.dll", - "args": [], - "cwd": "${workspaceFolder}/test/PowerShellEditorServices.Test.E2E", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 18313ef31..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/PowerShellEditorServices.sln", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/PowerShellEditorServices.sln", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/PowerShellEditorServices.sln" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file From 81951251461efe58999846e10135e4e38cea028a Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:33:36 +1100 Subject: [PATCH 112/203] deleting package-lock.json --- package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a839281bf..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "PowerShellEditorServices", - "lockfileVersion": 2, - "requires": true, - "packages": {} -} From a5b6f2effd6e030f303db8bd843b800d5c7d75f0 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:43:43 +1100 Subject: [PATCH 113/203] cleaning up comments and removed unused code --- .../Services/PowerShell/Refactoring/Utilities.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 7f621f208..7c07b1f15 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -45,7 +45,7 @@ public static Ast GetAstParentOfType(Ast ast, params Type[] type) public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) { - // Look up the targetted object + // Look up the targeted object CommandAst TargetCommand = (CommandAst)Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptFile , typeof(CommandAst)); @@ -69,12 +69,6 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i { return FunctionDefinitions[0]; } - // Sort function definitions - //FunctionDefinitions.Sort((a, b) => - //{ - // return b.Extent.EndColumnNumber + b.Extent.EndLineNumber - - // a.Extent.EndLineNumber + a.Extent.EndColumnNumber; - //}); // Determine which function definition is the right one FunctionDefinitionAst CorrectDefinition = null; for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) From 982bfdf194c01f3d17ecc290e838b831b67923f7 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Sun, 2 Jun 2024 10:41:13 +1000 Subject: [PATCH 114/203] Adjusted refactoring Tests to use IAsyncLifetime instead of IDisposable --- .../Refactoring/RefactorFunctionTests.cs | 23 ++++++++----------- .../Refactoring/RefactorUtilitiesTests.cs | 20 ++++++---------- .../Refactoring/RefactorVariableTests.cs | 22 ++++++++---------- 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 0d8a5df4c..2bce008cf 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; @@ -18,18 +19,18 @@ namespace PowerShellEditorServices.Test.Refactoring { [Trait("Category", "RefactorFunction")] - public class RefactorFunctionTests : IDisposable + public class RefactorFunctionTests : IAsyncLifetime { - private readonly PsesInternalHost psesHost; - private readonly WorkspaceService workspace; - public void Dispose() + private PsesInternalHost psesHost; + private WorkspaceService workspace; + public async Task InitializeAsync() { -#pragma warning disable VSTHRD002 - psesHost.StopAsync().Wait(); -#pragma warning restore VSTHRD002 - GC.SuppressFinalize(this); + psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); } + + public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Functions", fileName))); internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) @@ -72,11 +73,7 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams re }; return GetModifiedScript(scriptFile.Contents, changes); } - public RefactorFunctionTests() - { - psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } + [Fact] public void RefactorFunctionSingle() { diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 6fd2e4fa4..8630ba835 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; @@ -20,24 +20,18 @@ namespace PowerShellEditorServices.Test.Refactoring { [Trait("Category", "RefactorUtilities")] - public class RefactorUtilitiesTests : IDisposable + public class RefactorUtilitiesTests : IAsyncLifetime { - private readonly PsesInternalHost psesHost; - private readonly WorkspaceService workspace; + private PsesInternalHost psesHost; + private WorkspaceService workspace; - public RefactorUtilitiesTests() + public async Task InitializeAsync() { - psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); + psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); workspace = new WorkspaceService(NullLoggerFactory.Instance); } - public void Dispose() - { -#pragma warning disable VSTHRD002 - psesHost.StopAsync().Wait(); -#pragma warning restore VSTHRD002 - GC.SuppressFinalize(this); - } + public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Utilities", fileName))); [Fact] diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 7fca178c8..04402fdab 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; @@ -17,18 +18,17 @@ namespace PowerShellEditorServices.Test.Refactoring { [Trait("Category", "RenameVariables")] - public class RefactorVariableTests : IDisposable + public class RefactorVariableTests : IAsyncLifetime { - private readonly PsesInternalHost psesHost; - private readonly WorkspaceService workspace; - public void Dispose() + private PsesInternalHost psesHost; + private WorkspaceService workspace; + public async Task InitializeAsync() { -#pragma warning disable VSTHRD002 - psesHost.StopAsync().Wait(); -#pragma warning restore VSTHRD002 - GC.SuppressFinalize(this); + psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); } + public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Variables", fileName))); internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) @@ -71,11 +71,7 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams re }; return GetModifiedScript(scriptFile.Contents, changes); } - public RefactorVariableTests() - { - psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } + [Fact] public void RefactorVariableSingle() { From c89ed1612f78974613a486647173ad9ec841389c Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 3 Jun 2024 14:46:48 -0700 Subject: [PATCH 115/203] Fix Path.Combine to be cross platform --- .../Refactoring/RefactorFunctionTests.cs | 2 +- .../Refactoring/RefactorUtilitiesTests.cs | 2 +- .../Refactoring/RefactorVariableTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 2bce008cf..f7dfbf0f4 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -31,7 +31,7 @@ public async Task InitializeAsync() } public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Functions", fileName))); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 8630ba835..b52237c31 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -32,7 +32,7 @@ public async Task InitializeAsync() } public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Utilities", fileName))); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Utilities", fileName))); [Fact] public void GetVariableExpressionAst() diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 04402fdab..43acb60eb 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -29,7 +29,7 @@ public async Task InitializeAsync() workspace = new WorkspaceService(NullLoggerFactory.Instance); } public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring\\Variables", fileName))); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Variables", fileName))); internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { From 7d50476e2bcaefd308528eadac5edf5c6a358556 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:55:12 +1000 Subject: [PATCH 116/203] Fixing an odd edge case, of not being to rename a variable directly under a function definition, but selecting one column to the right worked --- .../PowerShell/Refactoring/Utilities.cs | 40 ++++++++++++++++--- .../TestDetectionUnderFunctionDef.ps1 | 15 +++++++ .../Refactoring/RefactorUtilitiesTests.cs | 17 ++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetectionUnderFunctionDef.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 7c07b1f15..3f42c6d35 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -100,10 +100,12 @@ public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, i return CorrectDefinition; } - public static bool AssertContainsDotSourced(Ast ScriptAst){ - Ast dotsourced = ScriptAst.Find(ast =>{ + public static bool AssertContainsDotSourced(Ast ScriptAst) + { + Ast dotsourced = ScriptAst.Find(ast => + { return ast is CommandAst commandAst && commandAst.InvocationOperator == TokenKind.Dot; - },true); + }, true); if (dotsourced != null) { return true; @@ -121,8 +123,36 @@ public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) StartColumnNumber >= ast.Extent.StartColumnNumber; }, true); - IEnumerable token = null; - token = Ast.FindAll(ast => + if (token is NamedBlockAst) + { + // NamedBlockAST starts on the same line as potentially another AST, + // its likley a user is not after the NamedBlockAst but what it contains + IEnumerable stacked_tokens = token.FindAll(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber + && StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + + if (stacked_tokens.Count() > 1) + { + return stacked_tokens.LastOrDefault(); + } + + return token.Parent; + } + + if (null == token) + { + IEnumerable LineT = Ast.FindAll(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + return LineT.OfType()?.LastOrDefault(); + } + + IEnumerable tokens = token.FindAll(ast => { return ast.Extent.EndColumnNumber >= StartColumnNumber && StartColumnNumber >= ast.Extent.StartColumnNumber; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetectionUnderFunctionDef.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetectionUnderFunctionDef.ps1 new file mode 100644 index 000000000..6ef6e2652 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/TestDetectionUnderFunctionDef.ps1 @@ -0,0 +1,15 @@ +function Write-Item($itemCount) { + $i = 1 + + while ($i -le $itemCount) { + $str = "Output $i" + Write-Output $str + + # In the gutter on the left, right click and select "Add Conditional Breakpoint" + # on the next line. Use the condition: $i -eq 25 + $i = $i + 1 + + # Slow down execution a bit so user can test the "Pause debugger" feature. + Start-Sleep -Milliseconds $DelayMilliseconds + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index b52237c31..fda1cafcb 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -152,6 +152,23 @@ public void GetFunctionDefinitionAst() Assert.Equal(1, symbol.Extent.StartLineNumber); Assert.Equal(1, symbol.Extent.StartColumnNumber); + } + [Fact] + public void GetVariableUnderFunctionDef() + { + RenameSymbolParams request = new(){ + Column=5, + Line=2, + RenameTo="Renamed", + FileName="TestDetectionUnderFunctionDef.ps1" + }; + ScriptFile scriptFile = GetTestScript(request.FileName); + + Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); + Assert.IsType(symbol); + Assert.Equal(2,symbol.Extent.StartLineNumber); + Assert.Equal(5,symbol.Extent.StartColumnNumber); + } [Fact] public void AssertContainsDotSourcingTrue() From d66397cf480b33a0f801795749dbc23d66dc1d07 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:16:09 +1000 Subject: [PATCH 117/203] added tests and logic for duplicate assignment cases for; foreach and for loops --- .../Refactoring/IterativeVariableVisitor.cs | 16 +++++++++++++- .../Variables/RefactorVariablesData.cs | 21 ++++++++++++++++++ .../VariableInForeachDuplicateAssignment.ps1 | 13 +++++++++++ ...bleInForeachDuplicateAssignmentRenamed.ps1 | 13 +++++++++++ .../VariableInForloopDuplicateAssignment.ps1 | 14 ++++++++++++ ...bleInForloopDuplicateAssignmentRenamed.ps1 | 14 ++++++++++++ .../Refactoring/RefactorVariableTests.cs | 22 +++++++++++++++++++ 7 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index c5935afa8..793db3b1a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -174,11 +174,25 @@ internal static Ast GetAstParentScope(Ast node) { Ast parent = node; // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst)); + parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst),typeof(ForStatementAst)); if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { parent = parent.Parent; } + // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match + // if so this is probably a variable defined within a foreach loop + else if(parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && + ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) { + parent = ForEachStmnt; + } + // Check if the parent of the VariableExpressionAst is a ForStatementAst then check if the variable names match + // if so this is probably a variable defined within a foreach loop + else if(parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && + ForStmnt.Initializer is AssignmentStatementAst AssignStmnt && AssignStmnt.Left is VariableExpressionAst VarExpStmnt && + VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath){ + parent = ForStmnt; + } + return parent; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index 78e4145a6..b893b9368 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -149,5 +149,26 @@ internal static class RenameVariableData Line = 16, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableInForeachDuplicateAssignment = new() + { + FileName = "VariableInForeachDuplicateAssignment.ps1", + Column = 18, + Line = 6, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableInForloopDuplicateAssignment = new() + { + FileName = "VariableInForloopDuplicateAssignment.ps1", + Column = 14, + Line = 7, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableInWhileDuplicateAssignment = new() + { + FileName = "VariableInWhileDuplicateAssignment.ps1", + Column = 13, + Line = 7, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 new file mode 100644 index 000000000..a69d1785e --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 @@ -0,0 +1,13 @@ +$a = 1..5 +$b = 6..10 +function test { + process { + foreach ($testvar in $a) { + $testvar + } + + foreach ($testvar in $b) { + $testvar + } + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 new file mode 100644 index 000000000..7b8ee8428 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 @@ -0,0 +1,13 @@ +$a = 1..5 +$b = 6..10 +function test { + process { + foreach ($Renamed in $a) { + $Renamed + } + + foreach ($testvar in $b) { + $testvar + } + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 new file mode 100644 index 000000000..8759c0242 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 @@ -0,0 +1,14 @@ +$a = 1..5 +$b = 6..10 +function test { + process { + + for ($i = 0; $i -lt $a.Count; $i++) { + $i + } + + for ($i = 0; $i -lt $a.Count; $i++) { + $i + } + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 new file mode 100644 index 000000000..bf7318b0f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 @@ -0,0 +1,14 @@ +$a = 1..5 +$b = 6..10 +function test { + process { + + for ($Renamed = 0; $Renamed -lt $a.Count; $Renamed++) { + $Renamed + } + + for ($i = 0; $i -lt $a.Count; $i++) { + $i + } + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 43acb60eb..db14d5c58 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -271,6 +271,28 @@ public void VarableCommandParameterSplattedFromSplat() string modifiedcontent = TestRenaming(scriptFile, request); + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VariableInForeachDuplicateAssignment() + { + RenameSymbolParams request = RenameVariableData.VariableInForeachDuplicateAssignment; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } + [Fact] + public void VariableInForloopDuplicateAssignment() + { + RenameSymbolParams request = RenameVariableData.VariableInForloopDuplicateAssignment; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + Assert.Equal(expectedContent.Contents, modifiedcontent); } } From 1269cc3e3ed9db73e93d31eeed2d2ce248e4acff Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:00:37 +1000 Subject: [PATCH 118/203] Adding in out of scope $i to test case which shouldnt be renamed --- .../Refactoring/Variables/RefactorVariablesData.cs | 2 +- .../Variables/VariableInForloopDuplicateAssignment.ps1 | 2 ++ .../Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index b893b9368..0ac902aa1 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -160,7 +160,7 @@ internal static class RenameVariableData { FileName = "VariableInForloopDuplicateAssignment.ps1", Column = 14, - Line = 7, + Line = 8, RenameTo = "Renamed" }; public static readonly RenameSymbolParams VariableInWhileDuplicateAssignment = new() diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 index 8759c0242..ec32f25b1 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 @@ -3,6 +3,8 @@ $b = 6..10 function test { process { + $i=10 + for ($i = 0; $i -lt $a.Count; $i++) { $i } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 index bf7318b0f..603211713 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 @@ -3,6 +3,8 @@ $b = 6..10 function test { process { + $i=10 + for ($Renamed = 0; $Renamed -lt $a.Count; $Renamed++) { $Renamed } From d2090ccc45a284b517a0d89390e45770b51f7df4 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:17:03 +1000 Subject: [PATCH 119/203] .net 8 requires a newline at the start of the script to recognise the scriptAST correctly otherwise it just picks up the $a = 1..5 --- .../Refactoring/Variables/RefactorVariablesData.cs | 2 +- .../Variables/VariableInForeachDuplicateAssignment.ps1 | 1 + .../Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 | 1 + .../Variables/VariableInForloopDuplicateAssignment.ps1 | 1 + .../Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index 0ac902aa1..1fbf1e53a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -160,7 +160,7 @@ internal static class RenameVariableData { FileName = "VariableInForloopDuplicateAssignment.ps1", Column = 14, - Line = 8, + Line = 9, RenameTo = "Renamed" }; public static readonly RenameSymbolParams VariableInWhileDuplicateAssignment = new() diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 index a69d1785e..ba03d8eb3 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignment.ps1 @@ -1,3 +1,4 @@ + $a = 1..5 $b = 6..10 function test { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 index 7b8ee8428..4467e88cb 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForeachDuplicateAssignmentRenamed.ps1 @@ -1,3 +1,4 @@ + $a = 1..5 $b = 6..10 function test { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 index ec32f25b1..66844c960 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignment.ps1 @@ -1,3 +1,4 @@ + $a = 1..5 $b = 6..10 function test { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 index 603211713..ff61eb4f6 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInForloopDuplicateAssignmentRenamed.ps1 @@ -1,3 +1,4 @@ + $a = 1..5 $b = 6..10 function test { From e51fb49e45d042e5b1b04defa5e434c22300787b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:20:47 +1000 Subject: [PATCH 120/203] fixing type in test name --- .../Refactoring/RefactorFunctionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index f7dfbf0f4..611d75529 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -182,7 +182,7 @@ public void RenameFunctionSameName() Assert.Equal(expectedContent.Contents, modifiedcontent); } [Fact] - public void RenameFunctionInSscriptblock() + public void RenameFunctionInScriptblock() { RenameSymbolParams request = RefactorsFunctionData.FunctionScriptblock; ScriptFile scriptFile = GetTestScript(request.FileName); From a436dcc41d902015dd6c1d8d1f0cd7031a21e2c1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:27:07 +1000 Subject: [PATCH 121/203] CommandAst input was being sent to variable renamer not function renamer, proper error return's now when renaming commandAST thats not defined in the script file --- .../Handlers/PrepareRenameSymbol.cs | 74 ++++++++++++------- .../PowerShell/Handlers/RenameSymbol.cs | 36 +++++---- 2 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 6e9e23343..5e72ad6a6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -67,40 +67,60 @@ public async Task Handle(PrepareRenameSymbolParams re result.message = "Dot Source detected, this is currently not supported operation aborted"; return result; } + + bool IsFunction = false; + string tokenName = ""; + switch (token) { - case FunctionDefinitionAst funcDef: - { - try - { - IterativeFunctionRename visitor = new(funcDef.Name, - request.RenameTo, - funcDef.Extent.StartLineNumber, - funcDef.Extent.StartColumnNumber, - scriptFile.ScriptAst); - } - catch (FunctionDefinitionNotFoundException) - { + case FunctionDefinitionAst FuncAst: + IsFunction = true; + tokenName = FuncAst.Name; + break; + case VariableExpressionAst or CommandParameterAst or ParameterAst: + IsFunction = false; + tokenName = request.RenameTo; + break; + case StringConstantExpressionAst: - result.message = "Failed to Find function definition within current file"; - } - - break; + if (token.Parent is CommandAst CommAst) + { + IsFunction = true; + tokenName = CommAst.GetCommandName(); } - - case VariableExpressionAst or CommandAst or CommandParameterAst or ParameterAst or StringConstantExpressionAst: + else { - IterativeVariableRename visitor = new(request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptFile.ScriptAst); - if (visitor.TargetVariableAst == null) - { - result.message = "Failed to find variable definition within the current file"; - } - break; + IsFunction = false; } + break; + } + + if (IsFunction) + { + try + { + IterativeFunctionRename visitor = new(tokenName, + request.RenameTo, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptFile.ScriptAst); + } + catch (FunctionDefinitionNotFoundException) + { + result.message = "Failed to Find function definition within current file"; + } + } + else + { + IterativeVariableRename visitor = new(tokenName, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptFile.ScriptAst); + if (visitor.TargetVariableAst == null) + { + result.message = "Failed to find variable definition within the current file"; + } } return result; }).ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 603e6a761..fc3490e5f 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -73,22 +73,28 @@ public RenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService worksp } internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameSymbolParams request) { + string tokenName = ""; if (token is FunctionDefinitionAst funcDef) { - IterativeFunctionRename visitor = new(funcDef.Name, - request.RenameTo, - funcDef.Extent.StartLineNumber, - funcDef.Extent.StartColumnNumber, - scriptAst); - visitor.Visit(scriptAst); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; - + tokenName = funcDef.Name; } - return null; + else if (token.Parent is CommandAst CommAst) + { + tokenName = CommAst.GetCommandName(); + } + IterativeFunctionRename visitor = new(tokenName, + request.RenameTo, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptAst); + visitor.Visit(scriptAst); + ModifiedFileResponse FileModifications = new(request.FileName) + { + Changes = visitor.Modifications + }; + return FileModifications; + + } internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameSymbolParams request) @@ -121,11 +127,11 @@ public async Task Handle(RenameSymbolParams request, Cancell return await Task.Run(() => { - Ast token = Utilities.GetAst(request.Line + 1,request.Column + 1,scriptFile.ScriptAst); + Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); if (token == null) { return null; } - ModifiedFileResponse FileModifications = token is FunctionDefinitionAst + ModifiedFileResponse FileModifications = (token is FunctionDefinitionAst || token.Parent is CommandAst) ? RenameFunction(token, scriptFile.ScriptAst, request) : RenameVariable(token, scriptFile.ScriptAst, request); From cef1696427cbadb8043c9fbd6ab076c743e28af1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:02:35 +1000 Subject: [PATCH 122/203] additional condition so that a function CommandAst will not be touched if it exists before the functions definition --- .../Services/PowerShell/Refactoring/IterativeFunctionVistor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 87ad9cc6a..36a8536d9 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -171,7 +171,8 @@ public void ProcessNode(Ast node, bool shouldRename) } break; case CommandAst ast: - if (ast.GetCommandName()?.ToLower() == OldName.ToLower()) + if (ast.GetCommandName()?.ToLower() == OldName.ToLower() && + TargetFunctionAst.Extent.StartLineNumber <= ast.Extent.StartLineNumber) { if (shouldRename) { From 54497e16270dd93ea9b399e6c1a3beffc9c65102 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:03:05 +1000 Subject: [PATCH 123/203] Making RenameSymbolParams public for xunit serializer --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index fc3490e5f..f1c2ebd5b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -16,14 +16,14 @@ namespace Microsoft.PowerShell.EditorServices.Handlers [Serial, Method("powerShell/renameSymbol")] internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } - internal class RenameSymbolParams : IRequest + public class RenameSymbolParams : IRequest { public string FileName { get; set; } public int Line { get; set; } public int Column { get; set; } public string RenameTo { get; set; } } - internal class TextChange + public class TextChange { public string NewText { get; set; } public int StartLine { get; set; } @@ -31,7 +31,7 @@ internal class TextChange public int EndLine { get; set; } public int EndColumn { get; set; } } - internal class ModifiedFileResponse + public class ModifiedFileResponse { public string FileName { get; set; } public List Changes { get; set; } @@ -55,7 +55,7 @@ public void AddTextChange(Ast Symbol, string NewText) ); } } - internal class RenameSymbolResult + public class RenameSymbolResult { public RenameSymbolResult() => Changes = new List(); public List Changes { get; set; } From 7a2b76f419c76147ec68abc2be3f1e8dc3538132 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:04:33 +1000 Subject: [PATCH 124/203] Renamed Test .ps1 to match class property name, reworked test cases to use TheoryData / parameterized tests --- ...{CmdletFunction.ps1 => FunctionCmdlet.ps1} | 0 ...nRenamed.ps1 => FunctionCmdletRenamed.ps1} | 0 ...oreachFunction.ps1 => FunctionForeach.ps1} | 0 ...Function.ps1 => FunctionForeachObject.ps1} | 0 ...d.ps1 => FunctionForeachObjectRenamed.ps1} | 0 ...Renamed.ps1 => FunctionForeachRenamed.ps1} | 0 ...unctions.ps1 => FunctionInnerIsNested.ps1} | 0 ...d.ps1 => FunctionInnerIsNestedRenamed.ps1} | 0 .../{LoopFunction.ps1 => FunctionLoop.ps1} | 0 ...ionRenamed.ps1 => FunctionLoopRenamed.ps1} | 0 ...es.ps1 => FunctionMultipleOccurrences.ps1} | 0 ...=> FunctionMultipleOccurrencesRenamed.ps1} | 0 .../Functions/FunctionNestedRedefinition.ps1 | 17 ++ .../FunctionNestedRedefinitionRenamed.ps1 | 17 ++ ...ps1 => FunctionOuterHasNestedFunction.ps1} | 0 ...FunctionOuterHasNestedFunctionRenamed.ps1} | 4 +- ...nameFunctions.ps1 => FunctionSameName.ps1} | 0 ...enamed.ps1 => FunctionSameNameRenamed.ps1} | 0 ...ckFunction.ps1 => FunctionScriptblock.ps1} | 0 ...med.ps1 => FunctionScriptblockRenamed.ps1} | 0 ...tion.ps1 => FunctionWithInnerFunction.ps1} | 0 ...1 => FunctionWithInnerFunctionRenamed.ps1} | 0 ...alls.ps1 => FunctionWithInternalCalls.ps1} | 0 ...1 => FunctionWithInternalCallsRenamed.ps1} | 0 ...{BasicFunction.ps1 => FunctionsSingle.ps1} | 0 ...Renamed.ps1 => FunctionsSingleRenamed.ps1} | 0 .../Functions/RefactorsFunctionData.cs | 35 +-- .../Refactoring/RefactorFunctionTests.cs | 233 ++++++++---------- 28 files changed, 163 insertions(+), 143 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{CmdletFunction.ps1 => FunctionCmdlet.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{CmdletFunctionRenamed.ps1 => FunctionCmdletRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ForeachFunction.ps1 => FunctionForeach.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ForeachObjectFunction.ps1 => FunctionForeachObject.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ForeachObjectFunctionRenamed.ps1 => FunctionForeachObjectRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ForeachFunctionRenamed.ps1 => FunctionForeachRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{NestedFunctions.ps1 => FunctionInnerIsNested.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{NestedFunctionsRenamed.ps1 => FunctionInnerIsNestedRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{LoopFunction.ps1 => FunctionLoop.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{LoopFunctionRenamed.ps1 => FunctionLoopRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{MultipleOccurrences.ps1 => FunctionMultipleOccurrences.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{MultipleOccurrencesRenamed.ps1 => FunctionMultipleOccurrencesRenamed.ps1} (100%) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinition.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinitionRenamed.ps1 rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{InnerFunction.ps1 => FunctionOuterHasNestedFunction.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{OuterFunctionRenamed.ps1 => FunctionOuterHasNestedFunctionRenamed.ps1} (67%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{SamenameFunctions.ps1 => FunctionSameName.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{SamenameFunctionsRenamed.ps1 => FunctionSameNameRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ScriptblockFunction.ps1 => FunctionScriptblock.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{ScriptblockFunctionRenamed.ps1 => FunctionScriptblockRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{OuterFunction.ps1 => FunctionWithInnerFunction.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{InnerFunctionRenamed.ps1 => FunctionWithInnerFunctionRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{InternalCalls.ps1 => FunctionWithInternalCalls.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{InternalCallsRenamed.ps1 => FunctionWithInternalCallsRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{BasicFunction.ps1 => FunctionsSingle.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{BasicFunctionRenamed.ps1 => FunctionsSingleRenamed.ps1} (100%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCmdlet.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCmdlet.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCmdletRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/CmdletFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionCmdletRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeach.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeach.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachObject.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachObject.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachObjectRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachObjectFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachObjectRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ForeachFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionForeachRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctions.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/NestedFunctionsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionLoop.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionLoop.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionLoopRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/LoopFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionLoopRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrences.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionMultipleOccurrences.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrences.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionMultipleOccurrences.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrencesRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionMultipleOccurrencesRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/MultipleOccurrencesRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionMultipleOccurrencesRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinition.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinition.ps1 new file mode 100644 index 000000000..2454effe6 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinition.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function testing_files { + param ( + $x + ) + write-host "Printing $x" +} + +foreach ($number in $x) { + testing_files $number + + function testing_files { + write-host "------------------" + } +} +testing_files "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinitionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinitionRenamed.ps1 new file mode 100644 index 000000000..304a97c87 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionNestedRedefinitionRenamed.ps1 @@ -0,0 +1,17 @@ +$x = 1..10 + +function testing_files { + param ( + $x + ) + write-host "Printing $x" +} + +foreach ($number in $x) { + testing_files $number + + function Renamed { + write-host "------------------" + } +} +testing_files "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionOuterHasNestedFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionOuterHasNestedFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionOuterHasNestedFunctionRenamed.ps1 similarity index 67% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionOuterHasNestedFunctionRenamed.ps1 index cd4062eb0..98f89d16f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunctionRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionOuterHasNestedFunctionRenamed.ps1 @@ -1,7 +1,7 @@ -function RenamedOuterFunction { +function Renamed { function NewInnerFunction { Write-Host "This is the inner function" } NewInnerFunction } -RenamedOuterFunction +Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctions.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctions.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctionsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/SamenameFunctionsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionScriptblock.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionScriptblock.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionScriptblockRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/ScriptblockFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionScriptblockRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/OuterFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InnerFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCalls.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInternalCalls.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCalls.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInternalCalls.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCallsRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInternalCallsRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/InternalCallsRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInternalCallsRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingle.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunction.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingle.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingleRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/BasicFunctionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingleRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs index 7b6918795..218257602 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs @@ -4,89 +4,89 @@ namespace PowerShellEditorServices.Test.Shared.Refactoring.Functions { - internal static class RefactorsFunctionData + internal class RefactorsFunctionData { public static readonly RenameSymbolParams FunctionsSingle = new() { - FileName = "BasicFunction.ps1", + FileName = "FunctionsSingle.ps1", Column = 1, Line = 5, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionMultipleOccurrences = new() { - FileName = "MultipleOccurrences.ps1", + FileName = "FunctionMultipleOccurrences.ps1", Column = 1, Line = 5, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionInnerIsNested = new() { - FileName = "NestedFunctions.ps1", + FileName = "FunctionInnerIsNested.ps1", Column = 5, Line = 5, RenameTo = "bar" }; public static readonly RenameSymbolParams FunctionOuterHasNestedFunction = new() { - FileName = "OuterFunction.ps1", + FileName = "FunctionOuterHasNestedFunction.ps1", Column = 10, Line = 1, - RenameTo = "RenamedOuterFunction" + RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionWithInnerFunction = new() { - FileName = "InnerFunction.ps1", + FileName = "FunctionWithInnerFunction.ps1", Column = 5, Line = 5, RenameTo = "RenamedInnerFunction" }; public static readonly RenameSymbolParams FunctionWithInternalCalls = new() { - FileName = "InternalCalls.ps1", + FileName = "FunctionWithInternalCalls.ps1", Column = 1, Line = 5, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionCmdlet = new() { - FileName = "CmdletFunction.ps1", + FileName = "FunctionCmdlet.ps1", Column = 10, Line = 1, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionSameName = new() { - FileName = "SamenameFunctions.ps1", + FileName = "FunctionSameName.ps1", Column = 14, Line = 3, RenameTo = "RenamedSameNameFunction" }; public static readonly RenameSymbolParams FunctionScriptblock = new() { - FileName = "ScriptblockFunction.ps1", + FileName = "FunctionScriptblock.ps1", Column = 5, Line = 5, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionLoop = new() { - FileName = "LoopFunction.ps1", + FileName = "FunctionLoop.ps1", Column = 5, Line = 5, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionForeach = new() { - FileName = "ForeachFunction.ps1", + FileName = "FunctionForeach.ps1", Column = 5, Line = 11, RenameTo = "Renamed" }; public static readonly RenameSymbolParams FunctionForeachObject = new() { - FileName = "ForeachObjectFunction.ps1", + FileName = "FunctionForeachObject.ps1", Column = 5, Line = 11, RenameTo = "Renamed" @@ -98,5 +98,12 @@ internal static class RefactorsFunctionData Line = 1, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams FunctionNestedRedefinition = new() + { + FileName = "FunctionNestedRedefinition.ps1", + Column = 15, + Line = 13, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 611d75529..02e2eed69 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -15,9 +15,12 @@ using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Refactoring; using PowerShellEditorServices.Test.Shared.Refactoring.Functions; +using Xunit.Abstractions; +using MediatR; namespace PowerShellEditorServices.Test.Refactoring { + [Trait("Category", "RefactorFunction")] public class RefactorFunctionTests : IAsyncLifetime @@ -51,22 +54,14 @@ internal static string GetModifiedScript(string OriginalScript, ModifiedFileResp return string.Join(Environment.NewLine, Lines); } - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request, SymbolReference symbol) + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) { - - //FunctionRename visitor = new(symbol.NameRegion.Text, - // request.RenameTo, - // symbol.ScriptRegion.StartLineNumber, - // symbol.ScriptRegion.StartColumnNumber, - // scriptFile.ScriptAst); - // scriptFile.ScriptAst.Visit(visitor); IterativeFunctionRename iterative = new(symbol.NameRegion.Text, request.RenameTo, symbol.ScriptRegion.StartLineNumber, symbol.ScriptRegion.StartColumnNumber, scriptFile.ScriptAst); iterative.Visit(scriptFile.ScriptAst); - //scriptFile.ScriptAst.Visit(visitor); ModifiedFileResponse changes = new(request.FileName) { Changes = iterative.Modifications @@ -74,176 +69,160 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams re return GetModifiedScript(scriptFile.Contents, changes); } - [Fact] - public void RefactorFunctionSingle() + public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable { - RenameSymbolParams request = RefactorsFunctionData.FunctionsSingle; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public string FileName { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string RenameTo { get; set; } - Assert.Equal(expectedContent.Contents, modifiedcontent); + // Default constructor needed for deserialization + public RenameSymbolParamsSerialized() { } - } - [Fact] - public void RenameFunctionMultipleOccurrences() - { - RenameSymbolParams request = RefactorsFunctionData.FunctionMultipleOccurrences; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + // Parameterized constructor for convenience + public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) + { + FileName = RenameSymbolParams.FileName; + Line = RenameSymbolParams.Line; + Column = RenameSymbolParams.Column; + RenameTo = RenameSymbolParams.RenameTo; + } - Assert.Equal(expectedContent.Contents, modifiedcontent); + public void Deserialize(IXunitSerializationInfo info) + { + FileName = info.GetValue("FileName"); + Line = info.GetValue("Line"); + Column = info.GetValue("Column"); + RenameTo = info.GetValue("RenameTo"); + } - } - [Fact] - public void RenameFunctionNested() - { - RenameSymbolParams request = RefactorsFunctionData.FunctionInnerIsNested; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("FileName", FileName); + info.AddValue("Line", Line); + info.AddValue("Column", Column); + info.AddValue("RenameTo", RenameTo); + } - Assert.Equal(expectedContent.Contents, modifiedcontent); + public override string ToString() => $"{FileName}"; } - [Fact] - public void RenameFunctionOuterHasNestedFunction() - { - RenameSymbolParams request = RefactorsFunctionData.FunctionOuterHasNestedFunction; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - [Fact] - public void RenameFunctionInnerIsNested() + public class SimpleData : TheoryData { - RenameSymbolParams request = RefactorsFunctionData.FunctionInnerIsNested; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public SimpleData() + { - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - [Fact] - public void RenameFunctionWithInternalCalls() - { - RenameSymbolParams request = RefactorsFunctionData.FunctionWithInternalCalls; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionsSingle)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionWithInternalCalls)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCmdlet)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionScriptblock)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCallWIthinStringExpression)); + } - Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionCmdlet() + + [Theory] + [ClassData(typeof(SimpleData))] + public void Simple(RenameSymbolParamsSerialized s) { - RenameSymbolParams request = RefactorsFunctionData.FunctionCmdlet; + // Arrange + RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); + request.Line, + request.Column); + // Act string modifiedcontent = TestRenaming(scriptFile, request, symbol); + // Assert Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionSameName() + + public class MultiOccurrenceData : TheoryData { - RenameSymbolParams request = RefactorsFunctionData.FunctionSameName; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public MultiOccurrenceData() + { + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); + } - Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionInScriptblock() + + [Theory] + [ClassData(typeof(MultiOccurrenceData))] + public void MultiOccurrence(RenameSymbolParamsSerialized s) { - RenameSymbolParams request = RefactorsFunctionData.FunctionScriptblock; + // Arrange + RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); + request.Line, + request.Column); + // Act string modifiedcontent = TestRenaming(scriptFile, request, symbol); + // Assert Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionInLoop() + + public class NestedData : TheoryData { - RenameSymbolParams request = RefactorsFunctionData.FunctionLoop; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public NestedData() + { + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + } - Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionInForeach() + + [Theory] + [ClassData(typeof(NestedData))] + public void Nested(RenameSymbolParamsSerialized s) { - RenameSymbolParams request = RefactorsFunctionData.FunctionForeach; + // Arrange + RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); + request.Line, + request.Column); + // Act string modifiedcontent = TestRenaming(scriptFile, request, symbol); + // Assert Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionInForeachObject() + public class LoopsData : TheoryData { - RenameSymbolParams request = RefactorsFunctionData.FunctionForeachObject; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + public LoopsData() + { + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionLoop)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeach)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeachObject)); + } - Assert.Equal(expectedContent.Contents, modifiedcontent); } - [Fact] - public void RenameFunctionCallWIthinStringExpression() + + [Theory] + [ClassData(typeof(LoopsData))] + public void Loops(RenameSymbolParamsSerialized s) { - RenameSymbolParams request = RefactorsFunctionData.FunctionCallWIthinStringExpression; + // Arrange + RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); + request.Line, + request.Column); + // Act string modifiedcontent = TestRenaming(scriptFile, request, symbol); + // Assert Assert.Equal(expectedContent.Contents, modifiedcontent); } } From 95c677bbc8e37d4903463aab6f84d2488151b2e1 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:15:12 +1000 Subject: [PATCH 125/203] consolidated tests as their are no special requirements --- .../Refactoring/RefactorFunctionTests.cs | 103 +++--------------- 1 file changed, 14 insertions(+), 89 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 02e2eed69..3bb610b34 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -107,110 +107,35 @@ public void Serialize(IXunitSerializationInfo info) public override string ToString() => $"{FileName}"; } - - public class SimpleData : TheoryData + public class FunctionRenameTestData : TheoryData { - public SimpleData() + public FunctionRenameTestData() { + // Simple Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionsSingle)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionWithInternalCalls)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCmdlet)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionScriptblock)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCallWIthinStringExpression)); - } - - } - - [Theory] - [ClassData(typeof(SimpleData))] - public void Simple(RenameSymbolParamsSerialized s) - { - // Arrange - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - // Act - string modifiedcontent = TestRenaming(scriptFile, request, symbol); - - // Assert - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - - public class MultiOccurrenceData : TheoryData - { - public MultiOccurrenceData() - { - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); - } - - } - - [Theory] - [ClassData(typeof(MultiOccurrenceData))] - public void MultiOccurrence(RenameSymbolParamsSerialized s) - { - // Arrange - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - // Act - string modifiedcontent = TestRenaming(scriptFile, request, symbol); - - // Assert - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - - public class NestedData : TheoryData - { - public NestedData() - { - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - } - - } - - [Theory] - [ClassData(typeof(NestedData))] - public void Nested(RenameSymbolParamsSerialized s) - { - // Arrange - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - // Act - string modifiedcontent = TestRenaming(scriptFile, request, symbol); - - // Assert - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - public class LoopsData : TheoryData - { - public LoopsData() - { + // Loops Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionLoop)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeach)); Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeachObject)); + // Nested + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + // Multi Occurance + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); } - } [Theory] - [ClassData(typeof(LoopsData))] - public void Loops(RenameSymbolParamsSerialized s) + [ClassData(typeof(FunctionRenameTestData))] + public void Rename(RenameSymbolParamsSerialized s) { // Arrange RenameSymbolParamsSerialized request = s; From 3d90b411df3b1ffcd57457b6e9c246881d012930 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:35:19 +1000 Subject: [PATCH 126/203] moved serializer and getmodifiedscriptcontent seperate file as it will be reused for variable tests --- .../Refactoring/RefactorFunctionTests.cs | 60 +--------------- .../Refactoring/RefactorUtilities.cs | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+), 59 deletions(-) create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs index 3bb610b34..0d1116d5c 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; @@ -15,8 +14,7 @@ using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Refactoring; using PowerShellEditorServices.Test.Shared.Refactoring.Functions; -using Xunit.Abstractions; -using MediatR; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; namespace PowerShellEditorServices.Test.Refactoring { @@ -36,24 +34,6 @@ public async Task InitializeAsync() public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); - internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) - { - - string[] Lines = OriginalScript.Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - foreach (TextChange change in Modification.Changes) - { - string TargetLine = Lines[change.StartLine]; - string begin = TargetLine.Substring(0, change.StartColumn); - string end = TargetLine.Substring(change.EndColumn); - Lines[change.StartLine] = begin + change.NewText + end; - } - - return string.Join(Environment.NewLine, Lines); - } - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) { IterativeFunctionRename iterative = new(symbol.NameRegion.Text, @@ -69,44 +49,6 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSer return GetModifiedScript(scriptFile.Contents, changes); } - public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable - { - public string FileName { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string RenameTo { get; set; } - - // Default constructor needed for deserialization - public RenameSymbolParamsSerialized() { } - - // Parameterized constructor for convenience - public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) - { - FileName = RenameSymbolParams.FileName; - Line = RenameSymbolParams.Line; - Column = RenameSymbolParams.Column; - RenameTo = RenameSymbolParams.RenameTo; - } - - public void Deserialize(IXunitSerializationInfo info) - { - FileName = info.GetValue("FileName"); - Line = info.GetValue("Line"); - Column = info.GetValue("Column"); - RenameTo = info.GetValue("RenameTo"); - } - - public void Serialize(IXunitSerializationInfo info) - { - info.AddValue("FileName", FileName); - info.AddValue("Line", Line); - info.AddValue("Column", Column); - info.AddValue("RenameTo", RenameTo); - } - - public override string ToString() => $"{FileName}"; - } - public class FunctionRenameTestData : TheoryData { public FunctionRenameTestData() diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs new file mode 100644 index 000000000..eb2d2a341 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -0,0 +1,70 @@ + +using System; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit.Abstractions; +using MediatR; + +namespace PowerShellEditorServices.Test.Refactoring +{ + public class RefactorUtilities + + { + + internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) + { + + string[] Lines = OriginalScript.Split( + new string[] { Environment.NewLine }, + StringSplitOptions.None); + + foreach (TextChange change in Modification.Changes) + { + string TargetLine = Lines[change.StartLine]; + string begin = TargetLine.Substring(0, change.StartColumn); + string end = TargetLine.Substring(change.EndColumn); + Lines[change.StartLine] = begin + change.NewText + end; + } + + return string.Join(Environment.NewLine, Lines); + } + + public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable + { + public string FileName { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string RenameTo { get; set; } + + // Default constructor needed for deserialization + public RenameSymbolParamsSerialized() { } + + // Parameterized constructor for convenience + public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) + { + FileName = RenameSymbolParams.FileName; + Line = RenameSymbolParams.Line; + Column = RenameSymbolParams.Column; + RenameTo = RenameSymbolParams.RenameTo; + } + + public void Deserialize(IXunitSerializationInfo info) + { + FileName = info.GetValue("FileName"); + Line = info.GetValue("Line"); + Column = info.GetValue("Column"); + RenameTo = info.GetValue("RenameTo"); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("FileName", FileName); + info.AddValue("Line", Line); + info.AddValue("Column", Column); + info.AddValue("RenameTo", RenameTo); + } + + public override string ToString() => $"{FileName}"; + } + + } +} From dd0194a9a2edaef567545b7d158bb366964b9c2e Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:47:34 +1000 Subject: [PATCH 127/203] Adding missing test case renamed.ps1 varient --- .../Refactoring/Variables/VariableRedefinitionRenamed.ps1 | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinitionRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinitionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinitionRenamed.ps1 new file mode 100644 index 000000000..29f3f87c7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableRedefinitionRenamed.ps1 @@ -0,0 +1,3 @@ +$Renamed = 10 +$Renamed = 20 +Write-Output $Renamed From bb9d27770c57ba215282ebde744707dcf89a2889 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:47:49 +1000 Subject: [PATCH 128/203] removing unused test case data --- .../Refactoring/Variables/RefactorVariablesData.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index 1fbf1e53a..8a8d40bfb 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -163,12 +163,5 @@ internal static class RenameVariableData Line = 9, RenameTo = "Renamed" }; - public static readonly RenameSymbolParams VariableInWhileDuplicateAssignment = new() - { - FileName = "VariableInWhileDuplicateAssignment.ps1", - Column = 13, - Line = 7, - RenameTo = "Renamed" - }; } } From 8b7ff3ad916b0dfba2cacc92bdc9f245c25e7fbe Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:48:11 +1000 Subject: [PATCH 129/203] updated GetModifiedScript with changes from the VariableRenameTests, which is just sorting the changes --- .../Refactoring/RefactorUtilities.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs index eb2d2a341..cfa16f1b1 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -12,7 +12,15 @@ public class RefactorUtilities internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) { + Modification.Changes.Sort((a, b) => + { + if (b.StartLine == a.StartLine) + { + return b.EndColumn - a.EndColumn; + } + return b.StartLine - a.StartLine; + }); string[] Lines = OriginalScript.Split( new string[] { Environment.NewLine }, StringSplitOptions.None); From 0c778dbf1cd41098d03e8ab279e7ecfa9a1dc104 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:48:46 +1000 Subject: [PATCH 130/203] modified variable test cases to use parameterized test cases --- .../Refactoring/RefactorVariableTests.cs | 274 +++--------------- 1 file changed, 33 insertions(+), 241 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index db14d5c58..95170194d 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; @@ -13,6 +12,7 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; using PowerShellEditorServices.Test.Shared.Refactoring.Variables; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; using Microsoft.PowerShell.EditorServices.Refactoring; namespace PowerShellEditorServices.Test.Refactoring @@ -31,33 +31,7 @@ public async Task InitializeAsync() public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Variables", fileName))); - internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) - { - Modification.Changes.Sort((a, b) => - { - if (b.StartLine == a.StartLine) - { - return b.EndColumn - a.EndColumn; - } - return b.StartLine - a.StartLine; - - }); - string[] Lines = OriginalScript.Split( - new string[] { Environment.NewLine }, - StringSplitOptions.None); - - foreach (TextChange change in Modification.Changes) - { - string TargetLine = Lines[change.StartLine]; - string begin = TargetLine.Substring(0, change.StartColumn); - string end = TargetLine.Substring(change.EndColumn); - Lines[change.StartLine] = begin + change.NewText + end; - } - - return string.Join(Environment.NewLine, Lines); - } - - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams request) + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request) { IterativeVariableRename iterative = new(request.RenameTo, @@ -71,223 +45,40 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParams re }; return GetModifiedScript(scriptFile.Contents, changes); } - - [Fact] - public void RefactorVariableSingle() - { - RenameSymbolParams request = RenameVariableData.SimpleVariableAssignment; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void RefactorVariableNestedScopeFunction() - { - RenameSymbolParams request = RenameVariableData.VariableNestedScopeFunction; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void RefactorVariableInPipeline() - { - RenameSymbolParams request = RenameVariableData.VariableInPipeline; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void RefactorVariableInScriptBlock() - { - RenameSymbolParams request = RenameVariableData.VariableInScriptblock; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void RefactorVariableInScriptBlockScoped() - { - RenameSymbolParams request = RenameVariableData.VariablewWithinHastableExpression; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableNestedFunctionScriptblock() - { - RenameSymbolParams request = RenameVariableData.VariableNestedFunctionScriptblock; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableWithinCommandAstScriptBlock() - { - RenameSymbolParams request = RenameVariableData.VariableWithinCommandAstScriptBlock; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableWithinForeachObject() - { - RenameSymbolParams request = RenameVariableData.VariableWithinForeachObject; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableusedInWhileLoop() - { - RenameSymbolParams request = RenameVariableData.VariableusedInWhileLoop; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableInParam() - { - RenameSymbolParams request = RenameVariableData.VariableInParam; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableCommandParameter() - { - RenameSymbolParams request = RenameVariableData.VariableCommandParameter; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableCommandParameterReverse() - { - RenameSymbolParams request = RenameVariableData.VariableCommandParameterReverse; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableScriptWithParamBlock() - { - RenameSymbolParams request = RenameVariableData.VariableScriptWithParamBlock; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableNonParam() - { - RenameSymbolParams request = RenameVariableData.VariableNonParam; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - - } - [Fact] - public void VariableParameterCommandWithSameName() - { - RenameSymbolParams request = RenameVariableData.VariableParameterCommandWithSameName; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - [Fact] - public void VarableCommandParameterSplattedFromCommandAst() - { - RenameSymbolParams request = RenameVariableData.VariableCommandParameterSplattedFromCommandAst; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - [Fact] - public void VarableCommandParameterSplattedFromSplat() + public class VariableRenameTestData : TheoryData { - RenameSymbolParams request = RenameVariableData.VariableCommandParameterSplattedFromSplat; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); + public VariableRenameTestData() + { + Add(new RenameSymbolParamsSerialized(RenameVariableData.SimpleVariableAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableRedefinition)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunction)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInLoop)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInPipeline)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblockScoped)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariablewWithinHastableExpression)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedFunctionScriptblock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinCommandAstScriptBlock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinForeachObject)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableusedInWhileLoop)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInParam)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameter)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterReverse)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableScriptWithParamBlock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNonParam)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableParameterCommandWithSameName)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromCommandAst)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromSplat)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); + } } - [Fact] - public void VariableInForeachDuplicateAssignment() - { - RenameSymbolParams request = RenameVariableData.VariableInForeachDuplicateAssignment; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - [Fact] - public void VariableInForloopDuplicateAssignment() + [Theory] + [ClassData(typeof(VariableRenameTestData))] + public void Rename(RenameSymbolParamsSerialized s) { - RenameSymbolParams request = RenameVariableData.VariableInForloopDuplicateAssignment; + RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); @@ -296,4 +87,5 @@ public void VariableInForloopDuplicateAssignment() Assert.Equal(expectedContent.Contents, modifiedcontent); } } + } From 83ab5b76bea096e2e9a89fd1932e55204e3f6d07 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:31:56 +1000 Subject: [PATCH 131/203] Added a new test case for renaming an inner variable leaking out the functions scope --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 7 +++++++ .../Refactoring/Variables/RefactorVariablesData.cs | 7 +++++++ .../Variables/VariableNestedScopeFunctionRefactorInner.ps1 | 7 +++++++ .../VariableNestedScopeFunctionRefactorInnerRenamed.ps1 | 7 +++++++ .../Refactoring/RefactorVariableTests.cs | 1 + 5 files changed, 29 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInner.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInnerRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 793db3b1a..4c7a52f3d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -322,6 +322,13 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi { ShouldRename = true; } + // The TargetVariable is defined within a function + // This commandAst is not within that function's scope so we should not rename + if (GetAstParentScope(TargetVariableAst) is FunctionDefinitionAst && !WithinTargetsScope(TargetVariableAst, commandAst)) + { + ShouldRename = false; + } + } // Is this a Variable Assignment thats not within scope else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index 8a8d40bfb..b9ea3ec49 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -163,5 +163,12 @@ internal static class RenameVariableData Line = 9, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableNestedScopeFunctionRefactorInner = new() + { + FileName = "VariableNestedScopeFunctionRefactorInner.ps1", + Column = 5, + Line = 3, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInner.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInner.ps1 new file mode 100644 index 000000000..3c6c22651 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInner.ps1 @@ -0,0 +1,7 @@ +$var = 10 +function TestFunction { + $var = 20 + Write-Output $var +} +TestFunction +Write-Output $var diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInnerRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInnerRenamed.ps1 new file mode 100644 index 000000000..d943f509a --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedScopeFunctionRefactorInnerRenamed.ps1 @@ -0,0 +1,7 @@ +$var = 10 +function TestFunction { + $Renamed = 20 + Write-Output $Renamed +} +TestFunction +Write-Output $var diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index 95170194d..ce3d8bcec 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -71,6 +71,7 @@ public VariableRenameTestData() Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromSplat)); Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunctionRefactorInner)); } } From b2393924d4f2e89faff07f3237c22a42f0a4f280 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:18:29 +1000 Subject: [PATCH 132/203] reworked applicable utilities tests to be parameterized --- .../Utilities/RefactorUtilitiesData.cs | 60 ++++++++ .../Refactoring/RefactorUtilitiesTests.cs | 145 ++++-------------- 2 files changed, 86 insertions(+), 119 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs new file mode 100644 index 000000000..5cc1ea89d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Microsoft.PowerShell.EditorServices.Handlers; + +namespace PowerShellEditorServices.Test.Shared.Refactoring.Utilities +{ + internal static class RenameUtilitiesData + { + + public static readonly RenameSymbolParams GetVariableExpressionAst = new() + { + Column = 11, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetVariableExpressionStartAst = new() + { + Column = 1, + Line = 15, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetVariableWithinParameterAst = new() + { + Column = 21, + Line = 3, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetHashTableKey = new() + { + Column = 9, + Line = 16, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetVariableWithinCommandAst = new() + { + Column = 29, + Line = 6, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetCommandParameterAst = new() + { + Column = 12, + Line = 21, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + public static readonly RenameSymbolParams GetFunctionDefinitionAst = new() + { + Column = 12, + Line = 1, + RenameTo = "Renamed", + FileName = "TestDetection.ps1" + }; + } +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index fda1cafcb..70f91fd03 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -12,10 +12,9 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; using System.Management.Automation.Language; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; using Microsoft.PowerShell.EditorServices.Refactoring; -using System.Management.Automation.Language; -using System.Collections.Generic; -using System.Linq; +using PowerShellEditorServices.Test.Shared.Refactoring.Utilities; namespace PowerShellEditorServices.Test.Refactoring { @@ -34,140 +33,48 @@ public async Task InitializeAsync() public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Utilities", fileName))); - [Fact] - public void GetVariableExpressionAst() - { - RenameSymbolParams request = new() - { - Column = 11, - Line = 15, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(15, symbol.Extent.StartLineNumber); - Assert.Equal(1, symbol.Extent.StartColumnNumber); - } - [Fact] - public void GetVariableExpressionStartAst() + public class GetAstShouldDetectTestData : TheoryData { - RenameSymbolParams request = new() + public GetAstShouldDetectTestData() { - Column = 1, - Line = 15, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(15, symbol.Extent.StartLineNumber); - Assert.Equal(1, symbol.Extent.StartColumnNumber); - + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionAst), 15, 1); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionStartAst), 15, 1); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinParameterAst), 3, 17); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetHashTableKey), 16, 5); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinCommandAst), 6, 28); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetCommandParameterAst), 21, 10); + Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetFunctionDefinitionAst), 1, 1); + } } - [Fact] - public void GetVariableWithinParameterAst() - { - RenameSymbolParams request = new() - { - Column = 21, - Line = 3, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(3, symbol.Extent.StartLineNumber); - Assert.Equal(17, symbol.Extent.StartColumnNumber); - - } - [Fact] - public void GetHashTableKey() - { - RenameSymbolParams request = new() - { - Column = 9, - Line = 16, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(16, symbol.Extent.StartLineNumber); - Assert.Equal(5, symbol.Extent.StartColumnNumber); - } - [Fact] - public void GetVariableWithinCommandAst() + [Theory] + [ClassData(typeof(GetAstShouldDetectTestData))] + public void GetAstShouldDetect(RenameSymbolParamsSerialized s, int l, int c) { - RenameSymbolParams request = new() - { - Column = 29, - Line = 6, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(6, symbol.Extent.StartLineNumber); - Assert.Equal(28, symbol.Extent.StartColumnNumber); - + ScriptFile scriptFile = GetTestScript(s.FileName); + Ast symbol = Utilities.GetAst(s.Line, s.Column, scriptFile.ScriptAst); + // Assert the Line and Column is what is expected + Assert.Equal(l, symbol.Extent.StartLineNumber); + Assert.Equal(c, symbol.Extent.StartColumnNumber); } - [Fact] - public void GetCommandParameterAst() - { - RenameSymbolParams request = new() - { - Column = 12, - Line = 21, - RenameTo = "Renamed", - FileName = "TestDetection.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(21, symbol.Extent.StartLineNumber); - Assert.Equal(10, symbol.Extent.StartColumnNumber); - - } [Fact] - public void GetFunctionDefinitionAst() + public void GetVariableUnderFunctionDef() { RenameSymbolParams request = new() { - Column = 12, - Line = 1, + Column = 5, + Line = 2, RenameTo = "Renamed", - FileName = "TestDetection.ps1" + FileName = "TestDetectionUnderFunctionDef.ps1" }; ScriptFile scriptFile = GetTestScript(request.FileName); Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.Equal(1, symbol.Extent.StartLineNumber); - Assert.Equal(1, symbol.Extent.StartColumnNumber); - - } - [Fact] - public void GetVariableUnderFunctionDef() - { - RenameSymbolParams request = new(){ - Column=5, - Line=2, - RenameTo="Renamed", - FileName="TestDetectionUnderFunctionDef.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); - - Ast symbol = Utilities.GetAst(request.Line,request.Column,scriptFile.ScriptAst); Assert.IsType(symbol); - Assert.Equal(2,symbol.Extent.StartLineNumber); - Assert.Equal(5,symbol.Extent.StartColumnNumber); + Assert.Equal(2, symbol.Extent.StartLineNumber); + Assert.Equal(5, symbol.Extent.StartColumnNumber); } [Fact] From e1e2112b167b4f2656e3a8297698ffd992d2e79b Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:19:02 +1000 Subject: [PATCH 133/203] Added test case for simple function parameter rename, added clause for is target is within a parameter block within a function to solve --- .../Refactoring/IterativeVariableVisitor.cs | 9 +++++++++ .../Variables/RefactorVariablesData.cs | 7 +++++++ .../VariableSimpleFunctionParameter.ps1 | 18 ++++++++++++++++++ .../VariableSimpleFunctionParameterRenamed.ps1 | 18 ++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 4c7a52f3d..a1e953059 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -82,6 +82,15 @@ public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnN } Ast TargetParent = GetAstParentScope(node); + + // Is the Variable sitting within a ParameterBlockAst that is within a Function Definition + // If so we don't need to look further as this is most likley the AssignmentStatement we are looking for + Ast paramParent = Utilities.GetAstParentOfType(node, typeof(ParamBlockAst)); + if (TargetParent is FunctionDefinitionAst && null != paramParent) + { + return node; + } + // Find all variables and parameter assignments with the same name before // The node found above List VariableAssignments = ScriptAst.FindAll(ast => diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index b9ea3ec49..b518cd135 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -170,5 +170,12 @@ internal static class RenameVariableData Line = 3, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableSimpleFunctionParameter = new() + { + FileName = "VariableSimpleFunctionParameter.ps1", + Column = 9, + Line = 6, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 new file mode 100644 index 000000000..8e2a4ef5d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 @@ -0,0 +1,18 @@ +$x = 1..10 + +function testing_files { + + param ( + $x + ) + write-host "Printing $x" +} + +foreach ($number in $x) { + testing_files $number + + function testing_files { + write-host "------------------" + } +} +testing_files "99" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 new file mode 100644 index 000000000..250d360ca --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 @@ -0,0 +1,18 @@ +$x = 1..10 + +function testing_files { + + param ( + [Alias("x")]$Renamed + ) + write-host "Printing $Renamed" +} + +foreach ($number in $x) { + testing_files $number + + function testing_files { + write-host "------------------" + } +} +testing_files "99" From b3cb27f6bb2f380e71493533a432b3dae6b49f0c Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:22:09 +1000 Subject: [PATCH 134/203] adding plumbling for shouldgenerateAlias on server side --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index f1c2ebd5b..dd9533252 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -16,12 +16,17 @@ namespace Microsoft.PowerShell.EditorServices.Handlers [Serial, Method("powerShell/renameSymbol")] internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } + public class RenameSymbolOptions { + public bool ShouldGenerateAlias { get; set; } + } + public class RenameSymbolParams : IRequest { public string FileName { get; set; } public int Line { get; set; } public int Column { get; set; } public string RenameTo { get; set; } + public RenameSymbolOptions Options { get; set; } } public class TextChange { From b465f6e8c43752e176cd5da73be51f90479df921 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:41:30 +1000 Subject: [PATCH 135/203] Passing through shouldGenerateAlias to VariableVisitor Class --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 4 +++- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index dd9533252..a6ef94ab2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -106,10 +106,12 @@ internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, R { if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { + IterativeVariableRename visitor = new(request.RenameTo, symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, - scriptAst); + scriptAst, + request.Options ?? null); visitor.Visit(scriptAst); ModifiedFileResponse FileModifications = new(request.FileName) { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index a1e953059..54b4ecb04 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -23,13 +23,15 @@ internal class IterativeVariableRename internal bool isParam; internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; + internal RenameSymbolOptions options; - public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst,RenameSymbolOptions options = null) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; + this.options = options ?? new RenameSymbolOptions { ShouldGenerateAlias = true }; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -366,7 +368,8 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, }; // If the variables parent is a parameterAst Add a modification - if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet) + if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && + options.ShouldGenerateAlias) { TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); From 895c3604b12513323287a08e452aa0bc9b71e03a Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:27:21 +1000 Subject: [PATCH 136/203] added new test cases for functions with variables defined outside of scope and a helper method IsVariableExpressionAssignedInTargetScope --- .../Refactoring/IterativeVariableVisitor.cs | 53 ++++++++++++++++--- .../Variables/RefactorVariablesData.cs | 14 +++++ .../VariableDotNotationFromInnerFunction.ps1 | 21 ++++++++ ...bleDotNotationFromInnerFunctionRenamed.ps1 | 21 ++++++++ .../Refactoring/RefactorVariableTests.cs | 2 + 5 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunction.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunctionRenamed.ps1 diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 54b4ecb04..9880d2663 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -25,7 +25,7 @@ internal class IterativeVariableRename internal FunctionDefinitionAst TargetFunction; internal RenameSymbolOptions options; - public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst,RenameSymbolOptions options = null) + public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, RenameSymbolOptions options = null) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; @@ -185,28 +185,60 @@ internal static Ast GetAstParentScope(Ast node) { Ast parent = node; // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst),typeof(ForStatementAst)); + parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst), typeof(ForStatementAst)); if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) { parent = parent.Parent; } // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match // if so this is probably a variable defined within a foreach loop - else if(parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && - ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) { + else if (parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && + ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) + { parent = ForEachStmnt; } // Check if the parent of the VariableExpressionAst is a ForStatementAst then check if the variable names match // if so this is probably a variable defined within a foreach loop - else if(parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && + else if (parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && ForStmnt.Initializer is AssignmentStatementAst AssignStmnt && AssignStmnt.Left is VariableExpressionAst VarExpStmnt && - VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath){ + VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath) + { parent = ForStmnt; } return parent; } + internal static bool IsVariableExpressionAssignedInTargetScope(VariableExpressionAst node, Ast scope) + { + bool r = false; + + List VariableAssignments = node.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && + // Look Backwards from the node above + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && + // Must be within the the designated scope + VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; + }, true).Cast().ToList(); + + if (VariableAssignments.Count > 0) + { + r = true; + } + // Node is probably the first Assignment Statement within scope + if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) + { + r = true; + } + + return r; + } + internal static bool WithinTargetsScope(Ast Target, Ast Child) { bool r = false; @@ -214,9 +246,14 @@ internal static bool WithinTargetsScope(Ast Target, Ast Child) Ast TargetScope = GetAstParentScope(Target); while (childParent != null) { - if (childParent is FunctionDefinitionAst) + if (childParent is FunctionDefinitionAst FuncDefAst) { - break; + if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) + { + + }else{ + break; + } } if (childParent == TargetScope) { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index b518cd135..ab166b165 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -177,5 +177,19 @@ internal static class RenameVariableData Line = 6, RenameTo = "Renamed" }; + public static readonly RenameSymbolParams VariableDotNotationFromInnerFunction = new() + { + FileName = "VariableDotNotationFromInnerFunction.ps1", + Column = 26, + Line = 11, + RenameTo = "Renamed" + }; + public static readonly RenameSymbolParams VariableDotNotationFromOuterVar = new() + { + FileName = "VariableDotNotationFromInnerFunction.ps1", + Column = 1, + Line = 1, + RenameTo = "Renamed" + }; } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunction.ps1 new file mode 100644 index 000000000..126a2745d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunction.ps1 @@ -0,0 +1,21 @@ +$NeededTools = @{ + OpenSsl = 'openssl for macOS' + PowerShellGet = 'PowerShellGet latest' + InvokeBuild = 'InvokeBuild latest' +} + +function getMissingTools () { + $missingTools = @() + + if (needsOpenSsl) { + $missingTools += $NeededTools.OpenSsl + } + if (needsPowerShellGet) { + $missingTools += $NeededTools.PowerShellGet + } + if (needsInvokeBuild) { + $missingTools += $NeededTools.InvokeBuild + } + + return $missingTools +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunctionRenamed.ps1 new file mode 100644 index 000000000..d8c478ec6 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDotNotationFromInnerFunctionRenamed.ps1 @@ -0,0 +1,21 @@ +$Renamed = @{ + OpenSsl = 'openssl for macOS' + PowerShellGet = 'PowerShellGet latest' + InvokeBuild = 'InvokeBuild latest' +} + +function getMissingTools () { + $missingTools = @() + + if (needsOpenSsl) { + $missingTools += $Renamed.OpenSsl + } + if (needsPowerShellGet) { + $missingTools += $Renamed.PowerShellGet + } + if (needsInvokeBuild) { + $missingTools += $Renamed.InvokeBuild + } + + return $missingTools +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs index ce3d8bcec..f940ccdb8 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs @@ -72,6 +72,8 @@ public VariableRenameTestData() Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunctionRefactorInner)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromOuterVar)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromInnerFunction)); } } From ae10c3742bc70f7699ec7b23d493a5a92eb8d7c2 Mon Sep 17 00:00:00 2001 From: Razmo99 <3089087+Razmo99@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:21:13 +1000 Subject: [PATCH 137/203] renaming ShouldGenerateAlias to create CreateAlias --- .../Services/PowerShell/Handlers/RenameSymbol.cs | 2 +- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index a6ef94ab2..8a3fb31f4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerShell.EditorServices.Handlers internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } public class RenameSymbolOptions { - public bool ShouldGenerateAlias { get; set; } + public bool CreateAlias { get; set; } } public class RenameSymbolParams : IRequest diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 9880d2663..2a11dce88 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -31,7 +31,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.options = options ?? new RenameSymbolOptions { ShouldGenerateAlias = true }; + this.options = options ?? new RenameSymbolOptions { CreateAlias = true }; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -406,7 +406,7 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - options.ShouldGenerateAlias) + options.CreateAlias) { TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); From 08a86858e57745a3ea120967f45ba4e99cf62b96 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 11 Sep 2024 21:25:46 -0700 Subject: [PATCH 138/203] Add Missing Disclaimer --- .../Refactoring/RefactorUtilities.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs index cfa16f1b1..c21b9aa0e 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System; using Microsoft.PowerShell.EditorServices.Handlers; From fec53197a6bf7a9e92b288d430f62308d86b77ef Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 11 Sep 2024 23:37:22 -0700 Subject: [PATCH 139/203] Explicitly Show Unsaved Files as currently unsupported. It's probably doable but will take work Also add convenience HandlerErrorException as RPCErrorException is super obtuse. --- .../Handlers/PrepareRenameSymbol.cs | 12 ++----- .../PowerShell/Handlers/RenameSymbol.cs | 36 ++++++++----------- .../Utility/HandlerErrorException.cs | 22 ++++++++++++ 3 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 src/PowerShellEditorServices/Utility/HandlerErrorException.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 5e72ad6a6..4ea6c0c64 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -7,7 +7,6 @@ using System.Management.Automation.Language; using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; using Microsoft.PowerShell.EditorServices.Services.Symbols; @@ -31,21 +30,16 @@ internal class PrepareRenameSymbolResult internal class PrepareRenameSymbolHandler : IPrepareRenameSymbolHandler { - private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; - public PrepareRenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService workspaceService) - { - _logger = loggerFactory.CreateLogger(); - _workspaceService = workspaceService; - } + public PrepareRenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; public async Task Handle(PrepareRenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) { - _logger.LogDebug("Failed to open file!"); - return await Task.FromResult(null).ConfigureAwait(false); + // TODO: Unsaved file support. We need to find the unsaved file in the text documents synced to the LSP and use that as our Ast Base. + throw new HandlerErrorException($"File {request.FileName} not found in workspace. Unsaved files currently do not support the rename symbol feature."); } return await Task.Run(() => { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 8a3fb31f4..6e4107ad4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -8,15 +8,16 @@ using System.Management.Automation.Language; using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; +using System; namespace Microsoft.PowerShell.EditorServices.Handlers { [Serial, Method("powerShell/renameSymbol")] internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } - public class RenameSymbolOptions { + public class RenameSymbolOptions + { public bool CreateAlias { get; set; } } @@ -68,14 +69,10 @@ public class RenameSymbolResult internal class RenameSymbolHandler : IRenameSymbolHandler { - private readonly ILogger _logger; private readonly WorkspaceService _workspaceService; - public RenameSymbolHandler(ILoggerFactory loggerFactory, WorkspaceService workspaceService) - { - _logger = loggerFactory.CreateLogger(); - _workspaceService = workspaceService; - } + public RenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; + internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameSymbolParams request) { string tokenName = ""; @@ -98,20 +95,20 @@ internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, Re Changes = visitor.Modifications }; return FileModifications; - - - } + internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameSymbolParams request) { if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { - IterativeVariableRename visitor = new(request.RenameTo, - symbol.Extent.StartLineNumber, - symbol.Extent.StartColumnNumber, - scriptAst, - request.Options ?? null); + IterativeVariableRename visitor = new( + request.RenameTo, + symbol.Extent.StartLineNumber, + symbol.Extent.StartColumnNumber, + scriptAst, + request.Options ?? null + ); visitor.Visit(scriptAst); ModifiedFileResponse FileModifications = new(request.FileName) { @@ -121,21 +118,18 @@ internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, R } return null; - } + public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) { if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) { - _logger.LogDebug("Failed to open file!"); - return await Task.FromResult(null).ConfigureAwait(false); + throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); } return await Task.Run(() => { - Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); - if (token == null) { return null; } ModifiedFileResponse FileModifications = (token is FunctionDefinitionAst || token.Parent is CommandAst) diff --git a/src/PowerShellEditorServices/Utility/HandlerErrorException.cs b/src/PowerShellEditorServices/Utility/HandlerErrorException.cs new file mode 100644 index 000000000..14c3b949e --- /dev/null +++ b/src/PowerShellEditorServices/Utility/HandlerErrorException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.PowerShell.EditorServices.Handlers; + +/// +/// A convenience exception for handlers to throw when a request fails for a normal reason, +/// and to communicate that reason to the user without a full internal stacktrace. +/// +/// The message describing the reason for the request failure. +/// Additional details to be logged regarding the failure. It should be serializable to JSON. +/// The severity level of the message. This is only shown in internal logging. +public class HandlerErrorException +( + string message, + object logDetails = null, + MessageType severity = MessageType.Error +) : RpcErrorException((int)severity, logDetails!, message) +{ } From 70af86337c809f72e400debb3cfc4717b10b2072 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 12 Sep 2024 02:41:05 -0700 Subject: [PATCH 140/203] Move all Handling to OmniSharp LSP types --- .../Server/PsesLanguageServer.cs | 4 +- .../Handlers/PrepareRenameSymbol.cs | 183 ++++++------ .../PowerShell/Handlers/RenameSymbol.cs | 271 +++++++++++------- .../PowerShell/Refactoring/Utilities.cs | 1 + 4 files changed, 255 insertions(+), 204 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 488f1ac07..8b62e85eb 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -123,8 +123,8 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler() - .WithHandler() + .WithHandler() + .WithHandler() // NOTE: The OnInitialize delegate gets run when we first receive the // _Initialize_ request: // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs index 4ea6c0c64..86ad2e2b0 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs @@ -3,121 +3,108 @@ using System.Threading; using System.Threading.Tasks; -using MediatR; using System.Management.Automation.Language; -using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; using Microsoft.PowerShell.EditorServices.Services.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; -namespace Microsoft.PowerShell.EditorServices.Handlers -{ - [Serial, Method("powerShell/PrepareRenameSymbol")] - internal interface IPrepareRenameSymbolHandler : IJsonRpcRequestHandler { } +namespace Microsoft.PowerShell.EditorServices.Handlers; - internal class PrepareRenameSymbolParams : IRequest - { - public string FileName { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string RenameTo { get; set; } - } - internal class PrepareRenameSymbolResult - { - public string message; - } +internal class PrepareRenameHandler(WorkspaceService workspaceService) : IPrepareRenameHandler +{ + public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - internal class PrepareRenameSymbolHandler : IPrepareRenameSymbolHandler + public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) { - private readonly WorkspaceService _workspaceService; + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) + { + throw new HandlerErrorException("Dot Source detected, this is currently not supported"); + } - public PrepareRenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; + int line = request.Position.Line; + int column = request.Position.Character; + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column); - public async Task Handle(PrepareRenameSymbolParams request, CancellationToken cancellationToken) + if (symbol == null) { - if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) - { - // TODO: Unsaved file support. We need to find the unsaved file in the text documents synced to the LSP and use that as our Ast Base. - throw new HandlerErrorException($"File {request.FileName} not found in workspace. Unsaved files currently do not support the rename symbol feature."); - } - return await Task.Run(() => - { - PrepareRenameSymbolResult result = new() - { - message = "" - }; - // ast is FunctionDefinitionAst or CommandAst or VariableExpressionAst or StringConstantExpressionAst && - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(request.Line + 1, request.Column + 1); - Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); + return null; + } + + RangeOrPlaceholderRange symbolRange = new(symbol.NameRegion.ToRange()); - if (token == null) - { - result.message = "Unable to find symbol"; - return result; - } - if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) - { - result.message = "Dot Source detected, this is currently not supported operation aborted"; - return result; - } + Ast token = Utilities.GetAst(line, column, scriptFile.ScriptAst); - bool IsFunction = false; - string tokenName = ""; + return token switch + { + FunctionDefinitionAst => symbolRange, + VariableExpressionAst => symbolRange, + CommandParameterAst => symbolRange, + ParameterAst => symbolRange, + StringConstantExpressionAst stringConstAst when stringConstAst.Parent is CommandAst => symbolRange, + _ => null, + }; - switch (token) - { + // TODO: Reimplement the more specific rename criteria (variables and functions only) - case FunctionDefinitionAst FuncAst: - IsFunction = true; - tokenName = FuncAst.Name; - break; - case VariableExpressionAst or CommandParameterAst or ParameterAst: - IsFunction = false; - tokenName = request.RenameTo; - break; - case StringConstantExpressionAst: + // bool IsFunction = false; + // string tokenName = ""; - if (token.Parent is CommandAst CommAst) - { - IsFunction = true; - tokenName = CommAst.GetCommandName(); - } - else - { - IsFunction = false; - } - break; - } + // switch (token) + // { + // case FunctionDefinitionAst FuncAst: + // IsFunction = true; + // tokenName = FuncAst.Name; + // break; + // case VariableExpressionAst or CommandParameterAst or ParameterAst: + // IsFunction = false; + // tokenName = request.RenameTo; + // break; + // case StringConstantExpressionAst: - if (IsFunction) - { - try - { - IterativeFunctionRename visitor = new(tokenName, - request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptFile.ScriptAst); - } - catch (FunctionDefinitionNotFoundException) - { - result.message = "Failed to Find function definition within current file"; - } - } - else - { - IterativeVariableRename visitor = new(tokenName, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptFile.ScriptAst); - if (visitor.TargetVariableAst == null) - { - result.message = "Failed to find variable definition within the current file"; - } - } - return result; - }).ConfigureAwait(false); - } + // if (token.Parent is CommandAst CommAst) + // { + // IsFunction = true; + // tokenName = CommAst.GetCommandName(); + // } + // else + // { + // IsFunction = false; + // } + // break; + // } + + // if (IsFunction) + // { + // try + // { + // IterativeFunctionRename visitor = new(tokenName, + // request.RenameTo, + // token.Extent.StartLineNumber, + // token.Extent.StartColumnNumber, + // scriptFile.ScriptAst); + // } + // catch (FunctionDefinitionNotFoundException) + // { + // result.message = "Failed to Find function definition within current file"; + // } + // } + // else + // { + // IterativeVariableRename visitor = new(tokenName, + // token.Extent.StartLineNumber, + // token.Extent.StartColumnNumber, + // scriptFile.ScriptAst); + // if (visitor.TargetVariableAst == null) + // { + // result.message = "Failed to find variable definition within the current file"; + // } + // } + // return result; + // }).ConfigureAwait(false); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs index 6e4107ad4..abf25dfe4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs @@ -6,142 +6,205 @@ using System.Threading.Tasks; using MediatR; using System.Management.Automation.Language; -using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; -using System; -namespace Microsoft.PowerShell.EditorServices.Handlers +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using System.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; +namespace Microsoft.PowerShell.EditorServices.Handlers; + +internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler { - [Serial, Method("powerShell/renameSymbol")] - internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } + // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. + public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - public class RenameSymbolOptions - { - public bool CreateAlias { get; set; } - } - public class RenameSymbolParams : IRequest + public async Task Handle(RenameParams request, CancellationToken cancellationToken) { - public string FileName { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string RenameTo { get; set; } - public RenameSymbolOptions Options { get; set; } - } - public class TextChange - { - public string NewText { get; set; } - public int StartLine { get; set; } - public int StartColumn { get; set; } - public int EndLine { get; set; } - public int EndColumn { get; set; } + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + + // AST counts from 1 whereas LSP counts from 0 + int line = request.Position.Line + 1; + int column = request.Position.Character + 1; + + Ast tokenToRename = Utilities.GetAst(line, column, scriptFile.ScriptAst); + + ModifiedFileResponse changes = tokenToRename switch + { + FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), + VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), + _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") + }; + + + // TODO: Update changes to work directly and not require this adapter + TextEdit[] textEdits = changes.Changes.Select(change => new TextEdit + { + Range = new Range + { + Start = new Position { Line = change.StartLine, Character = change.StartColumn }, + End = new Position { Line = change.EndLine, Character = change.EndColumn } + }, + NewText = change.NewText + }).ToArray(); + + return new WorkspaceEdit + { + Changes = new Dictionary> + { + [request.TextDocument.Uri] = textEdits + } + }; } - public class ModifiedFileResponse + + + internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameParams requestParams) { - public string FileName { get; set; } - public List Changes { get; set; } - public ModifiedFileResponse(string fileName) + RenameSymbolParams request = new() + { + FileName = requestParams.TextDocument.Uri.ToString(), + Line = requestParams.Position.Line, + Column = requestParams.Position.Character, + RenameTo = requestParams.NewName + }; + + string tokenName = ""; + if (token is FunctionDefinitionAst funcDef) { - FileName = fileName; - Changes = new List(); + tokenName = funcDef.Name; } - - public void AddTextChange(Ast Symbol, string NewText) + else if (token.Parent is CommandAst CommAst) { - Changes.Add( - new TextChange - { - StartColumn = Symbol.Extent.StartColumnNumber - 1, - StartLine = Symbol.Extent.StartLineNumber - 1, - EndColumn = Symbol.Extent.EndColumnNumber - 1, - EndLine = Symbol.Extent.EndLineNumber - 1, - NewText = NewText - } - ); + tokenName = CommAst.GetCommandName(); } - } - public class RenameSymbolResult - { - public RenameSymbolResult() => Changes = new List(); - public List Changes { get; set; } + IterativeFunctionRename visitor = new(tokenName, + request.RenameTo, + token.Extent.StartLineNumber, + token.Extent.StartColumnNumber, + scriptAst); + visitor.Visit(scriptAst); + ModifiedFileResponse FileModifications = new(request.FileName) + { + Changes = visitor.Modifications + }; + return FileModifications; } - internal class RenameSymbolHandler : IRenameSymbolHandler + internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) { - private readonly WorkspaceService _workspaceService; - - public RenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; - - internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameSymbolParams request) + RenameSymbolParams request = new() { - string tokenName = ""; - if (token is FunctionDefinitionAst funcDef) - { - tokenName = funcDef.Name; - } - else if (token.Parent is CommandAst CommAst) - { - tokenName = CommAst.GetCommandName(); - } - IterativeFunctionRename visitor = new(tokenName, - request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptAst); + FileName = requestParams.TextDocument.Uri.ToString(), + Line = requestParams.Position.Line, + Column = requestParams.Position.Character, + RenameTo = requestParams.NewName + }; + if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) + { + + IterativeVariableRename visitor = new( + request.RenameTo, + symbol.Extent.StartLineNumber, + symbol.Extent.StartColumnNumber, + scriptAst, + request.Options ?? null + ); visitor.Visit(scriptAst); ModifiedFileResponse FileModifications = new(request.FileName) { Changes = visitor.Modifications }; return FileModifications; + } + return null; + } +} - internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameSymbolParams request) - { - if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) - { +// { +// [Serial, Method("powerShell/renameSymbol")] +// internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } - IterativeVariableRename visitor = new( - request.RenameTo, - symbol.Extent.StartLineNumber, - symbol.Extent.StartColumnNumber, - scriptAst, - request.Options ?? null - ); - visitor.Visit(scriptAst); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; +public class RenameSymbolOptions +{ + public bool CreateAlias { get; set; } +} - } - return null; - } +public class RenameSymbolParams : IRequest +{ + public string FileName { get; set; } + public int Line { get; set; } + public int Column { get; set; } + public string RenameTo { get; set; } + public RenameSymbolOptions Options { get; set; } +} +public class TextChange +{ + public string NewText { get; set; } + public int StartLine { get; set; } + public int StartColumn { get; set; } + public int EndLine { get; set; } + public int EndColumn { get; set; } +} +public class ModifiedFileResponse +{ + public string FileName { get; set; } + public List Changes { get; set; } + public ModifiedFileResponse(string fileName) + { + FileName = fileName; + Changes = new List(); + } - public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) - { - if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) + public void AddTextChange(Ast Symbol, string NewText) + { + Changes.Add( + new TextChange { - throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); + StartColumn = Symbol.Extent.StartColumnNumber - 1, + StartLine = Symbol.Extent.StartLineNumber - 1, + EndColumn = Symbol.Extent.EndColumnNumber - 1, + EndLine = Symbol.Extent.EndLineNumber - 1, + NewText = NewText } + ); + } +} +public class RenameSymbolResult +{ + public RenameSymbolResult() => Changes = new List(); + public List Changes { get; set; } +} - return await Task.Run(() => - { - Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); - if (token == null) { return null; } +// internal class RenameSymbolHandler : IRenameSymbolHandler +// { +// private readonly WorkspaceService _workspaceService; - ModifiedFileResponse FileModifications = (token is FunctionDefinitionAst || token.Parent is CommandAst) - ? RenameFunction(token, scriptFile.ScriptAst, request) - : RenameVariable(token, scriptFile.ScriptAst, request); +// public RenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; - RenameSymbolResult result = new(); - result.Changes.Add(FileModifications); - return result; - }).ConfigureAwait(false); - } - } -} + +// public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) +// { +// // if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) +// // { +// // throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); +// // } + +// return await Task.Run(() => +// { +// ScriptFile scriptFile = _workspaceService.GetFile(new Uri(request.FileName)); +// Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); +// if (token == null) { return null; } + +// + +// return result; +// }).ConfigureAwait(false); +// } +// } +// } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 3f42c6d35..9af16328e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -112,6 +112,7 @@ public static bool AssertContainsDotSourced(Ast ScriptAst) } return false; } + public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) { Ast token = null; From bdfec36b194b6b0025fe28ce9aa818155299f63b Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 14 Sep 2024 13:24:28 -0700 Subject: [PATCH 141/203] Move HandlerError --- .../PowerShell/Handlers}/HandlerErrorException.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/PowerShellEditorServices/{Utility => Services/PowerShell/Handlers}/HandlerErrorException.cs (100%) diff --git a/src/PowerShellEditorServices/Utility/HandlerErrorException.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/HandlerErrorException.cs similarity index 100% rename from src/PowerShellEditorServices/Utility/HandlerErrorException.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/HandlerErrorException.cs From d1965820cc79bd1ad92b27e4b473f7a1c1c982ca Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 14 Sep 2024 23:53:17 -0700 Subject: [PATCH 142/203] Rework initial AST filter --- .../Handlers/PrepareRenameSymbol.cs | 110 ------------------ .../{RenameSymbol.cs => RenameHandler.cs} | 104 +++++++++++++++-- .../Refactoring/PrepareRenameHandlerTests.cs | 57 +++++++++ 3 files changed, 153 insertions(+), 118 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs rename src/PowerShellEditorServices/Services/PowerShell/Handlers/{RenameSymbol.cs => RenameHandler.cs} (64%) create mode 100644 test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs deleted file mode 100644 index 86ad2e2b0..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PrepareRenameSymbol.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Threading; -using System.Threading.Tasks; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Refactoring; -using Microsoft.PowerShell.EditorServices.Services.Symbols; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Protocol.Document; -using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; - -namespace Microsoft.PowerShell.EditorServices.Handlers; - -internal class PrepareRenameHandler(WorkspaceService workspaceService) : IPrepareRenameHandler -{ - public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - - public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) - { - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) - { - throw new HandlerErrorException("Dot Source detected, this is currently not supported"); - } - - int line = request.Position.Line; - int column = request.Position.Character; - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column); - - if (symbol == null) - { - return null; - } - - RangeOrPlaceholderRange symbolRange = new(symbol.NameRegion.ToRange()); - - Ast token = Utilities.GetAst(line, column, scriptFile.ScriptAst); - - return token switch - { - FunctionDefinitionAst => symbolRange, - VariableExpressionAst => symbolRange, - CommandParameterAst => symbolRange, - ParameterAst => symbolRange, - StringConstantExpressionAst stringConstAst when stringConstAst.Parent is CommandAst => symbolRange, - _ => null, - }; - - // TODO: Reimplement the more specific rename criteria (variables and functions only) - - // bool IsFunction = false; - // string tokenName = ""; - - // switch (token) - // { - // case FunctionDefinitionAst FuncAst: - // IsFunction = true; - // tokenName = FuncAst.Name; - // break; - // case VariableExpressionAst or CommandParameterAst or ParameterAst: - // IsFunction = false; - // tokenName = request.RenameTo; - // break; - // case StringConstantExpressionAst: - - // if (token.Parent is CommandAst CommAst) - // { - // IsFunction = true; - // tokenName = CommAst.GetCommandName(); - // } - // else - // { - // IsFunction = false; - // } - // break; - // } - - // if (IsFunction) - // { - // try - // { - // IterativeFunctionRename visitor = new(tokenName, - // request.RenameTo, - // token.Extent.StartLineNumber, - // token.Extent.StartColumnNumber, - // scriptFile.ScriptAst); - // } - // catch (FunctionDefinitionNotFoundException) - // { - // result.message = "Failed to Find function definition within current file"; - // } - // } - // else - // { - // IterativeVariableRename visitor = new(tokenName, - // token.Extent.StartLineNumber, - // token.Extent.StartColumnNumber, - // scriptFile.ScriptAst); - // if (visitor.TargetVariableAst == null) - // { - // result.message = "Failed to find variable definition within the current file"; - // } - // } - // return result; - // }).ConfigureAwait(false); - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs similarity index 64% rename from src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs rename to src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index abf25dfe4..c51aa97f9 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameSymbol.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -14,32 +14,109 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using System.Linq; using OmniSharp.Extensions.LanguageServer.Protocol; + namespace Microsoft.PowerShell.EditorServices.Handlers; +/// +/// A handler for textDocument/prepareRename +/// LSP Ref: +/// +internal class PrepareRenameHandler(WorkspaceService workspaceService) : IPrepareRenameHandler +{ + public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); + + public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + + // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. + if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) + { + throw new HandlerErrorException("Dot Source detected, this is currently not supported"); + } + + ScriptPosition scriptPosition = request.Position; + int line = scriptPosition.Line; + int column = scriptPosition.Column; + + // FIXME: Refactor out to utility when working + + // Cannot use generic here as our desired ASTs do not share a common parent + Ast token = scriptFile.ScriptAst.Find(ast => + { + // Supported types, filters out scriptblocks and whatnot + if (ast is not ( + FunctionDefinitionAst + or VariableExpressionAst + or CommandParameterAst + or ParameterAst + or StringConstantExpressionAst + or CommandAst + )) + { + return false; + } + + // Skip all statements that end before our target line or start after our target line + if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } + + // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing + // It's not foolproof but should work in most cases + if (ast is StringConstantExpressionAst stringAst) + { + if (stringAst.Parent is not CommandAst parent) { return false; } + // It will always be the first item in a defined command AST + if (parent.CommandElements[0] != stringAst) { return false; } + } + + Range astRange = new( + ast.Extent.StartLineNumber, + ast.Extent.StartColumnNumber, + ast.Extent.EndLineNumber, + ast.Extent.EndColumnNumber + ); + return astRange.Contains(new Position(line, column)); + }, true); + + if (token is null) { return null; } + + Range astRange = new( + token.Extent.StartLineNumber - 1, + token.Extent.StartColumnNumber - 1, + token.Extent.EndLineNumber - 1, + token.Extent.EndColumnNumber - 1 + ); + + return astRange; + } +} + +/// +/// A handler for textDocument/prepareRename +/// LSP Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename +/// internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler { // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - public async Task Handle(RenameParams request, CancellationToken cancellationToken) { - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - // AST counts from 1 whereas LSP counts from 0 - int line = request.Position.Line + 1; - int column = request.Position.Character + 1; - Ast tokenToRename = Utilities.GetAst(line, column, scriptFile.ScriptAst); + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + ScriptPosition scriptPosition = request.Position; + + Ast tokenToRename = Utilities.GetAst(scriptPosition.Line, scriptPosition.Column, scriptFile.ScriptAst); ModifiedFileResponse changes = tokenToRename switch { FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), + // FIXME: Only throw if capability is not prepareprovider _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; - // TODO: Update changes to work directly and not require this adapter TextEdit[] textEdits = changes.Changes.Select(change => new TextEdit { @@ -60,7 +137,6 @@ public async Task Handle(RenameParams request, CancellationToken }; } - internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameParams requestParams) { RenameSymbolParams request = new() @@ -133,6 +209,15 @@ public class RenameSymbolOptions public bool CreateAlias { get; set; } } +/// +/// Represents a position in a script file. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. +/// +public record ScriptPosition(int Line, int Column) +{ + public static implicit operator ScriptPosition(Position position) => new(position.Line + 1, position.Character + 1); + public static implicit operator Position(ScriptPosition position) => new() { Line = position.Line - 1, Character = position.Column - 1 }; +} + public class RenameSymbolParams : IRequest { public string FileName { get; set; } @@ -141,6 +226,7 @@ public class RenameSymbolParams : IRequest public string RenameTo { get; set; } public RenameSymbolOptions Options { get; set; } } + public class TextChange { public string NewText { get; set; } @@ -149,6 +235,7 @@ public class TextChange public int EndLine { get; set; } public int EndColumn { get; set; } } + public class ModifiedFileResponse { public string FileName { get; set; } @@ -173,6 +260,7 @@ public void AddTextChange(Ast Symbol, string NewText) ); } } + public class RenameSymbolResult { public RenameSymbolResult() => Changes = new List(); diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs new file mode 100644 index 000000000..00eab96af --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#nullable enable + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Xunit; +using static PowerShellEditorServices.Test.Refactoring.RefactorFunctionTests; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; + +namespace PowerShellEditorServices.Handlers.Test; + +[Trait("Category", "PrepareRename")] +public class PrepareRenameHandlerTests : TheoryData +{ + private readonly WorkspaceService workspace = new(NullLoggerFactory.Instance); + private readonly PrepareRenameHandler handler; + public PrepareRenameHandlerTests() + { + workspace.WorkspaceFolders.Add(new WorkspaceFolder + { + Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring")) + }); + handler = new(workspace); + } + + // TODO: Test an untitled document (maybe that belongs in E2E) + + [Theory] + [ClassData(typeof(FunctionRenameTestData))] + public async Task FindsSymbol(RenameSymbolParamsSerialized param) + { + // The test data is the PS script location. The handler expects 0-based line and column numbers. + Position position = new(param.Line - 1, param.Column - 1); + PrepareRenameParams testParams = new() + { + Position = position, + TextDocument = new TextDocumentIdentifier + { + Uri = DocumentUri.FromFileSystemPath( + TestUtilities.GetSharedPath($"Refactoring/Functions/{param.FileName}") + ) + } + }; + + RangeOrPlaceholderRange result = await handler.Handle(testParams, CancellationToken.None); + Assert.NotNull(result); + Assert.NotNull(result.Range); + Assert.True(result.Range.Contains(position)); + } +} From 1d86dccc606824cb4ba883ba46b3243ead036611 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 00:01:34 -0700 Subject: [PATCH 143/203] Reorganize Tests under Handler folder --- .../Refactoring/PrepareRenameHandlerTests.cs | 4 +- .../Refactoring/RefactorFunctionTests.cs | 96 ------------------- .../Refactoring/RefactorVariableTests.cs | 94 ------------------ .../Refactoring/RenameHandlerFunctionTests.cs | 94 ++++++++++++++++++ .../Refactoring/RenameHandlerVariableTests.cs | 92 ++++++++++++++++++ 5 files changed, 188 insertions(+), 192 deletions(-) delete mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs delete mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 00eab96af..5c002491d 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -11,7 +11,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using Xunit; -using static PowerShellEditorServices.Test.Refactoring.RefactorFunctionTests; +using static PowerShellEditorServices.Handlers.Test.RefactorFunctionTests; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; namespace PowerShellEditorServices.Handlers.Test; @@ -30,7 +30,7 @@ public PrepareRenameHandlerTests() handler = new(workspace); } - // TODO: Test an untitled document (maybe that belongs in E2E) + // TODO: Test an untitled document (maybe that belongs in E2E tests) [Theory] [ClassData(typeof(FunctionRenameTestData))] diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs deleted file mode 100644 index 0d1116d5c..000000000 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorFunctionTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Handlers; -using Xunit; -using Microsoft.PowerShell.EditorServices.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Refactoring; -using PowerShellEditorServices.Test.Shared.Refactoring.Functions; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; - -namespace PowerShellEditorServices.Test.Refactoring -{ - - [Trait("Category", "RefactorFunction")] - public class RefactorFunctionTests : IAsyncLifetime - - { - private PsesInternalHost psesHost; - private WorkspaceService workspace; - public async Task InitializeAsync() - { - psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } - - public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); - - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) - { - IterativeFunctionRename iterative = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, - scriptFile.ScriptAst); - iterative.Visit(scriptFile.ScriptAst); - ModifiedFileResponse changes = new(request.FileName) - { - Changes = iterative.Modifications - }; - return GetModifiedScript(scriptFile.Contents, changes); - } - - public class FunctionRenameTestData : TheoryData - { - public FunctionRenameTestData() - { - - // Simple - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionsSingle)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionWithInternalCalls)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCmdlet)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionScriptblock)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCallWIthinStringExpression)); - // Loops - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionLoop)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeach)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeachObject)); - // Nested - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - // Multi Occurance - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); - } - } - - [Theory] - [ClassData(typeof(FunctionRenameTestData))] - public void Rename(RenameSymbolParamsSerialized s) - { - // Arrange - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - // Act - string modifiedcontent = TestRenaming(scriptFile, request, symbol); - - // Assert - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - } -} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs deleted file mode 100644 index f940ccdb8..000000000 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorVariableTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Handlers; -using Xunit; -using PowerShellEditorServices.Test.Shared.Refactoring.Variables; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -using Microsoft.PowerShell.EditorServices.Refactoring; - -namespace PowerShellEditorServices.Test.Refactoring -{ - [Trait("Category", "RenameVariables")] - public class RefactorVariableTests : IAsyncLifetime - - { - private PsesInternalHost psesHost; - private WorkspaceService workspace; - public async Task InitializeAsync() - { - psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } - public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Variables", fileName))); - - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request) - { - - IterativeVariableRename iterative = new(request.RenameTo, - request.Line, - request.Column, - scriptFile.ScriptAst); - iterative.Visit(scriptFile.ScriptAst); - ModifiedFileResponse changes = new(request.FileName) - { - Changes = iterative.Modifications - }; - return GetModifiedScript(scriptFile.Contents, changes); - } - public class VariableRenameTestData : TheoryData - { - public VariableRenameTestData() - { - Add(new RenameSymbolParamsSerialized(RenameVariableData.SimpleVariableAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableRedefinition)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunction)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInLoop)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInPipeline)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblockScoped)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariablewWithinHastableExpression)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedFunctionScriptblock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinCommandAstScriptBlock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinForeachObject)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableusedInWhileLoop)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInParam)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameter)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterReverse)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableScriptWithParamBlock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNonParam)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableParameterCommandWithSameName)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromCommandAst)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromSplat)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunctionRefactorInner)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromOuterVar)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromInnerFunction)); - } - } - - [Theory] - [ClassData(typeof(VariableRenameTestData))] - public void Rename(RenameSymbolParamsSerialized s) - { - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - } - } - -} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs new file mode 100644 index 000000000..b728faab4 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Refactoring; +using PowerShellEditorServices.Test.Shared.Refactoring.Functions; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; + +namespace PowerShellEditorServices.Handlers.Test; + +[Trait("Category", "RenameHandlerFunction")] +public class RefactorFunctionTests : IAsyncLifetime +{ + private PsesInternalHost psesHost; + private WorkspaceService workspace; + public async Task InitializeAsync() + { + psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); + } + + public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); + + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) + { + IterativeFunctionRename iterative = new(symbol.NameRegion.Text, + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); + iterative.Visit(scriptFile.ScriptAst); + ModifiedFileResponse changes = new(request.FileName) + { + Changes = iterative.Modifications + }; + return GetModifiedScript(scriptFile.Contents, changes); + } + + public class FunctionRenameTestData : TheoryData + { + public FunctionRenameTestData() + { + + // Simple + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionsSingle)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionWithInternalCalls)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCmdlet)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionScriptblock)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCallWIthinStringExpression)); + // Loops + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionLoop)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeach)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeachObject)); + // Nested + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); + // Multi Occurance + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); + Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); + } + } + + [Theory] + [ClassData(typeof(FunctionRenameTestData))] + public void Rename(RenameSymbolParamsSerialized s) + { + // Arrange + RenameSymbolParamsSerialized request = s; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( + request.Line, + request.Column); + // Act + string modifiedcontent = TestRenaming(scriptFile, request, symbol); + + // Assert + Assert.Equal(expectedContent.Contents, modifiedcontent); + } +} + diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs new file mode 100644 index 000000000..ecfecd8a8 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using PowerShellEditorServices.Test.Shared.Refactoring.Variables; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; +using Microsoft.PowerShell.EditorServices.Refactoring; + +namespace PowerShellEditorServices.Handlers.Test; + +[Trait("Category", "RenameHandlerVariable")] +public class RefactorVariableTests : IAsyncLifetime + +{ + private PsesInternalHost psesHost; + private WorkspaceService workspace; + public async Task InitializeAsync() + { + psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance); + } + public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); + private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Variables", fileName))); + + internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request) + { + + IterativeVariableRename iterative = new(request.RenameTo, + request.Line, + request.Column, + scriptFile.ScriptAst); + iterative.Visit(scriptFile.ScriptAst); + ModifiedFileResponse changes = new(request.FileName) + { + Changes = iterative.Modifications + }; + return GetModifiedScript(scriptFile.Contents, changes); + } + public class VariableRenameTestData : TheoryData + { + public VariableRenameTestData() + { + Add(new RenameSymbolParamsSerialized(RenameVariableData.SimpleVariableAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableRedefinition)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunction)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInLoop)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInPipeline)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblockScoped)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariablewWithinHastableExpression)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedFunctionScriptblock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinCommandAstScriptBlock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinForeachObject)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableusedInWhileLoop)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInParam)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameter)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterReverse)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableScriptWithParamBlock)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNonParam)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableParameterCommandWithSameName)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromCommandAst)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromSplat)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunctionRefactorInner)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromOuterVar)); + Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromInnerFunction)); + } + } + + [Theory] + [ClassData(typeof(VariableRenameTestData))] + public void Rename(RenameSymbolParamsSerialized s) + { + RenameSymbolParamsSerialized request = s; + ScriptFile scriptFile = GetTestScript(request.FileName); + ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); + + string modifiedcontent = TestRenaming(scriptFile, request); + + Assert.Equal(expectedContent.Contents, modifiedcontent); + } +} From d2d060cd9136de550609be581754f8dbb769e5fc Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 12:23:57 -0700 Subject: [PATCH 144/203] Lots of removing of custom types. Currently broken until I create a ScriptExtent/Position Adapter --- .../PowerShell/Handlers/RenameHandler.cs | 165 +++++------------- .../Refactoring/IterativeFunctionVistor.cs | 28 +-- .../Refactoring/IterativeVariableVisitor.cs | 54 +++--- .../PowerShell/Refactoring/Utilities.cs | 22 +++ .../Handlers/CompletionHandler.cs | 4 +- .../Handlers/FormattingHandlers.cs | 4 +- .../Refactoring/RefactorUtilities.cs | 50 ++++-- .../Refactoring/RenameHandlerFunctionTests.cs | 15 +- .../Refactoring/RenameHandlerVariableTests.cs | 7 +- 9 files changed, 150 insertions(+), 199 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index c51aa97f9..c9161e5dd 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -12,7 +12,6 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; -using System.Linq; using OmniSharp.Extensions.LanguageServer.Protocol; namespace Microsoft.PowerShell.EditorServices.Handlers; @@ -41,6 +40,23 @@ public async Task Handle(PrepareRenameParams request, C // FIXME: Refactor out to utility when working + Ast token = FindRenamableSymbol(scriptFile, line, column); + + if (token is null) { return null; } + + // TODO: Really should have a class with implicit convertors handing these conversions to avoid off-by-one mistakes. + return Utilities.ToRange(token.Extent); ; + } + + /// + /// Finds a renamable symbol at a given position in a script file. + /// + /// + /// 1-based line number + /// 1-based column number + /// Ast of the token or null if no renamable symbol was found + internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPosition position) + { // Cannot use generic here as our desired ASTs do not share a common parent Ast token = scriptFile.ScriptAst.Find(ast => { @@ -57,16 +73,15 @@ or CommandAst return false; } - // Skip all statements that end before our target line or start after our target line + // Skip all statements that end before our target line or start after our target line. This is a performance optimization. if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing - // It's not foolproof but should work in most cases + // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) if (ast is StringConstantExpressionAst stringAst) { if (stringAst.Parent is not CommandAst parent) { return false; } - // It will always be the first item in a defined command AST - if (parent.CommandElements[0] != stringAst) { return false; } + if (parent.GetCommandName() != stringAst.Value) { return false; } } Range astRange = new( @@ -75,19 +90,9 @@ or CommandAst ast.Extent.EndLineNumber, ast.Extent.EndColumnNumber ); - return astRange.Contains(new Position(line, column)); + return astRange.Contains(position); }, true); - - if (token is null) { return null; } - - Range astRange = new( - token.Extent.StartLineNumber - 1, - token.Extent.StartColumnNumber - 1, - token.Extent.EndLineNumber - 1, - token.Extent.EndColumnNumber - 1 - ); - - return astRange; + return token; } } @@ -102,14 +107,13 @@ internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler public async Task Handle(RenameParams request, CancellationToken cancellationToken) { - - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); ScriptPosition scriptPosition = request.Position; - Ast tokenToRename = Utilities.GetAst(scriptPosition.Line, scriptPosition.Column, scriptFile.ScriptAst); + Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, scriptPosition.Line, scriptPosition.Column); - ModifiedFileResponse changes = tokenToRename switch + // TODO: Potentially future cross-file support + TextEdit[] changes = tokenToRename switch { FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), @@ -117,27 +121,18 @@ public async Task Handle(RenameParams request, CancellationToken _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; - // TODO: Update changes to work directly and not require this adapter - TextEdit[] textEdits = changes.Changes.Select(change => new TextEdit - { - Range = new Range - { - Start = new Position { Line = change.StartLine, Character = change.StartColumn }, - End = new Position { Line = change.EndLine, Character = change.EndColumn } - }, - NewText = change.NewText - }).ToArray(); - return new WorkspaceEdit { Changes = new Dictionary> { - [request.TextDocument.Uri] = textEdits + [request.TextDocument.Uri] = changes } }; } - internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, RenameParams requestParams) + // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading + + internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams requestParams) { RenameSymbolParams request = new() { @@ -162,14 +157,10 @@ internal static ModifiedFileResponse RenameFunction(Ast token, Ast scriptAst, Re token.Extent.StartColumnNumber, scriptAst); visitor.Visit(scriptAst); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; + return visitor.Modifications.ToArray(); } - internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) + internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) { RenameSymbolParams request = new() { @@ -189,33 +180,37 @@ internal static ModifiedFileResponse RenameVariable(Ast symbol, Ast scriptAst, R request.Options ?? null ); visitor.Visit(scriptAst); - ModifiedFileResponse FileModifications = new(request.FileName) - { - Changes = visitor.Modifications - }; - return FileModifications; + return visitor.Modifications.ToArray(); } return null; } } -// { -// [Serial, Method("powerShell/renameSymbol")] -// internal interface IRenameSymbolHandler : IJsonRpcRequestHandler { } - public class RenameSymbolOptions { public bool CreateAlias { get; set; } } /// -/// Represents a position in a script file. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. +/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default constructor is 1-based. /// -public record ScriptPosition(int Line, int Column) +internal record ScriptPosition(int Line, int Column) { public static implicit operator ScriptPosition(Position position) => new(position.Line + 1, position.Character + 1); public static implicit operator Position(ScriptPosition position) => new() { Line = position.Line - 1, Character = position.Column - 1 }; + + internal ScriptPosition Delta(int LineAdjust, int ColumnAdjust) => new( + Line + LineAdjust, + Column + ColumnAdjust + ); +} + +internal record ScriptRange(ScriptPosition Start, ScriptPosition End) +{ + // Positions will adjust per ScriptPosition + public static implicit operator ScriptRange(Range range) => new(range.Start, range.End); + public static implicit operator Range(ScriptRange range) => new() { Start = range.Start, End = range.End }; } public class RenameSymbolParams : IRequest @@ -227,72 +222,8 @@ public class RenameSymbolParams : IRequest public RenameSymbolOptions Options { get; set; } } -public class TextChange -{ - public string NewText { get; set; } - public int StartLine { get; set; } - public int StartColumn { get; set; } - public int EndLine { get; set; } - public int EndColumn { get; set; } -} - -public class ModifiedFileResponse -{ - public string FileName { get; set; } - public List Changes { get; set; } - public ModifiedFileResponse(string fileName) - { - FileName = fileName; - Changes = new List(); - } - - public void AddTextChange(Ast Symbol, string NewText) - { - Changes.Add( - new TextChange - { - StartColumn = Symbol.Extent.StartColumnNumber - 1, - StartLine = Symbol.Extent.StartLineNumber - 1, - EndColumn = Symbol.Extent.EndColumnNumber - 1, - EndLine = Symbol.Extent.EndLineNumber - 1, - NewText = NewText - } - ); - } -} - public class RenameSymbolResult { - public RenameSymbolResult() => Changes = new List(); - public List Changes { get; set; } + public RenameSymbolResult() => Changes = new List(); + public List Changes { get; set; } } - -// internal class RenameSymbolHandler : IRenameSymbolHandler -// { -// private readonly WorkspaceService _workspaceService; - -// public RenameSymbolHandler(WorkspaceService workspaceService) => _workspaceService = workspaceService; - - - - -// public async Task Handle(RenameSymbolParams request, CancellationToken cancellationToken) -// { -// // if (!_workspaceService.TryGetFile(request.FileName, out ScriptFile scriptFile)) -// // { -// // throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); -// // } - -// return await Task.Run(() => -// { -// ScriptFile scriptFile = _workspaceService.GetFile(new Uri(request.FileName)); -// Ast token = Utilities.GetAst(request.Line + 1, request.Column + 1, scriptFile.ScriptAst); -// if (token == null) { return null; } - -// - -// return result; -// }).ConfigureAwait(false); -// } -// } -// } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 36a8536d9..402f73d9e 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -12,7 +12,7 @@ internal class IterativeFunctionRename { private readonly string OldName; private readonly string NewName; - public List Modifications = new(); + public List Modifications = []; internal int StartLineNumber; internal int StartColumnNumber; internal FunctionDefinitionAst TargetFunctionAst; @@ -150,16 +150,19 @@ public void ProcessNode(Ast node, bool shouldRename) ast.Extent.StartColumnNumber == StartColumnNumber) { TargetFunctionAst = ast; - TextChange Change = new() + TextEdit change = new() { NewText = NewName, - StartLine = ast.Extent.StartLineNumber - 1, - StartColumn = ast.Extent.StartColumnNumber + "function ".Length - 1, - EndLine = ast.Extent.StartLineNumber - 1, - EndColumn = ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1, + // FIXME: Introduce adapter class to avoid off-by-one errors + Range = new( + ast.Extent.StartLineNumber - 1, + ast.Extent.StartColumnNumber + "function ".Length - 1, + ast.Extent.StartLineNumber - 1, + ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1 + ), }; - Modifications.Add(Change); + Modifications.Add(change); //node.ShouldRename = true; } else @@ -176,15 +179,12 @@ public void ProcessNode(Ast node, bool shouldRename) { if (shouldRename) { - TextChange Change = new() + TextEdit change = new() { NewText = NewName, - StartLine = ast.Extent.StartLineNumber - 1, - StartColumn = ast.Extent.StartColumnNumber - 1, - EndLine = ast.Extent.StartLineNumber - 1, - EndColumn = ast.Extent.StartColumnNumber + OldName.Length - 1, + Range = Utilities.ToRange(ast.Extent), }; - Modifications.Add(Change); + Modifications.Add(change); } } break; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 2a11dce88..d04d17caa 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -6,6 +6,7 @@ using Microsoft.PowerShell.EditorServices.Handlers; using System.Linq; using System; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring { @@ -15,7 +16,7 @@ internal class IterativeVariableRename private readonly string OldName; private readonly string NewName; internal bool ShouldRename; - public List Modifications = new(); + public List Modifications = []; internal int StartLineNumber; internal int StartColumnNumber; internal VariableExpressionAst TargetVariableAst; @@ -396,19 +397,16 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi if (ShouldRename) { // have some modifications to account for the dollar sign prefix powershell uses for variables - TextChange Change = new() + TextEdit Change = new() { NewText = NewName.Contains("$") ? NewName : "$" + NewName, - StartLine = variableExpressionAst.Extent.StartLineNumber - 1, - StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1, - EndLine = variableExpressionAst.Extent.StartLineNumber - 1, - EndColumn = variableExpressionAst.Extent.StartColumnNumber + OldName.Length, + Range = Utilities.ToRange(variableExpressionAst.Extent), }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && options.CreateAlias) { - TextChange aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); + TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); AliasSet = true; } @@ -431,13 +429,10 @@ private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) { - TextChange Change = new() + TextEdit Change = new() { NewText = NewName.Contains("-") ? NewName : "-" + NewName, - StartLine = commandParameterAst.Extent.StartLineNumber - 1, - StartColumn = commandParameterAst.Extent.StartColumnNumber - 1, - EndLine = commandParameterAst.Extent.StartLineNumber - 1, - EndColumn = commandParameterAst.Extent.StartColumnNumber + OldName.Length, + Range = Utilities.ToRange(commandParameterAst.Extent) }; Modifications.Add(Change); } @@ -468,13 +463,10 @@ assignmentStatementAst.Right is CommandExpressionAst commExpAst && if (element.Item1 is StringConstantExpressionAst strConstAst && strConstAst.Value.ToLower() == OldName.ToLower()) { - TextChange Change = new() + TextEdit Change = new() { NewText = NewName, - StartLine = strConstAst.Extent.StartLineNumber - 1, - StartColumn = strConstAst.Extent.StartColumnNumber - 1, - EndLine = strConstAst.Extent.StartLineNumber - 1, - EndColumn = strConstAst.Extent.EndColumnNumber - 1, + Range = Utilities.ToRange(strConstAst.Extent) }; Modifications.Add(Change); @@ -485,13 +477,14 @@ assignmentStatementAst.Right is CommandExpressionAst commExpAst && } } - internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) + internal TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) { // Check if an Alias AttributeAst already exists and append the new Alias to the existing list // Otherwise Create a new Alias Attribute // Add the modifications to the changes // The Attribute will be appended before the variable or in the existing location of the original alias - TextChange aliasChange = new(); + TextEdit aliasChange = new(); + // FIXME: Understand this more, if this returns more than one result, why does it overwrite the aliasChange? foreach (Ast Attr in paramAst.Attributes) { if (Attr is AttributeAst AttrAst) @@ -504,24 +497,21 @@ internal TextChange NewParameterAliasChange(VariableExpressionAst variableExpres existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); string nentries = existingEntries + $", \"{OldName}\""; - aliasChange.NewText = $"[Alias({nentries})]"; - aliasChange.StartLine = Attr.Extent.StartLineNumber - 1; - aliasChange.StartColumn = Attr.Extent.StartColumnNumber - 1; - aliasChange.EndLine = Attr.Extent.StartLineNumber - 1; - aliasChange.EndColumn = Attr.Extent.EndColumnNumber - 1; - - break; + aliasChange = aliasChange with + { + NewText = $"[Alias({nentries})]", + Range = Utilities.ToRange(AttrAst.Extent) + }; } - } } if (aliasChange.NewText == null) { - aliasChange.NewText = $"[Alias(\"{OldName}\")]"; - aliasChange.StartLine = variableExpressionAst.Extent.StartLineNumber - 1; - aliasChange.StartColumn = variableExpressionAst.Extent.StartColumnNumber - 1; - aliasChange.EndLine = variableExpressionAst.Extent.StartLineNumber - 1; - aliasChange.EndColumn = variableExpressionAst.Extent.StartColumnNumber - 1; + aliasChange = aliasChange with + { + NewText = $"[Alias(\"{OldName}\")]", + Range = Utilities.ToRange(paramAst.Extent) + }; } return aliasChange; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index 9af16328e..ca788c3b2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -5,11 +5,33 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring { internal class Utilities { + /// + /// Helper function to convert 1-based script positions to zero-based LSP positions + /// + /// + /// + public static Range ToRange(IScriptExtent extent) + { + return new Range + { + Start = new Position + { + Line = extent.StartLineNumber - 1, + Character = extent.StartColumnNumber - 1 + }, + End = new Position + { + Line = extent.EndLineNumber - 1, + Character = extent.EndColumnNumber - 1 + } + }; + } public static Ast GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) { diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index 29e36ce25..153d5d330 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -270,7 +270,7 @@ internal CompletionItem CreateCompletionItem( { Validate.IsNotNull(nameof(result), result); - TextEdit textEdit = new() + OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit textEdit = new() { NewText = result.CompletionText, Range = new Range @@ -374,7 +374,7 @@ private CompletionItem CreateProviderItemCompletion( } InsertTextFormat insertFormat; - TextEdit edit; + OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit edit; CompletionItemKind itemKind; if (result.ResultType is CompletionResultType.ProviderContainer && SupportsSnippets diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs index 64ccb3156..bf5f99d0f 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/FormattingHandlers.cs @@ -90,7 +90,7 @@ public override async Task Handle(DocumentFormattingParams re return s_emptyTextEditContainer; } - return new TextEditContainer(new TextEdit + return new TextEditContainer(new OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit { NewText = formattedScript, Range = editRange @@ -184,7 +184,7 @@ public override async Task Handle(DocumentRangeFormattingPara return s_emptyTextEditContainer; } - return new TextEditContainer(new TextEdit + return new TextEditContainer(new OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit { NewText = formattedScript, Range = editRange diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs index c21b9aa0e..4d68f3c3e 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -5,34 +5,50 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Xunit.Abstractions; using MediatR; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using System.Linq; +using System.Collections.Generic; +using TextEditRange = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; namespace PowerShellEditorServices.Test.Refactoring { - public class RefactorUtilities - + internal class TextEditComparer : IComparer { - - internal static string GetModifiedScript(string OriginalScript, ModifiedFileResponse Modification) + public int Compare(TextEdit a, TextEdit b) { - Modification.Changes.Sort((a, b) => - { - if (b.StartLine == a.StartLine) - { - return b.EndColumn - a.EndColumn; - } - return b.StartLine - a.StartLine; + return a.Range.Start.Line == b.Range.Start.Line + ? b.Range.End.Character - a.Range.End.Character + : b.Range.Start.Line - a.Range.Start.Line; + } + } - }); + public class RefactorUtilities + { + /// + /// A simplistic "Mock" implementation of vscode client performing rename activities. It is not comprehensive and an E2E test is recommended. + /// + /// + /// + /// + internal static string GetModifiedScript(string OriginalScript, TextEdit[] Modifications) + { string[] Lines = OriginalScript.Split( new string[] { Environment.NewLine }, StringSplitOptions.None); - foreach (TextChange change in Modification.Changes) + // FIXME: Verify that we should be returning modifications in ascending order anyways as the LSP spec dictates it + IEnumerable sortedModifications = Modifications.OrderBy + ( + x => x, new TextEditComparer() + ); + + foreach (TextEdit change in sortedModifications) { - string TargetLine = Lines[change.StartLine]; - string begin = TargetLine.Substring(0, change.StartColumn); - string end = TargetLine.Substring(change.EndColumn); - Lines[change.StartLine] = begin + change.NewText + end; + TextEditRange editRange = change.Range; + string TargetLine = Lines[editRange.Start.Line]; + string begin = TargetLine.Substring(0, editRange.Start.Character); + string end = TargetLine.Substring(editRange.End.Character); + Lines[editRange.Start.Line] = begin + change.NewText + end; } return string.Join(Environment.NewLine, Lines); diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs index b728faab4..c82bfb43f 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs @@ -9,12 +9,12 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Test; using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Refactoring; using PowerShellEditorServices.Test.Shared.Refactoring.Functions; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace PowerShellEditorServices.Handlers.Test; @@ -32,18 +32,15 @@ public async Task InitializeAsync() public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) + internal static string GetRenamedFunctionScriptContent(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) { - IterativeFunctionRename iterative = new(symbol.NameRegion.Text, + IterativeFunctionRename visitor = new(symbol.NameRegion.Text, request.RenameTo, symbol.ScriptRegion.StartLineNumber, symbol.ScriptRegion.StartColumnNumber, scriptFile.ScriptAst); - iterative.Visit(scriptFile.ScriptAst); - ModifiedFileResponse changes = new(request.FileName) - { - Changes = iterative.Modifications - }; + visitor.Visit(scriptFile.ScriptAst); + TextEdit[] changes = visitor.Modifications.ToArray(); return GetModifiedScript(scriptFile.Contents, changes); } @@ -85,7 +82,7 @@ public void Rename(RenameSymbolParamsSerialized s) request.Line, request.Column); // Act - string modifiedcontent = TestRenaming(scriptFile, request, symbol); + string modifiedcontent = GetRenamedFunctionScriptContent(scriptFile, request, symbol); // Assert Assert.Equal(expectedContent.Contents, modifiedcontent); diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs index ecfecd8a8..7ac177ee1 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs @@ -9,7 +9,6 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Test; using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Handlers; using Xunit; using PowerShellEditorServices.Test.Shared.Refactoring.Variables; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; @@ -39,11 +38,7 @@ internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSer request.Column, scriptFile.ScriptAst); iterative.Visit(scriptFile.ScriptAst); - ModifiedFileResponse changes = new(request.FileName) - { - Changes = iterative.Modifications - }; - return GetModifiedScript(scriptFile.Contents, changes); + return GetModifiedScript(scriptFile.Contents, iterative.Modifications.ToArray()); } public class VariableRenameTestData : TheoryData { From 7ef4dfabbf9bad56bac1b99590cbedee119c49f2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 13:36:28 -0700 Subject: [PATCH 145/203] Rework RenameHandler to use Adapters --- .../PowerShell/Handlers/RenameHandler.cs | 127 +++++++++++++----- .../Utility/IScriptExtentExtensions.cs | 13 ++ 2 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index c9161e5dd..84d40a69d 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -13,6 +13,8 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol; +using System; +using PowerShellEditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Handlers; @@ -34,14 +36,8 @@ public async Task Handle(PrepareRenameParams request, C throw new HandlerErrorException("Dot Source detected, this is currently not supported"); } - ScriptPosition scriptPosition = request.Position; - int line = scriptPosition.Line; - int column = scriptPosition.Column; - - // FIXME: Refactor out to utility when working - - Ast token = FindRenamableSymbol(scriptFile, line, column); - + ScriptPositionAdapter position = request.Position; + Ast token = FindRenamableSymbol(scriptFile, position); if (token is null) { return null; } // TODO: Really should have a class with implicit convertors handing these conversions to avoid off-by-one mistakes. @@ -49,17 +45,29 @@ public async Task Handle(PrepareRenameParams request, C } /// - /// Finds a renamable symbol at a given position in a script file. - /// + /// Finds a renamable symbol at a given position in a script file using 1-based row/column references /// /// 1-based line number /// 1-based column number + /// + internal static Ast FindRenamableSymbol(ScriptFile scriptFile, int line, int column) => + FindRenamableSymbol(scriptFile, new ScriptPositionAdapter(line, column)); + + /// + /// Finds a renamable symbol at a given position in a script file. + /// /// Ast of the token or null if no renamable symbol was found - internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPosition position) + internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) { + int line = position.Line; + int column = position.Column; + // Cannot use generic here as our desired ASTs do not share a common parent Ast token = scriptFile.ScriptAst.Find(ast => { + // Skip all statements that end before our target line or start after our target line. This is a performance optimization. + if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } + // Supported types, filters out scriptblocks and whatnot if (ast is not ( FunctionDefinitionAst @@ -73,9 +81,6 @@ or CommandAst return false; } - // Skip all statements that end before our target line or start after our target line. This is a performance optimization. - if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } - // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) if (ast is StringConstantExpressionAst stringAst) @@ -84,13 +89,7 @@ or CommandAst if (parent.GetCommandName() != stringAst.Value) { return false; } } - Range astRange = new( - ast.Extent.StartLineNumber, - ast.Extent.StartColumnNumber, - ast.Extent.EndLineNumber, - ast.Extent.EndColumnNumber - ); - return astRange.Contains(position); + return ast.Extent.Contains(position); }, true); return token; } @@ -108,9 +107,9 @@ internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler public async Task Handle(RenameParams request, CancellationToken cancellationToken) { ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - ScriptPosition scriptPosition = request.Position; + ScriptPositionAdapter position = request.Position; - Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, scriptPosition.Line, scriptPosition.Column); + Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, position); // TODO: Potentially future cross-file support TextEdit[] changes = tokenToRename switch @@ -193,24 +192,84 @@ public class RenameSymbolOptions } /// -/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default constructor is 1-based. +/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default line/column constructor is 1-based. /// -internal record ScriptPosition(int Line, int Column) +public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable, IComparable, IComparable { - public static implicit operator ScriptPosition(Position position) => new(position.Line + 1, position.Character + 1); - public static implicit operator Position(ScriptPosition position) => new() { Line = position.Line - 1, Character = position.Column - 1 }; + public int Line => position.LineNumber; + public int Column => position.ColumnNumber; + public int Character => position.ColumnNumber; + public int LineNumber => position.LineNumber; + public int ColumnNumber => position.ColumnNumber; + + public string File => position.File; + string IScriptPosition.Line => position.Line; + public int Offset => position.Offset; + + public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } + public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } + + public static implicit operator ScriptPositionAdapter(Position position) => new(position); + public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); + + public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new(scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1); + public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; - internal ScriptPosition Delta(int LineAdjust, int ColumnAdjust) => new( - Line + LineAdjust, - Column + ColumnAdjust + internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( + position.LineNumber + LineAdjust, + position.ColumnNumber + ColumnAdjust ); + + public int CompareTo(ScriptPositionAdapter other) + { + if (position.LineNumber == other.position.LineNumber) + { + return position.ColumnNumber.CompareTo(other.position.ColumnNumber); + } + return position.LineNumber.CompareTo(other.position.LineNumber); + } + public int CompareTo(Position other) => CompareTo((ScriptPositionAdapter)other); + public int CompareTo(ScriptPosition other) => CompareTo((ScriptPositionAdapter)other); + public string GetFullScript() => throw new NotImplementedException(); } -internal record ScriptRange(ScriptPosition Start, ScriptPosition End) +/// +/// Represents a range in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default ScriptExtent constructor is 1-based +/// +/// +internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent { - // Positions will adjust per ScriptPosition - public static implicit operator ScriptRange(Range range) => new(range.Start, range.End); - public static implicit operator Range(ScriptRange range) => new() { Start = range.Start, End = range.End }; + public readonly ScriptPositionAdapter Start = new(extent.StartScriptPosition); + public readonly ScriptPositionAdapter End = new(extent.StartScriptPosition); + + public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); + public static implicit operator ScriptExtent(ScriptExtentAdapter extent) => extent; + + public static implicit operator Range(ScriptExtentAdapter extent) => new() + { + // Will get shifted to 0-based + Start = extent.Start, + End = extent.End + }; + public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( + // Will get shifted to 1-based + new ScriptPositionAdapter(range.Start), + new ScriptPositionAdapter(range.End) + )); + + public IScriptPosition StartScriptPosition => Start; + public IScriptPosition EndScriptPosition => End; + public int EndColumnNumber => End.ColumnNumber; + public int EndLineNumber => End.LineNumber; + public int StartOffset => extent.EndOffset; + public int EndOffset => extent.EndOffset; + public string File => extent.File; + public int StartColumnNumber => extent.StartColumnNumber; + public int StartLineNumber => extent.StartLineNumber; + public string Text => extent.Text; + + public bool Contains(Position position) => ContainsPosition(this, position); + public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); } public class RenameSymbolParams : IRequest diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs new file mode 100644 index 000000000..2db8a5a4f --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; + +namespace PowerShellEditorServices.Services.PowerShell.Utility +{ + public static class IScriptExtentExtensions + { + public static bool Contains(this IScriptExtent extent, ScriptPositionAdapter position) => ScriptExtentAdapter.ContainsPosition(new(extent), position); + } +} From 36df0f0a3c2672ad890da67c12d35437c3d97eb7 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 13:58:05 -0700 Subject: [PATCH 146/203] Fix namespacing --- .../PowerShell/Handlers/RenameHandler.cs | 23 ++++++++----------- .../Refactoring/PrepareRenameHandlerTests.cs | 4 ++-- .../Refactoring/RenameHandlerFunctionTests.cs | 12 +++++----- .../Refactoring/RenameHandlerVariableTests.cs | 2 +- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index 84d40a69d..c405e67b2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -110,6 +110,7 @@ public async Task Handle(RenameParams request, CancellationToken ScriptPositionAdapter position = request.Position; Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, position); + if (tokenToRename is null) { return null; } // TODO: Potentially future cross-file support TextEdit[] changes = tokenToRename switch @@ -131,15 +132,9 @@ public async Task Handle(RenameParams request, CancellationToken // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading - internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams requestParams) + internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams renameParams) { - RenameSymbolParams request = new() - { - FileName = requestParams.TextDocument.Uri.ToString(), - Line = requestParams.Position.Line, - Column = requestParams.Position.Character, - RenameTo = requestParams.NewName - }; + ScriptPositionAdapter position = renameParams.Position; string tokenName = ""; if (token is FunctionDefinitionAst funcDef) @@ -150,11 +145,13 @@ internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams { tokenName = CommAst.GetCommandName(); } - IterativeFunctionRename visitor = new(tokenName, - request.RenameTo, - token.Extent.StartLineNumber, - token.Extent.StartColumnNumber, - scriptAst); + IterativeFunctionRename visitor = new( + tokenName, + renameParams.NewName, + position.Line, + position.Column, + scriptAst + ); visitor.Visit(scriptAst); return visitor.Modifications.ToArray(); } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 5c002491d..9398fb71d 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -11,10 +11,10 @@ using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using Xunit; -using static PowerShellEditorServices.Handlers.Test.RefactorFunctionTests; +using static PowerShellEditorServices.Test.Handlers.RefactorFunctionTests; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -namespace PowerShellEditorServices.Handlers.Test; +namespace PowerShellEditorServices.Test.Handlers; [Trait("Category", "PrepareRename")] public class PrepareRenameHandlerTests : TheoryData diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs index c82bfb43f..0e9861376 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs @@ -16,7 +16,7 @@ using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -namespace PowerShellEditorServices.Handlers.Test; +namespace PowerShellEditorServices.Test.Handlers; [Trait("Category", "RenameHandlerFunction")] public class RefactorFunctionTests : IAsyncLifetime @@ -74,17 +74,17 @@ public FunctionRenameTestData() [ClassData(typeof(FunctionRenameTestData))] public void Rename(RenameSymbolParamsSerialized s) { - // Arrange RenameSymbolParamsSerialized request = s; ScriptFile scriptFile = GetTestScript(request.FileName); ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column); - // Act + request.Line, + request.Column + ); + string modifiedcontent = GetRenamedFunctionScriptContent(scriptFile, request, symbol); - // Assert + Assert.Equal(expectedContent.Contents, modifiedcontent); } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs index 7ac177ee1..43944fc72 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs @@ -14,7 +14,7 @@ using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; using Microsoft.PowerShell.EditorServices.Refactoring; -namespace PowerShellEditorServices.Handlers.Test; +namespace PowerShellEditorServices.Test.Handlers; [Trait("Category", "RenameHandlerVariable")] public class RefactorVariableTests : IAsyncLifetime From b5035c1c1b8d9703cc2f2e260d9fc74881a8c0df Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 13:58:23 -0700 Subject: [PATCH 147/203] Remove Alias from tests for now, will have separate Alias test in future --- .../Refactoring/Variables/VariableCommandParameterRenamed.ps1 | 2 +- .../Variables/VariableCommandParameterSplattedRenamed.ps1 | 2 +- .../Refactoring/Variables/VariableInParamRenamed.ps1 | 2 +- .../Variables/VariableParameterCommndWithSameNameRenamed.ps1 | 2 +- .../Variables/VariableScriptWithParamBlockRenamed.ps1 | 2 +- .../Variables/VariableSimpleFunctionParameterRenamed.ps1 | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 index 1e6ac9d0f..e74504a4d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 @@ -1,6 +1,6 @@ function Get-foo { param ( - [string][Alias("string")]$Renamed, + [string]$Renamed, [int]$pos ) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 index c799fd852..f89b69118 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 @@ -1,6 +1,6 @@ function New-User { param ( - [string][Alias("Username")]$Renamed, + [string]$Renamed, [string]$password ) write-host $Renamed + $password diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 index 4f567188c..2a810e887 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 @@ -19,7 +19,7 @@ function Write-Item($itemCount) { # Do-Work will be underlined in green if you haven't disable script analysis. # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. -function Do-Work([Alias("workCount")]$Renamed) { +function Do-Work($Renamed) { Write-Output "Doing work..." Write-Item $Renamed Write-Host "Done!" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 index 9c88a44d4..1f5bcc598 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 @@ -1,7 +1,7 @@ function Test-AADConnected { param ( - [Parameter(Mandatory = $false)][Alias("UPName", "UserPrincipalName")][String]$Renamed + [Parameter(Mandatory = $false)][String]$Renamed ) Begin {} Process { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 index e218fce9f..ba0ae7702 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 @@ -1,4 +1,4 @@ -param([int]$Count=50, [int][Alias("DelayMilliSeconds")]$Renamed=200) +param([int]$Count = 50, [int]$Renamed = 200) function Write-Item($itemCount) { $i = 1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 index 250d360ca..12af8cd08 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 @@ -3,7 +3,7 @@ $x = 1..10 function testing_files { param ( - [Alias("x")]$Renamed + $Renamed ) write-host "Printing $Renamed" } From 2d15151ddfb77caa6fab1814a79648f02f450786 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 14:04:19 -0700 Subject: [PATCH 148/203] Default CreateAlias to false per feedback --- .../Services/PowerShell/Refactoring/IterativeVariableVisitor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index d04d17caa..59fc337a8 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -32,7 +32,7 @@ public IterativeVariableRename(string NewName, int StartLineNumber, int StartCol this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.options = options ?? new RenameSymbolOptions { CreateAlias = true }; + this.options = options ?? new RenameSymbolOptions { CreateAlias = false }; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) From 0505be7ebe6f475262a739fa42048b49636c3c62 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 16:10:54 -0700 Subject: [PATCH 149/203] Reworked Visitor to use ScriptPositionAdapter --- .../PowerShell/Handlers/RenameHandler.cs | 14 +++++++-- .../Refactoring/IterativeFunctionVistor.cs | 29 ++++++++++++++----- .../Refactoring/RenameHandlerFunctionTests.cs | 10 +++---- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index c405e67b2..a8c1a97b5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -204,12 +204,14 @@ public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, public int Offset => position.Offset; public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } + public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } public static implicit operator ScriptPositionAdapter(Position position) => new(position); public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new(scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1); + public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( @@ -236,17 +238,23 @@ public int CompareTo(ScriptPositionAdapter other) /// internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent { - public readonly ScriptPositionAdapter Start = new(extent.StartScriptPosition); - public readonly ScriptPositionAdapter End = new(extent.StartScriptPosition); + public ScriptPositionAdapter Start = new(extent.StartScriptPosition); + public ScriptPositionAdapter End = new(extent.EndScriptPosition); public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); public static implicit operator ScriptExtent(ScriptExtentAdapter extent) => extent; public static implicit operator Range(ScriptExtentAdapter extent) => new() { - // Will get shifted to 0-based Start = extent.Start, End = extent.End + // End = extent.End with + // { + // // The end position in Script Extents is actually shifted an additional 1 column, no idea why + // // https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.iscriptextent.endscriptposition?view=powershellsdk-7.4.0#system-management-automation-language-iscriptextent-endscriptposition + + // position = (extent.EndScriptPosition as ScriptPositionAdapter).Delta(0, -1) + // } }; public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( // Will get shifted to 1-based diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 402f73d9e..03dfd44c2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.IO; using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Handlers; using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring @@ -150,16 +152,24 @@ public void ProcessNode(Ast node, bool shouldRename) ast.Extent.StartColumnNumber == StartColumnNumber) { TargetFunctionAst = ast; + int functionPrefixLength = "function ".Length; + int functionNameStartColumn = ast.Extent.StartColumnNumber + functionPrefixLength; + TextEdit change = new() { NewText = NewName, - // FIXME: Introduce adapter class to avoid off-by-one errors + // HACK: Because we cannot get a token extent of the function name itself, we have to adjust to find it here + // TOOD: Parse the upfront and use offsets probably to get the function name token Range = new( - ast.Extent.StartLineNumber - 1, - ast.Extent.StartColumnNumber + "function ".Length - 1, - ast.Extent.StartLineNumber - 1, - ast.Extent.StartColumnNumber + "function ".Length + ast.Name.Length - 1 - ), + new ScriptPositionAdapter( + ast.Extent.StartLineNumber, + functionNameStartColumn + ), + new ScriptPositionAdapter( + ast.Extent.StartLineNumber, + functionNameStartColumn + OldName.Length + ) + ) }; Modifications.Add(change); @@ -179,10 +189,15 @@ public void ProcessNode(Ast node, bool shouldRename) { if (shouldRename) { + // What we weant to rename is actually the first token of the command + if (ast.CommandElements[0] is not StringConstantExpressionAst funcName) + { + throw new InvalidDataException("Command element should always have a string expresssion as its first item. This is a bug and you should report it."); + } TextEdit change = new() { NewText = NewName, - Range = Utilities.ToRange(ast.Extent), + Range = new ScriptExtentAdapter(funcName.Extent) }; Modifications.Add(change); } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs index 0e9861376..ede640c46 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs @@ -35,10 +35,10 @@ public async Task InitializeAsync() internal static string GetRenamedFunctionScriptContent(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) { IterativeFunctionRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, - scriptFile.ScriptAst); + request.RenameTo, + symbol.ScriptRegion.StartLineNumber, + symbol.ScriptRegion.StartColumnNumber, + scriptFile.ScriptAst); visitor.Visit(scriptFile.ScriptAst); TextEdit[] changes = visitor.Modifications.ToArray(); return GetModifiedScript(scriptFile.Contents, changes); @@ -83,8 +83,6 @@ public void Rename(RenameSymbolParamsSerialized s) ); string modifiedcontent = GetRenamedFunctionScriptContent(scriptFile, request, symbol); - - Assert.Equal(expectedContent.Contents, modifiedcontent); } } From 554d4c1f08275f8d143ad2bbf5c4e9025658632f Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 16:20:00 -0700 Subject: [PATCH 150/203] Fixup tests with default PowerShell Formatting --- .../Variables/RefactorVariablesData.cs | 4 ++-- ...> VariableParameterCommandWithSameName.ps1} | 18 +++++++++--------- ...bleParameterCommandWithSameNameRenamed.ps1} | 16 ++++++++-------- .../Variables/VariableScriptWithParamBlock.ps1 | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VariableParameterCommndWithSameName.ps1 => VariableParameterCommandWithSameName.ps1} (66%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VariableParameterCommndWithSameNameRenamed.ps1 => VariableParameterCommandWithSameNameRenamed.ps1} (68%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs index ab166b165..9d9e63fdf 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs @@ -117,7 +117,7 @@ internal static class RenameVariableData public static readonly RenameSymbolParams VariableScriptWithParamBlock = new() { FileName = "VariableScriptWithParamBlock.ps1", - Column = 28, + Column = 30, Line = 1, RenameTo = "Renamed" }; @@ -130,7 +130,7 @@ internal static class RenameVariableData }; public static readonly RenameSymbolParams VariableParameterCommandWithSameName = new() { - FileName = "VariableParameterCommndWithSameName.ps1", + FileName = "VariableParameterCommandWithSameName.ps1", Column = 13, Line = 9, RenameTo = "Renamed" diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameName.ps1 similarity index 66% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameName.ps1 index 650271316..88d091f84 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameName.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameName.ps1 @@ -1,7 +1,7 @@ function Test-AADConnected { param ( - [Parameter(Mandatory = $false)][Alias("UPName")][String]$UserPrincipalName + [Parameter(Mandatory = $false)][String]$UserPrincipalName ) Begin {} Process { @@ -16,10 +16,10 @@ function Test-AADConnected { } function Set-MSolUMFA{ - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string]$UserPrincipalName, - [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateSet('Enabled','Disabled','Enforced')][String]$StrongAuthenticationRequiremets + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)][string]$UserPrincipalName, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)][ValidateSet('Enabled', 'Disabled', 'Enforced')][String]$StrongAuthenticationRequiremets ) begin{ # Check if connected to Msol Session already @@ -29,11 +29,11 @@ function Set-MSolUMFA{ Write-Verbose('Initiating connection to Msol') Connect-MsolService -ErrorAction Stop Write-Verbose('Connected to Msol successfully') - }catch{ + } catch{ return Write-Error($_.Exception.Message) } } - if(!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ + if (!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ return Write-Error('Insufficient permissions to set MFA') } } @@ -41,16 +41,16 @@ function Set-MSolUMFA{ # Get the time and calc 2 min to the future $TimeStart = Get-Date $TimeEnd = $timeStart.addminutes(1) - $Finished=$false + $Finished = $false #Loop to check if the user exists already - if ($PSCmdlet.ShouldProcess($UserPrincipalName, "StrongAuthenticationRequiremets = "+$StrongAuthenticationRequiremets)) { + if ($PSCmdlet.ShouldProcess($UserPrincipalName, 'StrongAuthenticationRequiremets = ' + $StrongAuthenticationRequiremets)) { } } End{} } Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop -$UserPrincipalName = "Bob" +$UserPrincipalName = 'Bob' if ($UserPrincipalName) { $SplatTestAADConnected.Add('UserPrincipalName', $UserPrincipalName) } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameNameRenamed.ps1 similarity index 68% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameNameRenamed.ps1 index 1f5bcc598..fb21baa6e 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommndWithSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableParameterCommandWithSameNameRenamed.ps1 @@ -16,10 +16,10 @@ function Test-AADConnected { } function Set-MSolUMFA{ - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string]$UserPrincipalName, - [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)][ValidateSet('Enabled','Disabled','Enforced')][String]$StrongAuthenticationRequiremets + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)][string]$UserPrincipalName, + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)][ValidateSet('Enabled', 'Disabled', 'Enforced')][String]$StrongAuthenticationRequiremets ) begin{ # Check if connected to Msol Session already @@ -29,11 +29,11 @@ function Set-MSolUMFA{ Write-Verbose('Initiating connection to Msol') Connect-MsolService -ErrorAction Stop Write-Verbose('Connected to Msol successfully') - }catch{ + } catch{ return Write-Error($_.Exception.Message) } } - if(!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ + if (!(Get-MsolUser -MaxResults 1 -ErrorAction Stop)){ return Write-Error('Insufficient permissions to set MFA') } } @@ -41,16 +41,16 @@ function Set-MSolUMFA{ # Get the time and calc 2 min to the future $TimeStart = Get-Date $TimeEnd = $timeStart.addminutes(1) - $Finished=$false + $Finished = $false #Loop to check if the user exists already - if ($PSCmdlet.ShouldProcess($UserPrincipalName, "StrongAuthenticationRequiremets = "+$StrongAuthenticationRequiremets)) { + if ($PSCmdlet.ShouldProcess($UserPrincipalName, 'StrongAuthenticationRequiremets = ' + $StrongAuthenticationRequiremets)) { } } End{} } Set-MsolUser -UserPrincipalName $UPN -StrongAuthenticationRequirements $sta -ErrorAction Stop -$UserPrincipalName = "Bob" +$UserPrincipalName = 'Bob' if ($UserPrincipalName) { $SplatTestAADConnected.Add('UserPrincipalName', $UserPrincipalName) } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 index c3175bd0d..ff874d121 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 @@ -1,4 +1,4 @@ -param([int]$Count=50, [int]$DelayMilliSeconds=200) +param([int]$Count = 50, [int]$DelayMilliSeconds = 200) function Write-Item($itemCount) { $i = 1 From 58fab45b74da2b5acf8756ad9f9b6eece19cc0a3 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 16:49:37 -0700 Subject: [PATCH 151/203] Refine VariableVisitor to use ScriptExtentAdapter --- .../PowerShell/Handlers/RenameHandler.cs | 25 +++++++------------ .../Refactoring/IterativeVariableVisitor.cs | 10 ++++---- .../PowerShell/Refactoring/Utilities.cs | 23 ----------------- 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index a8c1a97b5..aa6e326ab 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -41,7 +41,7 @@ public async Task Handle(PrepareRenameParams request, C if (token is null) { return null; } // TODO: Really should have a class with implicit convertors handing these conversions to avoid off-by-one mistakes. - return Utilities.ToRange(token.Extent); ; + return new ScriptExtentAdapter(token.Extent); } /// @@ -211,7 +211,6 @@ public ScriptPositionAdapter(Position position) : this(position.Line + 1, positi public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new(scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1); - public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( @@ -242,26 +241,20 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public ScriptPositionAdapter End = new(extent.EndScriptPosition); public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); - public static implicit operator ScriptExtent(ScriptExtentAdapter extent) => extent; - - public static implicit operator Range(ScriptExtentAdapter extent) => new() - { - Start = extent.Start, - End = extent.End - // End = extent.End with - // { - // // The end position in Script Extents is actually shifted an additional 1 column, no idea why - // // https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.iscriptextent.endscriptposition?view=powershellsdk-7.4.0#system-management-automation-language-iscriptextent-endscriptposition - - // position = (extent.EndScriptPosition as ScriptPositionAdapter).Delta(0, -1) - // } - }; public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( // Will get shifted to 1-based new ScriptPositionAdapter(range.Start), new ScriptPositionAdapter(range.End) )); + public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; + public static implicit operator Range(ScriptExtentAdapter adapter) => new() + { + Start = adapter.Start, + End = adapter.End + }; + public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new(adapter); + public IScriptPosition StartScriptPosition => Start; public IScriptPosition EndScriptPosition => End; public int EndColumnNumber => End.ColumnNumber; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 59fc337a8..52ae87c25 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -400,7 +400,7 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi TextEdit Change = new() { NewText = NewName.Contains("$") ? NewName : "$" + NewName, - Range = Utilities.ToRange(variableExpressionAst.Extent), + Range = new ScriptExtentAdapter(variableExpressionAst.Extent), }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && @@ -432,7 +432,7 @@ private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) TextEdit Change = new() { NewText = NewName.Contains("-") ? NewName : "-" + NewName, - Range = Utilities.ToRange(commandParameterAst.Extent) + Range = new ScriptExtentAdapter(commandParameterAst.Extent) }; Modifications.Add(Change); } @@ -466,7 +466,7 @@ assignmentStatementAst.Right is CommandExpressionAst commExpAst && TextEdit Change = new() { NewText = NewName, - Range = Utilities.ToRange(strConstAst.Extent) + Range = new ScriptExtentAdapter(strConstAst.Extent) }; Modifications.Add(Change); @@ -500,7 +500,7 @@ internal TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressi aliasChange = aliasChange with { NewText = $"[Alias({nentries})]", - Range = Utilities.ToRange(AttrAst.Extent) + Range = new ScriptExtentAdapter(AttrAst.Extent) }; } } @@ -510,7 +510,7 @@ internal TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressi aliasChange = aliasChange with { NewText = $"[Alias(\"{OldName}\")]", - Range = Utilities.ToRange(paramAst.Extent) + Range = new ScriptExtentAdapter(paramAst.Extent) }; } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs index ca788c3b2..2f441d02b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs @@ -5,34 +5,11 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring { internal class Utilities { - /// - /// Helper function to convert 1-based script positions to zero-based LSP positions - /// - /// - /// - public static Range ToRange(IScriptExtent extent) - { - return new Range - { - Start = new Position - { - Line = extent.StartLineNumber - 1, - Character = extent.StartColumnNumber - 1 - }, - End = new Position - { - Line = extent.EndLineNumber - 1, - Character = extent.EndColumnNumber - 1 - } - }; - } - public static Ast GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) { Ast result = null; From 7254b6fc96c26a524df162c69b51994caf12f797 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 15 Sep 2024 21:39:44 -0700 Subject: [PATCH 152/203] Extract some duplicate functions and enable NRT checking for RenameHandler --- .../PowerShell/Handlers/RenameHandler.cs | 68 ++++++++++++++----- .../Refactoring/PrepareRenameHandlerTests.cs | 2 +- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index aa6e326ab..aba366ddd 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable using System.Collections.Generic; using System.Threading; @@ -14,10 +15,10 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol; using System; -using PowerShellEditorServices.Services.PowerShell.Utility; namespace Microsoft.PowerShell.EditorServices.Handlers; + /// /// A handler for textDocument/prepareRename /// LSP Ref: @@ -26,7 +27,7 @@ internal class PrepareRenameHandler(WorkspaceService workspaceService) : IPrepar { public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) + public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) { ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); @@ -37,11 +38,28 @@ public async Task Handle(PrepareRenameParams request, C } ScriptPositionAdapter position = request.Position; - Ast token = FindRenamableSymbol(scriptFile, position); - if (token is null) { return null; } + Ast target = FindRenamableSymbol(scriptFile, position); + if (target is null) { return null; } + return target switch + { + FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), + _ => new ScriptExtentAdapter(target.Extent) + }; + } + + private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) + { + string name = ast.Name; + // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present + int funcLength = "function ".Length; + ScriptExtentAdapter funcExtent = new(ast.Extent); - // TODO: Really should have a class with implicit convertors handing these conversions to avoid off-by-one mistakes. - return new ScriptExtentAdapter(token.Extent); + // Get a range that represents only the function name + return funcExtent with + { + Start = funcExtent.Start.Delta(0, funcLength), + End = funcExtent.Start.Delta(0, funcLength + name.Length) + }; } /// @@ -89,8 +107,15 @@ or CommandAst if (parent.GetCommandName() != stringAst.Value) { return false; } } - return ast.Extent.Contains(position); + ScriptExtentAdapter target = ast switch + { + FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), + _ => new ScriptExtentAdapter(ast.Extent) + }; + + return target.Contains(position); }, true); + return token; } } @@ -104,7 +129,7 @@ internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); - public async Task Handle(RenameParams request, CancellationToken cancellationToken) + public async Task Handle(RenameParams request, CancellationToken cancellationToken) { ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); ScriptPositionAdapter position = request.Position; @@ -179,7 +204,7 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam return visitor.Modifications.ToArray(); } - return null; + return []; } } @@ -205,12 +230,16 @@ public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } - public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } + public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } public static implicit operator ScriptPositionAdapter(Position position) => new(position); - public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); + public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new + ( + scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 + ); - public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new(scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1); + + public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( @@ -241,19 +270,22 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public ScriptPositionAdapter End = new(extent.EndScriptPosition); public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); + public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( // Will get shifted to 1-based new ScriptPositionAdapter(range.Start), new ScriptPositionAdapter(range.End) )); - - public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; public static implicit operator Range(ScriptExtentAdapter adapter) => new() { + // Will get shifted to 0-based Start = adapter.Start, End = adapter.End }; - public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new(adapter); + + public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; + + public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter); public IScriptPosition StartScriptPosition => Start; public IScriptPosition EndScriptPosition => End; @@ -272,11 +304,11 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public class RenameSymbolParams : IRequest { - public string FileName { get; set; } + public string? FileName { get; set; } public int Line { get; set; } public int Column { get; set; } - public string RenameTo { get; set; } - public RenameSymbolOptions Options { get; set; } + public string? RenameTo { get; set; } + public RenameSymbolOptions? Options { get; set; } } public class RenameSymbolResult diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 9398fb71d..81d93e446 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -49,7 +49,7 @@ public async Task FindsSymbol(RenameSymbolParamsSerialized param) } }; - RangeOrPlaceholderRange result = await handler.Handle(testParams, CancellationToken.None); + RangeOrPlaceholderRange? result = await handler.Handle(testParams, CancellationToken.None); Assert.NotNull(result); Assert.NotNull(result.Range); Assert.True(result.Range.Contains(position)); From 57ca6848d0fbc08e69248648311abff713a3d420 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 16 Sep 2024 07:59:32 -0700 Subject: [PATCH 153/203] Add initial EUA prompt scaffolding --- .../PowerShell/Handlers/RenameHandler.cs | 27 +++++++- .../Refactoring/PrepareRenameHandlerTests.cs | 63 ++++++++++++++++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs index aba366ddd..9a74c3655 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs @@ -2,11 +2,12 @@ // Licensed under the MIT License. #nullable enable +using System; using System.Collections.Generic; +using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; using MediatR; -using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; @@ -14,7 +15,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol; -using System; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Handlers; @@ -23,12 +24,32 @@ namespace Microsoft.PowerShell.EditorServices.Handlers; /// A handler for textDocument/prepareRename /// LSP Ref: /// -internal class PrepareRenameHandler(WorkspaceService workspaceService) : IPrepareRenameHandler +internal class PrepareRenameHandler(WorkspaceService workspaceService, ILanguageServerFacade lsp, ILanguageServerConfiguration config) : IPrepareRenameHandler { public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) { + // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied + config.ToString(); + ShowMessageRequestParams reqParams = new ShowMessageRequestParams + { + Type = MessageType.Warning, + Message = "Test Send", + Actions = new MessageActionItem[] { + new MessageActionItem() { Title = "I Accept" }, + new MessageActionItem() { Title = "I Accept [Workspace]" }, + new MessageActionItem() { Title = "Decline" } + } + }; + + MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); + if (result.Title == "Test Action") + { + // FIXME: Need to accept + Console.WriteLine("yay"); + } + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 81d93e446..b662c67e2 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -1,15 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable - +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Test.Shared; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Progress; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using Xunit; using static PowerShellEditorServices.Test.Handlers.RefactorFunctionTests; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; @@ -27,7 +35,9 @@ public PrepareRenameHandlerTests() { Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring")) }); - handler = new(workspace); + // FIXME: Need to make a Mock to pass to the ExtensionService constructor + + handler = new(workspace, new fakeLspSendMessageRequestFacade("I Accept"), new fakeConfigurationService()); } // TODO: Test an untitled document (maybe that belongs in E2E tests) @@ -55,3 +65,52 @@ public async Task FindsSymbol(RenameSymbolParamsSerialized param) Assert.True(result.Range.Contains(position)); } } + +public class fakeLspSendMessageRequestFacade(string title) : ILanguageServerFacade +{ + public async Task SendRequest(IRequest request, CancellationToken cancellationToken) + { + if (request is ShowMessageRequestParams) + { + return (TResponse)(object)new MessageActionItem { Title = title }; + } + else + { + throw new NotSupportedException(); + } + } + + public ITextDocumentLanguageServer TextDocument => throw new NotImplementedException(); + public INotebookDocumentLanguageServer NotebookDocument => throw new NotImplementedException(); + public IClientLanguageServer Client => throw new NotImplementedException(); + public IGeneralLanguageServer General => throw new NotImplementedException(); + public IWindowLanguageServer Window => throw new NotImplementedException(); + public IWorkspaceLanguageServer Workspace => throw new NotImplementedException(); + public IProgressManager ProgressManager => throw new NotImplementedException(); + public InitializeParams ClientSettings => throw new NotImplementedException(); + public InitializeResult ServerSettings => throw new NotImplementedException(); + public object GetService(Type serviceType) => throw new NotImplementedException(); + public IDisposable Register(Action registryAction) => throw new NotImplementedException(); + public void SendNotification(string method) => throw new NotImplementedException(); + public void SendNotification(string method, T @params) => throw new NotImplementedException(); + public void SendNotification(IRequest request) => throw new NotImplementedException(); + public IResponseRouterReturns SendRequest(string method) => throw new NotImplementedException(); + public IResponseRouterReturns SendRequest(string method, T @params) => throw new NotImplementedException(); + public bool TryGetRequest(long id, out string method, out TaskCompletionSource pendingTask) => throw new NotImplementedException(); +} + +public class fakeConfigurationService : ILanguageServerConfiguration +{ + public string this[string key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public bool IsSupported => throw new NotImplementedException(); + + public ILanguageServerConfiguration AddConfigurationItems(IEnumerable configurationItems) => throw new NotImplementedException(); + public IEnumerable GetChildren() => throw new NotImplementedException(); + public Task GetConfiguration(params ConfigurationItem[] items) => throw new NotImplementedException(); + public IChangeToken GetReloadToken() => throw new NotImplementedException(); + public Task GetScopedConfiguration(DocumentUri scopeUri, CancellationToken cancellationToken) => throw new NotImplementedException(); + public IConfigurationSection GetSection(string key) => throw new NotImplementedException(); + public ILanguageServerConfiguration RemoveConfigurationItems(IEnumerable configurationItems) => throw new NotImplementedException(); + public bool TryGetScopedConfiguration(DocumentUri scopeUri, out IScopedConfiguration configuration) => throw new NotImplementedException(); +} From 6e196bee493b03c218542345e5695e4d77c8bba4 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 16 Sep 2024 21:08:06 -0700 Subject: [PATCH 154/203] Move RenameHandler to TextDocument (more appropriate context) --- .../{PowerShell => TextDocument}/Handlers/RenameHandler.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/PowerShellEditorServices/Services/{PowerShell => TextDocument}/Handlers/RenameHandler.cs (100%) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs similarity index 100% rename from src/PowerShellEditorServices/Services/PowerShell/Handlers/RenameHandler.cs rename to src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs From d87ae85b3af70baf3fe36f7bf0c64f1ac8a3263b Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 16 Sep 2024 21:10:27 -0700 Subject: [PATCH 155/203] Remove Unnecessary code --- .../TextDocument/Handlers/RenameHandler.cs | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs index 9a74c3655..797a45730 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs @@ -7,7 +7,6 @@ using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; -using MediatR; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Refactoring; @@ -19,7 +18,6 @@ namespace Microsoft.PowerShell.EditorServices.Handlers; - /// /// A handler for textDocument/prepareRename /// LSP Ref: @@ -83,15 +81,6 @@ private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst a }; } - /// - /// Finds a renamable symbol at a given position in a script file using 1-based row/column references - /// - /// 1-based line number - /// 1-based column number - /// - internal static Ast FindRenamableSymbol(ScriptFile scriptFile, int line, int column) => - FindRenamableSymbol(scriptFile, new ScriptPositionAdapter(line, column)); - /// /// Finds a renamable symbol at a given position in a script file. /// @@ -204,22 +193,15 @@ internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) { - RenameSymbolParams request = new() - { - FileName = requestParams.TextDocument.Uri.ToString(), - Line = requestParams.Position.Line, - Column = requestParams.Position.Character, - RenameTo = requestParams.NewName - }; if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { IterativeVariableRename visitor = new( - request.RenameTo, + requestParams.NewName, symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst, - request.Options ?? null + null //FIXME: Pass through Alias config ); visitor.Visit(scriptAst); return visitor.Modifications.ToArray(); @@ -322,18 +304,3 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public bool Contains(Position position) => ContainsPosition(this, position); public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); } - -public class RenameSymbolParams : IRequest -{ - public string? FileName { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string? RenameTo { get; set; } - public RenameSymbolOptions? Options { get; set; } -} - -public class RenameSymbolResult -{ - public RenameSymbolResult() => Changes = new List(); - public List Changes { get; set; } -} From 690119f2a455600bce8baab09eb7b3ff20c1ec13 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 16 Sep 2024 23:47:05 -0700 Subject: [PATCH 156/203] Split out RenameService. **TESTS NEED FIXING** --- .../Refactoring/IterativeFunctionVistor.cs | 2 +- .../Refactoring/IterativeVariableVisitor.cs | 2 +- .../Utility/IScriptExtentExtensions.cs | 2 +- .../TextDocument/Handlers/RenameHandler.cs | 285 +---------- .../TextDocument/Services/RenameService.cs | 469 ++++++++++++++++++ 5 files changed, 484 insertions(+), 276 deletions(-) create mode 100644 src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs index 03dfd44c2..aa1e84609 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace Microsoft.PowerShell.EditorServices.Refactoring diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 52ae87c25..14cf50a7a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; using System.Linq; using System; using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Microsoft.PowerShell.EditorServices.Services; namespace Microsoft.PowerShell.EditorServices.Refactoring { diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs index 2db8a5a4f..25fd74349 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services; namespace PowerShellEditorServices.Services.PowerShell.Utility { diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs index 797a45730..061eb334e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs @@ -2,19 +2,15 @@ // Licensed under the MIT License. #nullable enable -using System; -using System.Collections.Generic; -using System.Management.Automation.Language; + using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Refactoring; + using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; -using OmniSharp.Extensions.LanguageServer.Protocol; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; + namespace Microsoft.PowerShell.EditorServices.Handlers; @@ -22,285 +18,28 @@ namespace Microsoft.PowerShell.EditorServices.Handlers; /// A handler for textDocument/prepareRename /// LSP Ref: /// -internal class PrepareRenameHandler(WorkspaceService workspaceService, ILanguageServerFacade lsp, ILanguageServerConfiguration config) : IPrepareRenameHandler +internal class PrepareRenameHandler +( + IRenameService renameService +) : IPrepareRenameHandler { public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); public async Task Handle(PrepareRenameParams request, CancellationToken cancellationToken) - { - // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied - config.ToString(); - ShowMessageRequestParams reqParams = new ShowMessageRequestParams - { - Type = MessageType.Warning, - Message = "Test Send", - Actions = new MessageActionItem[] { - new MessageActionItem() { Title = "I Accept" }, - new MessageActionItem() { Title = "I Accept [Workspace]" }, - new MessageActionItem() { Title = "Decline" } - } - }; - - MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); - if (result.Title == "Test Action") - { - // FIXME: Need to accept - Console.WriteLine("yay"); - } - - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - - // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. - if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) - { - throw new HandlerErrorException("Dot Source detected, this is currently not supported"); - } - - ScriptPositionAdapter position = request.Position; - Ast target = FindRenamableSymbol(scriptFile, position); - if (target is null) { return null; } - return target switch - { - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), - _ => new ScriptExtentAdapter(target.Extent) - }; - } - - private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) - { - string name = ast.Name; - // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present - int funcLength = "function ".Length; - ScriptExtentAdapter funcExtent = new(ast.Extent); - - // Get a range that represents only the function name - return funcExtent with - { - Start = funcExtent.Start.Delta(0, funcLength), - End = funcExtent.Start.Delta(0, funcLength + name.Length) - }; - } - - /// - /// Finds a renamable symbol at a given position in a script file. - /// - /// Ast of the token or null if no renamable symbol was found - internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) - { - int line = position.Line; - int column = position.Column; - - // Cannot use generic here as our desired ASTs do not share a common parent - Ast token = scriptFile.ScriptAst.Find(ast => - { - // Skip all statements that end before our target line or start after our target line. This is a performance optimization. - if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } - - // Supported types, filters out scriptblocks and whatnot - if (ast is not ( - FunctionDefinitionAst - or VariableExpressionAst - or CommandParameterAst - or ParameterAst - or StringConstantExpressionAst - or CommandAst - )) - { - return false; - } - - // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing - // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) - if (ast is StringConstantExpressionAst stringAst) - { - if (stringAst.Parent is not CommandAst parent) { return false; } - if (parent.GetCommandName() != stringAst.Value) { return false; } - } - - ScriptExtentAdapter target = ast switch - { - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), - _ => new ScriptExtentAdapter(ast.Extent) - }; - - return target.Contains(position); - }, true); - - return token; - } + => await renameService.PrepareRenameSymbol(request, cancellationToken).ConfigureAwait(false); } /// /// A handler for textDocument/prepareRename /// LSP Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename /// -internal class RenameHandler(WorkspaceService workspaceService) : IRenameHandler +internal class RenameHandler( + IRenameService renameService +) : IRenameHandler { // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); public async Task Handle(RenameParams request, CancellationToken cancellationToken) - { - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - ScriptPositionAdapter position = request.Position; - - Ast tokenToRename = PrepareRenameHandler.FindRenamableSymbol(scriptFile, position); - if (tokenToRename is null) { return null; } - - // TODO: Potentially future cross-file support - TextEdit[] changes = tokenToRename switch - { - FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), - VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), - // FIXME: Only throw if capability is not prepareprovider - _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") - }; - - return new WorkspaceEdit - { - Changes = new Dictionary> - { - [request.TextDocument.Uri] = changes - } - }; - } - - // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading - - internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams renameParams) - { - ScriptPositionAdapter position = renameParams.Position; - - string tokenName = ""; - if (token is FunctionDefinitionAst funcDef) - { - tokenName = funcDef.Name; - } - else if (token.Parent is CommandAst CommAst) - { - tokenName = CommAst.GetCommandName(); - } - IterativeFunctionRename visitor = new( - tokenName, - renameParams.NewName, - position.Line, - position.Column, - scriptAst - ); - visitor.Visit(scriptAst); - return visitor.Modifications.ToArray(); - } - - internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) - { - if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) - { - - IterativeVariableRename visitor = new( - requestParams.NewName, - symbol.Extent.StartLineNumber, - symbol.Extent.StartColumnNumber, - scriptAst, - null //FIXME: Pass through Alias config - ); - visitor.Visit(scriptAst); - return visitor.Modifications.ToArray(); - - } - return []; - } -} - -public class RenameSymbolOptions -{ - public bool CreateAlias { get; set; } -} - -/// -/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default line/column constructor is 1-based. -/// -public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable, IComparable, IComparable -{ - public int Line => position.LineNumber; - public int Column => position.ColumnNumber; - public int Character => position.ColumnNumber; - public int LineNumber => position.LineNumber; - public int ColumnNumber => position.ColumnNumber; - - public string File => position.File; - string IScriptPosition.Line => position.Line; - public int Offset => position.Offset; - - public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } - public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } - - public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } - public static implicit operator ScriptPositionAdapter(Position position) => new(position); - public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new - ( - scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 - ); - - - public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); - public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; - - internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( - position.LineNumber + LineAdjust, - position.ColumnNumber + ColumnAdjust - ); - - public int CompareTo(ScriptPositionAdapter other) - { - if (position.LineNumber == other.position.LineNumber) - { - return position.ColumnNumber.CompareTo(other.position.ColumnNumber); - } - return position.LineNumber.CompareTo(other.position.LineNumber); - } - public int CompareTo(Position other) => CompareTo((ScriptPositionAdapter)other); - public int CompareTo(ScriptPosition other) => CompareTo((ScriptPositionAdapter)other); - public string GetFullScript() => throw new NotImplementedException(); -} - -/// -/// Represents a range in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default ScriptExtent constructor is 1-based -/// -/// -internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent -{ - public ScriptPositionAdapter Start = new(extent.StartScriptPosition); - public ScriptPositionAdapter End = new(extent.EndScriptPosition); - - public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); - - public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( - // Will get shifted to 1-based - new ScriptPositionAdapter(range.Start), - new ScriptPositionAdapter(range.End) - )); - public static implicit operator Range(ScriptExtentAdapter adapter) => new() - { - // Will get shifted to 0-based - Start = adapter.Start, - End = adapter.End - }; - - public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; - - public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter); - - public IScriptPosition StartScriptPosition => Start; - public IScriptPosition EndScriptPosition => End; - public int EndColumnNumber => End.ColumnNumber; - public int EndLineNumber => End.LineNumber; - public int StartOffset => extent.EndOffset; - public int EndOffset => extent.EndOffset; - public string File => extent.File; - public int StartColumnNumber => extent.StartColumnNumber; - public int StartLineNumber => extent.StartLineNumber; - public string Text => extent.Text; - - public bool Contains(Position position) => ContainsPosition(this, position); - public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); + => await renameService.RenameSymbol(request, cancellationToken).ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs new file mode 100644 index 000000000..0c7dc3bb8 --- /dev/null +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Refactoring; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices.Services; + +public interface IRenameService +{ + /// + /// Implementation of textDocument/prepareRename + /// + public Task PrepareRenameSymbol(PrepareRenameParams prepareRenameParams, CancellationToken cancellationToken); + + /// + /// Implementation of textDocument/rename + /// + public Task RenameSymbol(RenameParams renameParams, CancellationToken cancellationToken); +} + +/// +/// Providers service for renaming supported symbols such as functions and variables. +/// +internal class RenameService( + WorkspaceService workspaceService, + ILanguageServerFacade lsp, + ILanguageServerConfiguration config +) : IRenameService +{ + public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) + { + // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied + config.ToString(); + ShowMessageRequestParams reqParams = new() + { + Type = MessageType.Warning, + Message = "Test Send", + Actions = new MessageActionItem[] { + new MessageActionItem() { Title = "I Accept" }, + new MessageActionItem() { Title = "I Accept [Workspace]" }, + new MessageActionItem() { Title = "Decline" } + } + }; + + MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); + if (result.Title == "Test Action") + { + // FIXME: Need to accept + Console.WriteLine("yay"); + } + + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + + // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. + if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) + { + throw new HandlerErrorException("Dot Source detected, this is currently not supported"); + } + + ScriptPositionAdapter position = request.Position; + Ast target = FindRenamableSymbol(scriptFile, position); + if (target is null) { return null; } + return target switch + { + FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), + _ => new ScriptExtentAdapter(target.Extent) + }; + } + + public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) + { + + ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); + ScriptPositionAdapter position = request.Position; + + Ast tokenToRename = FindRenamableSymbol(scriptFile, position); + if (tokenToRename is null) { return null; } + + // TODO: Potentially future cross-file support + TextEdit[] changes = tokenToRename switch + { + FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), + VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), + // FIXME: Only throw if capability is not prepareprovider + _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") + }; + + return new WorkspaceEdit + { + Changes = new Dictionary> + { + [request.TextDocument.Uri] = changes + } + }; + } + + // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading + + internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams renameParams) + { + ScriptPositionAdapter position = renameParams.Position; + + string tokenName = ""; + if (token is FunctionDefinitionAst funcDef) + { + tokenName = funcDef.Name; + } + else if (token.Parent is CommandAst CommAst) + { + tokenName = CommAst.GetCommandName(); + } + IterativeFunctionRename visitor = new( + tokenName, + renameParams.NewName, + position.Line, + position.Column, + scriptAst + ); + visitor.Visit(scriptAst); + return visitor.Modifications.ToArray(); + } + + internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) + { + if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) + { + + IterativeVariableRename visitor = new( + requestParams.NewName, + symbol.Extent.StartLineNumber, + symbol.Extent.StartColumnNumber, + scriptAst, + null //FIXME: Pass through Alias config + ); + visitor.Visit(scriptAst); + return visitor.Modifications.ToArray(); + + } + return []; + } + + + /// + /// Finds a renamable symbol at a given position in a script file. + /// + /// Ast of the token or null if no renamable symbol was found + internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) + { + int line = position.Line; + int column = position.Column; + + // Cannot use generic here as our desired ASTs do not share a common parent + Ast token = scriptFile.ScriptAst.Find(ast => + { + // Skip all statements that end before our target line or start after our target line. This is a performance optimization. + if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } + + // Supported types, filters out scriptblocks and whatnot + if (ast is not ( + FunctionDefinitionAst + or VariableExpressionAst + or CommandParameterAst + or ParameterAst + or StringConstantExpressionAst + or CommandAst + )) + { + return false; + } + + // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing + // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) + if (ast is StringConstantExpressionAst stringAst) + { + if (stringAst.Parent is not CommandAst parent) { return false; } + if (parent.GetCommandName() != stringAst.Value) { return false; } + } + + ScriptExtentAdapter target = ast switch + { + FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), + _ => new ScriptExtentAdapter(ast.Extent) + }; + + return target.Contains(position); + }, true); + + return token; + } + + private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) + { + string name = ast.Name; + // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present + int funcLength = "function ".Length; + ScriptExtentAdapter funcExtent = new(ast.Extent); + + // Get a range that represents only the function name + return funcExtent with + { + Start = funcExtent.Start.Delta(0, funcLength), + End = funcExtent.Start.Delta(0, funcLength + name.Length) + }; + } +} + +public class RenameSymbolOptions +{ + public bool CreateAlias { get; set; } +} + +internal class Utilities +{ + public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) + { + Ast? result = null; + result = ScriptAst.Find(ast => + { + return ast.Extent.StartLineNumber == StartLineNumber && + ast.Extent.StartColumnNumber == StartColumnNumber && + type.Contains(ast.GetType()); + }, true); + if (result == null) + { + throw new TargetSymbolNotFoundException(); + } + return result; + } + + public static Ast? GetAstParentOfType(Ast ast, params Type[] type) + { + Ast parent = ast; + // walk backwards till we hit a parent of the specified type or return null + while (null != parent) + { + if (type.Contains(parent.GetType())) + { + return parent; + } + parent = parent.Parent; + } + return null; + + } + + public static FunctionDefinitionAst? GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) + { + // Look up the targeted object + CommandAst? TargetCommand = (CommandAst?)GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptFile + , typeof(CommandAst)); + + if (TargetCommand?.GetCommandName().ToLower() != OldName.ToLower()) + { + TargetCommand = null; + } + + string? FunctionName = TargetCommand?.GetCommandName(); + + List FunctionDefinitions = ScriptFile.FindAll(ast => + { + return ast is FunctionDefinitionAst FuncDef && + FuncDef.Name.ToLower() == OldName.ToLower() && + (FuncDef.Extent.EndLineNumber < TargetCommand?.Extent.StartLineNumber || + (FuncDef.Extent.EndColumnNumber <= TargetCommand?.Extent.StartColumnNumber && + FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Determine which function definition is the right one + FunctionDefinitionAst? CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (TargetCommand?.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + + public static bool AssertContainsDotSourced(Ast ScriptAst) + { + Ast dotsourced = ScriptAst.Find(ast => + { + return ast is CommandAst commandAst && commandAst.InvocationOperator == TokenKind.Dot; + }, true); + if (dotsourced != null) + { + return true; + } + return false; + } + + public static Ast? GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) + { + Ast? token = null; + + token = Ast.Find(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + + if (token is NamedBlockAst) + { + // NamedBlockAST starts on the same line as potentially another AST, + // its likley a user is not after the NamedBlockAst but what it contains + IEnumerable stacked_tokens = token.FindAll(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + ast.Extent.EndColumnNumber >= StartColumnNumber + && StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + + if (stacked_tokens.Count() > 1) + { + return stacked_tokens.LastOrDefault(); + } + + return token.Parent; + } + + if (null == token) + { + IEnumerable LineT = Ast.FindAll(ast => + { + return StartLineNumber == ast.Extent.StartLineNumber && + StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + return LineT.OfType()?.LastOrDefault(); + } + + IEnumerable tokens = token.FindAll(ast => + { + return ast.Extent.EndColumnNumber >= StartColumnNumber + && StartColumnNumber >= ast.Extent.StartColumnNumber; + }, true); + if (tokens.Count() > 1) + { + token = tokens.LastOrDefault(); + } + return token; + } +} + + +/// +/// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default line/column constructor is 1-based. +/// +public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable, IComparable, IComparable +{ + public int Line => position.LineNumber; + public int Column => position.ColumnNumber; + public int Character => position.ColumnNumber; + public int LineNumber => position.LineNumber; + public int ColumnNumber => position.ColumnNumber; + + public string File => position.File; + string IScriptPosition.Line => position.Line; + public int Offset => position.Offset; + + public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(null, Line, Column, null)) { } + public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } + + public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } + public static implicit operator ScriptPositionAdapter(Position position) => new(position); + public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new + ( + scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 + ); + + + public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); + public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; + + internal ScriptPositionAdapter Delta(int LineAdjust, int ColumnAdjust) => new( + position.LineNumber + LineAdjust, + position.ColumnNumber + ColumnAdjust + ); + + public int CompareTo(ScriptPositionAdapter other) + { + if (position.LineNumber == other.position.LineNumber) + { + return position.ColumnNumber.CompareTo(other.position.ColumnNumber); + } + return position.LineNumber.CompareTo(other.position.LineNumber); + } + public int CompareTo(Position other) => CompareTo((ScriptPositionAdapter)other); + public int CompareTo(ScriptPosition other) => CompareTo((ScriptPositionAdapter)other); + public string GetFullScript() => throw new NotImplementedException(); +} + +/// +/// Represents a range in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default ScriptExtent constructor is 1-based +/// +/// +internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent +{ + public ScriptPositionAdapter Start = new(extent.StartScriptPosition); + public ScriptPositionAdapter End = new(extent.EndScriptPosition); + + public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); + + public static implicit operator ScriptExtentAdapter(Range range) => new(new ScriptExtent( + // Will get shifted to 1-based + new ScriptPositionAdapter(range.Start), + new ScriptPositionAdapter(range.End) + )); + public static implicit operator Range(ScriptExtentAdapter adapter) => new() + { + // Will get shifted to 0-based + Start = adapter.Start, + End = adapter.End + }; + + public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; + + public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter); + + public IScriptPosition StartScriptPosition => Start; + public IScriptPosition EndScriptPosition => End; + public int EndColumnNumber => End.ColumnNumber; + public int EndLineNumber => End.LineNumber; + public int StartOffset => extent.EndOffset; + public int EndOffset => extent.EndOffset; + public string File => extent.File; + public int StartColumnNumber => extent.StartColumnNumber; + public int StartLineNumber => extent.StartLineNumber; + public string Text => extent.Text; + + public bool Contains(Position position) => ContainsPosition(this, position); + public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); +} From 209f2ef7388320e866f09018f1da3c3dc4bb323b Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 17 Sep 2024 14:09:10 -0700 Subject: [PATCH 157/203] Introduce service to the Extension --- .../Server/PsesServiceCollectionExtensions.cs | 3 +- .../TextDocument/Handlers/RenameHandler.cs | 9 ++--- .../TextDocument/Services/RenameService.cs | 36 +++++++++---------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 82081c341..5a75ce448 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -50,7 +50,8 @@ public static IServiceCollection AddPsesLanguageServices( extensionService.InitializeAsync(); return extensionService; }) - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } public static IServiceCollection AddPsesDebugServices( diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs index 061eb334e..77ad58d7b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/RenameHandler.cs @@ -11,16 +11,14 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; - namespace Microsoft.PowerShell.EditorServices.Handlers; /// /// A handler for textDocument/prepareRename -/// LSP Ref: /// internal class PrepareRenameHandler ( - IRenameService renameService + RenameService renameService ) : IPrepareRenameHandler { public RenameRegistrationOptions GetRegistrationOptions(RenameCapability capability, ClientCapabilities clientCapabilities) => capability.PrepareSupport ? new() { PrepareProvider = true } : new(); @@ -30,11 +28,10 @@ IRenameService renameService } /// -/// A handler for textDocument/prepareRename -/// LSP Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename +/// A handler for textDocument/rename /// internal class RenameHandler( - IRenameService renameService + RenameService renameService ) : IRenameHandler { // RenameOptions may only be specified if the client states that it supports prepareSupport in its initial initialize request. diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs index 0c7dc3bb8..c1b649df5 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -42,24 +42,24 @@ ILanguageServerConfiguration config public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied - config.ToString(); - ShowMessageRequestParams reqParams = new() - { - Type = MessageType.Warning, - Message = "Test Send", - Actions = new MessageActionItem[] { - new MessageActionItem() { Title = "I Accept" }, - new MessageActionItem() { Title = "I Accept [Workspace]" }, - new MessageActionItem() { Title = "Decline" } - } - }; - - MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); - if (result.Title == "Test Action") - { - // FIXME: Need to accept - Console.WriteLine("yay"); - } + // config.ToString(); + // ShowMessageRequestParams reqParams = new() + // { + // Type = MessageType.Warning, + // Message = "Test Send", + // Actions = new MessageActionItem[] { + // new MessageActionItem() { Title = "I Accept" }, + // new MessageActionItem() { Title = "I Accept [Workspace]" }, + // new MessageActionItem() { Title = "Decline" } + // } + // }; + + // MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); + // if (result.Title == "Test Action") + // { + // // FIXME: Need to accept + // Console.WriteLine("yay"); + // } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); From e6d39337a10dd4979069847666febe552074b5ad Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 17 Sep 2024 16:57:25 -0700 Subject: [PATCH 158/203] Redo tests to be data-driven --- .../Functions/RefactorFunctionTestCases.cs | 25 +++ .../Functions/RefactorsFunctionData.cs | 109 ---------- .../Refactoring/RenameTestTarget.cs | 18 ++ .../Utilities/RefactorUtilitiesData.cs | 32 ++- .../Variables/RefactorVariableTestCases.cs | 34 +++ .../Variables/RefactorVariablesData.cs | 195 ------------------ .../Refactoring/PrepareRenameHandlerTests.cs | 73 +++++-- .../Refactoring/RefactorUtilities.cs | 67 +++--- .../Refactoring/RefactorUtilitiesTests.cs | 160 +++++++------- .../Refactoring/RenameHandlerFunctionTests.cs | 89 -------- .../Refactoring/RenameHandlerTests.cs | 100 +++++++++ .../Refactoring/RenameHandlerVariableTests.cs | 87 -------- 12 files changed, 355 insertions(+), 634 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs delete mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs delete mode 100644 test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs create mode 100644 test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs delete mode 100644 test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs new file mode 100644 index 000000000..3362f5477 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PowerShellEditorServices.Test.Shared.Refactoring; + +public class RefactorFunctionTestCases +{ + public static RenameTestTarget[] TestCases = + [ + new("FunctionsSingle.ps1", Line: 1, Column: 5 ), + new("FunctionMultipleOccurrences.ps1", Line: 1, Column: 5 ), + new("FunctionInnerIsNested.ps1", Line: 5, Column: 5, "bar"), + new("FunctionOuterHasNestedFunction.ps1", Line: 10, Column: 1 ), + new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5, "RenamedInnerFunction"), + new("FunctionWithInternalCalls.ps1", Line: 1, Column: 5 ), + new("FunctionCmdlet.ps1", Line: 10, Column: 1 ), + new("FunctionSameName.ps1", Line: 14, Column: 3, "RenamedSameNameFunction"), + new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), + new("FunctionLoop.ps1", Line: 5, Column: 5 ), + new("FunctionForeach.ps1", Line: 5, Column: 11 ), + new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), + new("FunctionCallWIthinStringExpression.ps1", Line: 10, Column: 1 ), + new("FunctionNestedRedefinition.ps1", Line: 15, Column: 13 ) + ]; +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs deleted file mode 100644 index 218257602..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorsFunctionData.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Handlers; - -namespace PowerShellEditorServices.Test.Shared.Refactoring.Functions -{ - internal class RefactorsFunctionData - { - - public static readonly RenameSymbolParams FunctionsSingle = new() - { - FileName = "FunctionsSingle.ps1", - Column = 1, - Line = 5, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionMultipleOccurrences = new() - { - FileName = "FunctionMultipleOccurrences.ps1", - Column = 1, - Line = 5, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionInnerIsNested = new() - { - FileName = "FunctionInnerIsNested.ps1", - Column = 5, - Line = 5, - RenameTo = "bar" - }; - public static readonly RenameSymbolParams FunctionOuterHasNestedFunction = new() - { - FileName = "FunctionOuterHasNestedFunction.ps1", - Column = 10, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionWithInnerFunction = new() - { - FileName = "FunctionWithInnerFunction.ps1", - Column = 5, - Line = 5, - RenameTo = "RenamedInnerFunction" - }; - public static readonly RenameSymbolParams FunctionWithInternalCalls = new() - { - FileName = "FunctionWithInternalCalls.ps1", - Column = 1, - Line = 5, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionCmdlet = new() - { - FileName = "FunctionCmdlet.ps1", - Column = 10, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionSameName = new() - { - FileName = "FunctionSameName.ps1", - Column = 14, - Line = 3, - RenameTo = "RenamedSameNameFunction" - }; - public static readonly RenameSymbolParams FunctionScriptblock = new() - { - FileName = "FunctionScriptblock.ps1", - Column = 5, - Line = 5, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionLoop = new() - { - FileName = "FunctionLoop.ps1", - Column = 5, - Line = 5, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionForeach = new() - { - FileName = "FunctionForeach.ps1", - Column = 5, - Line = 11, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionForeachObject = new() - { - FileName = "FunctionForeachObject.ps1", - Column = 5, - Line = 11, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionCallWIthinStringExpression = new() - { - FileName = "FunctionCallWIthinStringExpression.ps1", - Column = 10, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams FunctionNestedRedefinition = new() - { - FileName = "FunctionNestedRedefinition.ps1", - Column = 15, - Line = 13, - RenameTo = "Renamed" - }; - } -} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs new file mode 100644 index 000000000..8943cfbd0 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +namespace PowerShellEditorServices.Test.Shared.Refactoring; + +/// +/// Describes a test case for renaming a file +/// +/// The test case file name e.g. testScript.ps1 +/// The line where the cursor should be positioned for the rename +/// The column/character indent where ther cursor should be positioned for the rename +/// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified +public record RenameTestTarget(string FileName = "UNKNOWN", int Line = -1, int Column = -1, string NewName = "Renamed") +{ + public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)}"; +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs index 5cc1ea89d..a2452620e 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Utilities/RefactorUtilitiesData.cs @@ -1,59 +1,57 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Handlers; -namespace PowerShellEditorServices.Test.Shared.Refactoring.Utilities +namespace PowerShellEditorServices.Test.Shared.Refactoring { internal static class RenameUtilitiesData { - - public static readonly RenameSymbolParams GetVariableExpressionAst = new() + public static readonly RenameTestTarget GetVariableExpressionAst = new() { Column = 11, Line = 15, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetVariableExpressionStartAst = new() + public static readonly RenameTestTarget GetVariableExpressionStartAst = new() { Column = 1, Line = 15, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetVariableWithinParameterAst = new() + public static readonly RenameTestTarget GetVariableWithinParameterAst = new() { Column = 21, Line = 3, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetHashTableKey = new() + public static readonly RenameTestTarget GetHashTableKey = new() { Column = 9, Line = 16, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetVariableWithinCommandAst = new() + public static readonly RenameTestTarget GetVariableWithinCommandAst = new() { Column = 29, Line = 6, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetCommandParameterAst = new() + public static readonly RenameTestTarget GetCommandParameterAst = new() { Column = 12, Line = 21, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; - public static readonly RenameSymbolParams GetFunctionDefinitionAst = new() + public static readonly RenameTestTarget GetFunctionDefinitionAst = new() { Column = 12, Line = 1, - RenameTo = "Renamed", + NewName = "Renamed", FileName = "TestDetection.ps1" }; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs new file mode 100644 index 000000000..4c5c1f9c4 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +namespace PowerShellEditorServices.Test.Shared.Refactoring; +public class RefactorVariableTestCases +{ + public static RenameTestTarget[] TestCases = + [ + new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1 ), + new ("VariableRedefinition.ps1", Line: 1, Column: 1 ), + new ("VariableNestedScopeFunction.ps1", Line: 1, Column: 1 ), + new ("VariableInLoop.ps1", Line: 1, Column: 1 ), + new ("VariableInPipeline.ps1", Line: 23, Column: 2 ), + new ("VariableInScriptblockScoped.ps1", Line: 36, Column: 3 ), + new ("VariablewWithinHastableExpression.ps1", Line: 46, Column: 3 ), + new ("VariableNestedFunctionScriptblock.ps1", Line: 20, Column: 4 ), + new ("VariableWithinCommandAstScriptBlock.ps1", Line: 75, Column: 3 ), + new ("VariableWithinForeachObject.ps1", Line: 1, Column: 2 ), + new ("VariableusedInWhileLoop.ps1", Line: 5, Column: 2 ), + new ("VariableInParam.ps1", Line: 16, Column: 24 ), + new ("VariableCommandParameter.ps1", Line: 9, Column: 10 ), + new ("VariableCommandParameter.ps1", Line: 17, Column: 3 ), + new ("VariableScriptWithParamBlock.ps1", Line: 30, Column: 1 ), + new ("VariableNonParam.ps1", Line: 1, Column: 7 ), + new ("VariableParameterCommandWithSameName.ps1", Line: 13, Column: 9 ), + new ("VariableCommandParameterSplatted.ps1", Line: 10, Column: 21 ), + new ("VariableCommandParameterSplatted.ps1", Line: 5, Column: 16 ), + new ("VariableInForeachDuplicateAssignment.ps1", Line: 18, Column: 6 ), + new ("VariableInForloopDuplicateAssignment.ps1", Line: 14, Column: 9 ), + new ("VariableNestedScopeFunctionRefactorInner.ps1", Line: 5, Column: 3 ), + new ("VariableSimpleFunctionParameter.ps1", Line: 9, Column: 6 ), + new ("VariableDotNotationFromInnerFunction.ps1", Line: 26, Column: 11 ), + new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1 ) + ]; +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs deleted file mode 100644 index 9d9e63fdf..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariablesData.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -using Microsoft.PowerShell.EditorServices.Handlers; - -namespace PowerShellEditorServices.Test.Shared.Refactoring.Variables -{ - internal static class RenameVariableData - { - - public static readonly RenameSymbolParams SimpleVariableAssignment = new() - { - FileName = "SimpleVariableAssignment.ps1", - Column = 1, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableRedefinition = new() - { - FileName = "VariableRedefinition.ps1", - Column = 1, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableNestedScopeFunction = new() - { - FileName = "VariableNestedScopeFunction.ps1", - Column = 1, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInLoop = new() - { - FileName = "VariableInLoop.ps1", - Column = 1, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInPipeline = new() - { - FileName = "VariableInPipeline.ps1", - Column = 23, - Line = 2, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInScriptblock = new() - { - FileName = "VariableInScriptblock.ps1", - Column = 26, - Line = 2, - RenameTo = "Renamed" - }; - - public static readonly RenameSymbolParams VariableInScriptblockScoped = new() - { - FileName = "VariableInScriptblockScoped.ps1", - Column = 36, - Line = 2, - RenameTo = "Renamed" - }; - - public static readonly RenameSymbolParams VariablewWithinHastableExpression = new() - { - FileName = "VariablewWithinHastableExpression.ps1", - Column = 46, - Line = 3, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableNestedFunctionScriptblock = new() - { - FileName = "VariableNestedFunctionScriptblock.ps1", - Column = 20, - Line = 4, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableWithinCommandAstScriptBlock = new() - { - FileName = "VariableWithinCommandAstScriptBlock.ps1", - Column = 75, - Line = 3, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableWithinForeachObject = new() - { - FileName = "VariableWithinForeachObject.ps1", - Column = 1, - Line = 2, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableusedInWhileLoop = new() - { - FileName = "VariableusedInWhileLoop.ps1", - Column = 5, - Line = 2, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInParam = new() - { - FileName = "VariableInParam.ps1", - Column = 16, - Line = 24, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableCommandParameter = new() - { - FileName = "VariableCommandParameter.ps1", - Column = 9, - Line = 10, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableCommandParameterReverse = new() - { - FileName = "VariableCommandParameter.ps1", - Column = 17, - Line = 3, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableScriptWithParamBlock = new() - { - FileName = "VariableScriptWithParamBlock.ps1", - Column = 30, - Line = 1, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableNonParam = new() - { - FileName = "VariableNonParam.ps1", - Column = 1, - Line = 7, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableParameterCommandWithSameName = new() - { - FileName = "VariableParameterCommandWithSameName.ps1", - Column = 13, - Line = 9, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableCommandParameterSplattedFromCommandAst = new() - { - FileName = "VariableCommandParameterSplatted.ps1", - Column = 10, - Line = 21, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableCommandParameterSplattedFromSplat = new() - { - FileName = "VariableCommandParameterSplatted.ps1", - Column = 5, - Line = 16, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInForeachDuplicateAssignment = new() - { - FileName = "VariableInForeachDuplicateAssignment.ps1", - Column = 18, - Line = 6, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableInForloopDuplicateAssignment = new() - { - FileName = "VariableInForloopDuplicateAssignment.ps1", - Column = 14, - Line = 9, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableNestedScopeFunctionRefactorInner = new() - { - FileName = "VariableNestedScopeFunctionRefactorInner.ps1", - Column = 5, - Line = 3, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableSimpleFunctionParameter = new() - { - FileName = "VariableSimpleFunctionParameter.ps1", - Column = 9, - Line = 6, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableDotNotationFromInnerFunction = new() - { - FileName = "VariableDotNotationFromInnerFunction.ps1", - Column = 26, - Line = 11, - RenameTo = "Renamed" - }; - public static readonly RenameSymbolParams VariableDotNotationFromOuterVar = new() - { - FileName = "VariableDotNotationFromInnerFunction.ps1", - Column = 1, - Line = 1, - RenameTo = "Renamed" - }; - } -} diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index b662c67e2..0e3bf183d 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -18,52 +18,83 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Progress; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Test.Shared.Refactoring; using Xunit; -using static PowerShellEditorServices.Test.Handlers.RefactorFunctionTests; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; namespace PowerShellEditorServices.Test.Handlers; [Trait("Category", "PrepareRename")] -public class PrepareRenameHandlerTests : TheoryData +public class PrepareRenameHandlerTests { - private readonly WorkspaceService workspace = new(NullLoggerFactory.Instance); - private readonly PrepareRenameHandler handler; + private readonly PrepareRenameHandler testHandler; + public PrepareRenameHandlerTests() { + WorkspaceService workspace = new(NullLoggerFactory.Instance); workspace.WorkspaceFolders.Add(new WorkspaceFolder { Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring")) }); - // FIXME: Need to make a Mock to pass to the ExtensionService constructor - handler = new(workspace, new fakeLspSendMessageRequestFacade("I Accept"), new fakeConfigurationService()); + testHandler = new + ( + new RenameService + ( + workspace, + new fakeLspSendMessageRequestFacade("I Accept"), + new fakeConfigurationService() + ) + ); } - // TODO: Test an untitled document (maybe that belongs in E2E tests) + /// + /// Convert test cases into theory data. This keeps us from needing xunit in the test data project + /// This type has a special ToString to add a data-driven test name which is why we dont convert directly to the param type first + /// + public static TheoryData FunctionTestCases() + => new(RefactorFunctionTestCases.TestCases); + + public static TheoryData VariableTestCases() + => new(RefactorVariableTestCases.TestCases); + + [Theory] + [MemberData(nameof(FunctionTestCases))] + public async Task FindsFunction(RenameTestTarget testTarget) + { + PrepareRenameParams testParams = testTarget.ToPrepareRenameParams("Functions"); + + RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); + + Assert.NotNull(result?.Range); + Assert.True(result.Range.Contains(testParams.Position)); + } [Theory] - [ClassData(typeof(FunctionRenameTestData))] - public async Task FindsSymbol(RenameSymbolParamsSerialized param) + [MemberData(nameof(VariableTestCases))] + public async Task FindsVariable(RenameTestTarget testTarget) { - // The test data is the PS script location. The handler expects 0-based line and column numbers. - Position position = new(param.Line - 1, param.Column - 1); - PrepareRenameParams testParams = new() + PrepareRenameParams testParams = testTarget.ToPrepareRenameParams("Variables"); + + RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); + + Assert.NotNull(result?.Range); + Assert.True(result.Range.Contains(testParams.Position)); + } +} + +public static partial class RenameTestTargetExtensions +{ + public static PrepareRenameParams ToPrepareRenameParams(this RenameTestTarget testCase, string baseFolder) + => new() { - Position = position, + Position = new ScriptPositionAdapter(Line: testCase.Line, Column: testCase.Column), TextDocument = new TextDocumentIdentifier { Uri = DocumentUri.FromFileSystemPath( - TestUtilities.GetSharedPath($"Refactoring/Functions/{param.FileName}") + TestUtilities.GetSharedPath($"Refactoring/{baseFolder}/{testCase.FileName}") ) } }; - - RangeOrPlaceholderRange? result = await handler.Handle(testParams, CancellationToken.None); - Assert.NotNull(result); - Assert.NotNull(result.Range); - Assert.True(result.Range.Contains(position)); - } } public class fakeLspSendMessageRequestFacade(string title) : ILanguageServerFacade diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs index 4d68f3c3e..6338d5fcf 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -2,9 +2,6 @@ // Licensed under the MIT License. using System; -using Microsoft.PowerShell.EditorServices.Handlers; -using Xunit.Abstractions; -using MediatR; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using System.Linq; using System.Collections.Generic; @@ -54,43 +51,43 @@ internal static string GetModifiedScript(string OriginalScript, TextEdit[] Modif return string.Join(Environment.NewLine, Lines); } - public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable - { - public string FileName { get; set; } - public int Line { get; set; } - public int Column { get; set; } - public string RenameTo { get; set; } + // public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable + // { + // public string FileName { get; set; } + // public int Line { get; set; } + // public int Column { get; set; } + // public string RenameTo { get; set; } - // Default constructor needed for deserialization - public RenameSymbolParamsSerialized() { } + // // Default constructor needed for deserialization + // public RenameSymbolParamsSerialized() { } - // Parameterized constructor for convenience - public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) - { - FileName = RenameSymbolParams.FileName; - Line = RenameSymbolParams.Line; - Column = RenameSymbolParams.Column; - RenameTo = RenameSymbolParams.RenameTo; - } + // // Parameterized constructor for convenience + // public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) + // { + // FileName = RenameSymbolParams.FileName; + // Line = RenameSymbolParams.Line; + // Column = RenameSymbolParams.Column; + // RenameTo = RenameSymbolParams.RenameTo; + // } - public void Deserialize(IXunitSerializationInfo info) - { - FileName = info.GetValue("FileName"); - Line = info.GetValue("Line"); - Column = info.GetValue("Column"); - RenameTo = info.GetValue("RenameTo"); - } + // public void Deserialize(IXunitSerializationInfo info) + // { + // FileName = info.GetValue("FileName"); + // Line = info.GetValue("Line"); + // Column = info.GetValue("Column"); + // RenameTo = info.GetValue("RenameTo"); + // } - public void Serialize(IXunitSerializationInfo info) - { - info.AddValue("FileName", FileName); - info.AddValue("Line", Line); - info.AddValue("Column", Column); - info.AddValue("RenameTo", RenameTo); - } + // public void Serialize(IXunitSerializationInfo info) + // { + // info.AddValue("FileName", FileName); + // info.AddValue("Line", Line); + // info.AddValue("Column", Column); + // info.AddValue("RenameTo", RenameTo); + // } - public override string ToString() => $"{FileName}"; - } + // public override string ToString() => $"{FileName}"; + // } } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs index 70f91fd03..fea824839 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs @@ -1,93 +1,91 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Microsoft.PowerShell.EditorServices.Handlers; -using Xunit; -using System.Management.Automation.Language; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -using Microsoft.PowerShell.EditorServices.Refactoring; -using PowerShellEditorServices.Test.Shared.Refactoring.Utilities; +// FIXME: Fix these tests (if it is even worth doing so) +// using System.IO; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging.Abstractions; +// using Microsoft.PowerShell.EditorServices.Services; +// using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; +// using Microsoft.PowerShell.EditorServices.Services.TextDocument; +// using Microsoft.PowerShell.EditorServices.Test; +// using Microsoft.PowerShell.EditorServices.Test.Shared; +// using Xunit; +// using System.Management.Automation.Language; +// using Microsoft.PowerShell.EditorServices.Refactoring; -namespace PowerShellEditorServices.Test.Refactoring -{ - [Trait("Category", "RefactorUtilities")] - public class RefactorUtilitiesTests : IAsyncLifetime - { - private PsesInternalHost psesHost; - private WorkspaceService workspace; +// namespace PowerShellEditorServices.Test.Refactoring +// { +// [Trait("Category", "RefactorUtilities")] +// public class RefactorUtilitiesTests : IAsyncLifetime +// { +// private PsesInternalHost psesHost; +// private WorkspaceService workspace; - public async Task InitializeAsync() - { - psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } +// public async Task InitializeAsync() +// { +// psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); +// workspace = new WorkspaceService(NullLoggerFactory.Instance); +// } - public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Utilities", fileName))); +// public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); +// private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Utilities", fileName))); - public class GetAstShouldDetectTestData : TheoryData - { - public GetAstShouldDetectTestData() - { - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionAst), 15, 1); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionStartAst), 15, 1); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinParameterAst), 3, 17); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetHashTableKey), 16, 5); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinCommandAst), 6, 28); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetCommandParameterAst), 21, 10); - Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetFunctionDefinitionAst), 1, 1); - } - } +// public class GetAstShouldDetectTestData : TheoryData +// { +// public GetAstShouldDetectTestData() +// { +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionAst), 15, 1); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionStartAst), 15, 1); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinParameterAst), 3, 17); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetHashTableKey), 16, 5); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinCommandAst), 6, 28); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetCommandParameterAst), 21, 10); +// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetFunctionDefinitionAst), 1, 1); +// } +// } - [Theory] - [ClassData(typeof(GetAstShouldDetectTestData))] - public void GetAstShouldDetect(RenameSymbolParamsSerialized s, int l, int c) - { - ScriptFile scriptFile = GetTestScript(s.FileName); - Ast symbol = Utilities.GetAst(s.Line, s.Column, scriptFile.ScriptAst); - // Assert the Line and Column is what is expected - Assert.Equal(l, symbol.Extent.StartLineNumber); - Assert.Equal(c, symbol.Extent.StartColumnNumber); - } +// [Theory] +// [ClassData(typeof(GetAstShouldDetectTestData))] +// public void GetAstShouldDetect(RenameSymbolParamsSerialized s, int l, int c) +// { +// ScriptFile scriptFile = GetTestScript(s.FileName); +// Ast symbol = Utilities.GetAst(s.Line, s.Column, scriptFile.ScriptAst); +// // Assert the Line and Column is what is expected +// Assert.Equal(l, symbol.Extent.StartLineNumber); +// Assert.Equal(c, symbol.Extent.StartColumnNumber); +// } - [Fact] - public void GetVariableUnderFunctionDef() - { - RenameSymbolParams request = new() - { - Column = 5, - Line = 2, - RenameTo = "Renamed", - FileName = "TestDetectionUnderFunctionDef.ps1" - }; - ScriptFile scriptFile = GetTestScript(request.FileName); +// [Fact] +// public void GetVariableUnderFunctionDef() +// { +// RenameSymbolParams request = new() +// { +// Column = 5, +// Line = 2, +// RenameTo = "Renamed", +// FileName = "TestDetectionUnderFunctionDef.ps1" +// }; +// ScriptFile scriptFile = GetTestScript(request.FileName); - Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); - Assert.IsType(symbol); - Assert.Equal(2, symbol.Extent.StartLineNumber); - Assert.Equal(5, symbol.Extent.StartColumnNumber); +// Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); +// Assert.IsType(symbol); +// Assert.Equal(2, symbol.Extent.StartLineNumber); +// Assert.Equal(5, symbol.Extent.StartColumnNumber); - } - [Fact] - public void AssertContainsDotSourcingTrue() - { - ScriptFile scriptFile = GetTestScript("TestDotSourcingTrue.ps1"); - Assert.True(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); - } - [Fact] - public void AssertContainsDotSourcingFalse() - { - ScriptFile scriptFile = GetTestScript("TestDotSourcingFalse.ps1"); - Assert.False(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); - } - } -} +// } +// [Fact] +// public void AssertContainsDotSourcingTrue() +// { +// ScriptFile scriptFile = GetTestScript("TestDotSourcingTrue.ps1"); +// Assert.True(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); +// } +// [Fact] +// public void AssertContainsDotSourcingFalse() +// { +// ScriptFile scriptFile = GetTestScript("TestDotSourcingFalse.ps1"); +// Assert.False(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); +// } +// } +// } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs deleted file mode 100644 index ede640c46..000000000 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerFunctionTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Xunit; -using Microsoft.PowerShell.EditorServices.Services.Symbols; -using Microsoft.PowerShell.EditorServices.Refactoring; -using PowerShellEditorServices.Test.Shared.Refactoring.Functions; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; - -namespace PowerShellEditorServices.Test.Handlers; - -[Trait("Category", "RenameHandlerFunction")] -public class RefactorFunctionTests : IAsyncLifetime -{ - private PsesInternalHost psesHost; - private WorkspaceService workspace; - public async Task InitializeAsync() - { - psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } - - public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); - - internal static string GetRenamedFunctionScriptContent(ScriptFile scriptFile, RenameSymbolParamsSerialized request, SymbolReference symbol) - { - IterativeFunctionRename visitor = new(symbol.NameRegion.Text, - request.RenameTo, - symbol.ScriptRegion.StartLineNumber, - symbol.ScriptRegion.StartColumnNumber, - scriptFile.ScriptAst); - visitor.Visit(scriptFile.ScriptAst); - TextEdit[] changes = visitor.Modifications.ToArray(); - return GetModifiedScript(scriptFile.Contents, changes); - } - - public class FunctionRenameTestData : TheoryData - { - public FunctionRenameTestData() - { - - // Simple - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionsSingle)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionWithInternalCalls)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCmdlet)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionScriptblock)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionCallWIthinStringExpression)); - // Loops - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionLoop)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeach)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionForeachObject)); - // Nested - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionOuterHasNestedFunction)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionInnerIsNested)); - // Multi Occurance - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionMultipleOccurrences)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionSameName)); - Add(new RenameSymbolParamsSerialized(RefactorsFunctionData.FunctionNestedRedefinition)); - } - } - - [Theory] - [ClassData(typeof(FunctionRenameTestData))] - public void Rename(RenameSymbolParamsSerialized s) - { - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition( - request.Line, - request.Column - ); - - string modifiedcontent = GetRenamedFunctionScriptContent(scriptFile, request, symbol); - Assert.Equal(expectedContent.Contents, modifiedcontent); - } -} - diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs new file mode 100644 index 000000000..e00b1d6b4 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test.Shared; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; +using System.IO; +using System.Linq; +using System.Threading; +using Xunit; +using PowerShellEditorServices.Test.Shared.Refactoring; + +namespace PowerShellEditorServices.Test.Handlers; +#pragma warning disable VSTHRD100 // XUnit handles async void with a custom SyncContext + +[Trait("Category", "RenameHandlerFunction")] +public class RenameHandlerTests +{ + internal WorkspaceService workspace = new(NullLoggerFactory.Instance); + + private readonly RenameHandler testHandler; + public RenameHandlerTests() + { + workspace.WorkspaceFolders.Add(new WorkspaceFolder + { + Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("Refactoring")) + }); + + testHandler = new + ( + new RenameService + ( + workspace, + new fakeLspSendMessageRequestFacade("I Accept"), + new fakeConfigurationService() + ) + ); + } + + // Decided to keep this DAMP instead of DRY due to memberdata boundaries, duplicates with PrepareRenameHandler + public static TheoryData VariableTestCases() + => new(RefactorVariableTestCases.TestCases); + + public static TheoryData FunctionTestCases() + => new(RefactorFunctionTestCases.TestCases); + + [Theory] + [MemberData(nameof(VariableTestCases))] + public async void RenamedSymbol(RenameTestTarget request) + { + string fileName = request.FileName; + ScriptFile scriptFile = GetTestScript(fileName); + + WorkspaceEdit response = await testHandler.Handle(request.ToRenameParams(), CancellationToken.None); + + string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[request.ToRenameParams().TextDocument.Uri].ToArray()); + + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(FunctionTestCases))] + public async void RenamedFunction(RenameTestTarget request) + { + string fileName = request.FileName; + ScriptFile scriptFile = GetTestScript(fileName); + + WorkspaceEdit response = await testHandler.Handle(request.ToRenameParams(), CancellationToken.None); + + string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[request.ToRenameParams().TextDocument.Uri].ToArray()); + + Assert.Equal(expected, actual); + } + + private ScriptFile GetTestScript(string fileName) => + workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); +} + +public static partial class RenameTestTargetExtensions +{ + public static RenameParams ToRenameParams(this RenameTestTarget testCase) + => new() + { + Position = new ScriptPositionAdapter(Line: testCase.Line, Column: testCase.Column), + TextDocument = new TextDocumentIdentifier + { + Uri = DocumentUri.FromFileSystemPath( + TestUtilities.GetSharedPath($"Refactoring/Functions/{testCase.FileName}") + ) + }, + NewName = testCase.NewName + }; +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs deleted file mode 100644 index 43944fc72..000000000 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerVariableTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Test; -using Microsoft.PowerShell.EditorServices.Test.Shared; -using Xunit; -using PowerShellEditorServices.Test.Shared.Refactoring.Variables; -using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -using Microsoft.PowerShell.EditorServices.Refactoring; - -namespace PowerShellEditorServices.Test.Handlers; - -[Trait("Category", "RenameHandlerVariable")] -public class RefactorVariableTests : IAsyncLifetime - -{ - private PsesInternalHost psesHost; - private WorkspaceService workspace; - public async Task InitializeAsync() - { - psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); - } - public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); - private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Variables", fileName))); - - internal static string TestRenaming(ScriptFile scriptFile, RenameSymbolParamsSerialized request) - { - - IterativeVariableRename iterative = new(request.RenameTo, - request.Line, - request.Column, - scriptFile.ScriptAst); - iterative.Visit(scriptFile.ScriptAst); - return GetModifiedScript(scriptFile.Contents, iterative.Modifications.ToArray()); - } - public class VariableRenameTestData : TheoryData - { - public VariableRenameTestData() - { - Add(new RenameSymbolParamsSerialized(RenameVariableData.SimpleVariableAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableRedefinition)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunction)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInLoop)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInPipeline)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInScriptblockScoped)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariablewWithinHastableExpression)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedFunctionScriptblock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinCommandAstScriptBlock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableWithinForeachObject)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableusedInWhileLoop)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInParam)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameter)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterReverse)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableScriptWithParamBlock)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNonParam)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableParameterCommandWithSameName)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromCommandAst)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableCommandParameterSplattedFromSplat)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForeachDuplicateAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableInForloopDuplicateAssignment)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableNestedScopeFunctionRefactorInner)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromOuterVar)); - Add(new RenameSymbolParamsSerialized(RenameVariableData.VariableDotNotationFromInnerFunction)); - } - } - - [Theory] - [ClassData(typeof(VariableRenameTestData))] - public void Rename(RenameSymbolParamsSerialized s) - { - RenameSymbolParamsSerialized request = s; - ScriptFile scriptFile = GetTestScript(request.FileName); - ScriptFile expectedContent = GetTestScript(request.FileName.Substring(0, request.FileName.Length - 4) + "Renamed.ps1"); - - string modifiedcontent = TestRenaming(scriptFile, request); - - Assert.Equal(expectedContent.Contents, modifiedcontent); - } -} From 2bcabba67259df723440110f3d47ceffa422a5c5 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 17 Sep 2024 22:06:30 -0700 Subject: [PATCH 159/203] Fixup Tests, still a bug in rename logic with functions --- .../Utility/IScriptExtentExtensions.cs | 19 ++- .../TextDocument/Services/RenameService.cs | 154 ++++++++++++------ .../Functions/RefactorFunctionTestCases.cs | 6 +- .../Refactoring/RenameTestTarget.cs | 36 +++- .../Refactoring/PrepareRenameHandlerTests.cs | 71 +++++++- .../Refactoring/RenameHandlerTests.cs | 40 ++--- 6 files changed, 225 insertions(+), 101 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs index 25fd74349..5235ea3e5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/IScriptExtentExtensions.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Services; +// using System.Management.Automation.Language; +// using Microsoft.PowerShell.EditorServices.Services; -namespace PowerShellEditorServices.Services.PowerShell.Utility -{ - public static class IScriptExtentExtensions - { - public static bool Contains(this IScriptExtent extent, ScriptPositionAdapter position) => ScriptExtentAdapter.ContainsPosition(new(extent), position); - } -} +// namespace PowerShellEditorServices.Services.PowerShell.Utility +// { +// public static class IScriptExtentExtensions +// { +// public static bool Contains(this IScriptExtent extent, IScriptExtent position) +// => ScriptExtentAdapter.ContainsPosition(extent, position); +// } +// } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs index c1b649df5..b381c34d0 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -70,8 +70,10 @@ ILanguageServerConfiguration config } ScriptPositionAdapter position = request.Position; - Ast target = FindRenamableSymbol(scriptFile, position); + Ast? target = FindRenamableSymbol(scriptFile, position); if (target is null) { return null; } + + // Will implicitly convert to RangeOrPlaceholder and adjust to 0-based return target switch { FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), @@ -85,7 +87,7 @@ ILanguageServerConfiguration config ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); ScriptPositionAdapter position = request.Position; - Ast tokenToRename = FindRenamableSymbol(scriptFile, position); + Ast? tokenToRename = FindRenamableSymbol(scriptFile, position); if (tokenToRename is null) { return null; } // TODO: Potentially future cross-file support @@ -151,55 +153,35 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam return []; } - /// - /// Finds a renamable symbol at a given position in a script file. + /// Finds the most specific renamable symbol at the given position /// /// Ast of the token or null if no renamable symbol was found - internal static Ast FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) + internal static Ast? FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) { - int line = position.Line; - int column = position.Column; - - // Cannot use generic here as our desired ASTs do not share a common parent - Ast token = scriptFile.ScriptAst.Find(ast => + Ast? ast = scriptFile.ScriptAst.FindAtPosition(position, + [ + // Filters just the ASTs that are candidates for rename + typeof(FunctionDefinitionAst), + typeof(VariableExpressionAst), + typeof(CommandParameterAst), + typeof(ParameterAst), + typeof(StringConstantExpressionAst), + typeof(CommandAst) + ]); + + // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing + // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) + if (ast is StringConstantExpressionAst stringAst) { - // Skip all statements that end before our target line or start after our target line. This is a performance optimization. - if (ast.Extent.EndLineNumber < line || ast.Extent.StartLineNumber > line) { return false; } - - // Supported types, filters out scriptblocks and whatnot - if (ast is not ( - FunctionDefinitionAst - or VariableExpressionAst - or CommandParameterAst - or ParameterAst - or StringConstantExpressionAst - or CommandAst - )) - { - return false; - } - - // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing - // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) - if (ast is StringConstantExpressionAst stringAst) - { - if (stringAst.Parent is not CommandAst parent) { return false; } - if (parent.GetCommandName() != stringAst.Value) { return false; } - } - - ScriptExtentAdapter target = ast switch - { - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), - _ => new ScriptExtentAdapter(ast.Extent) - }; - - return target.Contains(position); - }, true); + if (stringAst.Parent is not CommandAst parent) { return null; } + if (parent.GetCommandName() != stringAst.Value) { return null; } + } - return token; + return ast; } + private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) { string name = ast.Name; @@ -221,6 +203,63 @@ public class RenameSymbolOptions public bool CreateAlias { get; set; } } + +public static class AstExtensions +{ + /// + /// Finds the most specific Ast at the given script position, or returns null if none found.
+ /// For example, if the position is on a variable expression within a function definition, + /// the variable will be returned even if the function definition is found first. + ///
+ internal static Ast? FindAtPosition(this Ast ast, IScriptPosition position, Type[]? allowedTypes) + { + // Short circuit quickly if the position is not in the provided range, no need to traverse if not + // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened. + if (!new ScriptExtentAdapter(ast.Extent).Contains(position)) { return null; } + + // This will be updated with each loop, and re-Find to dig deeper + Ast? mostSpecificAst = null; + + do + { + ast = ast.Find(currentAst => + { + if (currentAst == mostSpecificAst) { return false; } + + int line = position.LineNumber; + int column = position.ColumnNumber; + + // Performance optimization, skip statements that don't contain the position + if ( + currentAst.Extent.EndLineNumber < line + || currentAst.Extent.StartLineNumber > line + || (currentAst.Extent.EndLineNumber == line && currentAst.Extent.EndColumnNumber < column) + || (currentAst.Extent.StartLineNumber == line && currentAst.Extent.StartColumnNumber > column) + ) + { + return false; + } + + if (allowedTypes is not null && !allowedTypes.Contains(currentAst.GetType())) + { + return false; + } + + if (new ScriptExtentAdapter(currentAst.Extent).Contains(position)) + { + mostSpecificAst = currentAst; + return true; //Stops the find + } + + return false; + }, true); + } while (ast is not null); + + return mostSpecificAst; + } + +} + internal class Utilities { public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) @@ -385,10 +424,10 @@ public static bool AssertContainsDotSourced(Ast ScriptAst) public record ScriptPositionAdapter(IScriptPosition position) : IScriptPosition, IComparable, IComparable, IComparable { public int Line => position.LineNumber; - public int Column => position.ColumnNumber; - public int Character => position.ColumnNumber; public int LineNumber => position.LineNumber; + public int Column => position.ColumnNumber; public int ColumnNumber => position.ColumnNumber; + public int Character => position.ColumnNumber; public string File => position.File; string IScriptPosition.Line => position.Line; @@ -457,13 +496,32 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public IScriptPosition EndScriptPosition => End; public int EndColumnNumber => End.ColumnNumber; public int EndLineNumber => End.LineNumber; - public int StartOffset => extent.EndOffset; + public int StartOffset => extent.StartOffset; public int EndOffset => extent.EndOffset; public string File => extent.File; public int StartColumnNumber => extent.StartColumnNumber; public int StartLineNumber => extent.StartLineNumber; public string Text => extent.Text; - public bool Contains(Position position) => ContainsPosition(this, position); - public static bool ContainsPosition(ScriptExtentAdapter range, ScriptPositionAdapter position) => Range.ContainsPosition(range, position); + public bool Contains(IScriptPosition position) => Contains((ScriptPositionAdapter)position); + + public bool Contains(ScriptPositionAdapter position) + { + if (position.Line < Start.Line || position.Line > End.Line) + { + return false; + } + + if (position.Line == Start.Line && position.Character < Start.Character) + { + return false; + } + + if (position.Line == End.Line && position.Character > End.Character) + { + return false; + } + + return true; + } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs index 3362f5477..2ebbb06df 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -7,7 +7,7 @@ public class RefactorFunctionTestCases { public static RenameTestTarget[] TestCases = [ - new("FunctionsSingle.ps1", Line: 1, Column: 5 ), + new("FunctionsSingle.ps1", Line: 1, Column: 11 ), new("FunctionMultipleOccurrences.ps1", Line: 1, Column: 5 ), new("FunctionInnerIsNested.ps1", Line: 5, Column: 5, "bar"), new("FunctionOuterHasNestedFunction.ps1", Line: 10, Column: 1 ), @@ -19,7 +19,7 @@ public class RefactorFunctionTestCases new("FunctionLoop.ps1", Line: 5, Column: 5 ), new("FunctionForeach.ps1", Line: 5, Column: 11 ), new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), - new("FunctionCallWIthinStringExpression.ps1", Line: 10, Column: 1 ), - new("FunctionNestedRedefinition.ps1", Line: 15, Column: 13 ) + new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ), + new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ) ]; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs index 8943cfbd0..fc08347af 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs @@ -8,11 +8,37 @@ namespace PowerShellEditorServices.Test.Shared.Refactoring; /// /// Describes a test case for renaming a file /// -/// The test case file name e.g. testScript.ps1 -/// The line where the cursor should be positioned for the rename -/// The column/character indent where ther cursor should be positioned for the rename -/// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified -public record RenameTestTarget(string FileName = "UNKNOWN", int Line = -1, int Column = -1, string NewName = "Renamed") +public class RenameTestTarget { + /// + /// The test case file name e.g. testScript.ps1 + /// + public string FileName { get; set; } = "UNKNOWN"; + /// + /// The line where the cursor should be positioned for the rename + /// + public int Line { get; set; } = -1; + /// + /// The column/character indent where ther cursor should be positioned for the rename + /// + public int Column { get; set; } = -1; + /// + /// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified + /// + public string NewName = "Renamed"; + + /// The test case file name e.g. testScript.ps1 + /// The line where the cursor should be positioned for the rename + /// The column/character indent where ther cursor should be positioned for the rename + /// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified + public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed") + { + this.FileName = FileName; + this.Line = Line; + this.Column = Column; + this.NewName = NewName; + } + public RenameTestTarget() { } + public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)}"; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 0e3bf183d..5f81013e0 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -3,6 +3,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -20,6 +21,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Server; using PowerShellEditorServices.Test.Shared.Refactoring; using Xunit; +using Xunit.Abstractions; namespace PowerShellEditorServices.Test.Handlers; @@ -51,17 +53,17 @@ public PrepareRenameHandlerTests() /// Convert test cases into theory data. This keeps us from needing xunit in the test data project /// This type has a special ToString to add a data-driven test name which is why we dont convert directly to the param type first ///
- public static TheoryData FunctionTestCases() - => new(RefactorFunctionTestCases.TestCases); + public static TheoryData VariableTestCases() + => new(RefactorVariableTestCases.TestCases.Select(RenameTestTargetSerializable.FromRenameTestTarget)); - public static TheoryData VariableTestCases() - => new(RefactorVariableTestCases.TestCases); + public static TheoryData FunctionTestCases() + => new(RefactorFunctionTestCases.TestCases.Select(RenameTestTargetSerializable.FromRenameTestTarget)); [Theory] [MemberData(nameof(FunctionTestCases))] - public async Task FindsFunction(RenameTestTarget testTarget) + public async Task FindsFunction(RenameTestTarget s) { - PrepareRenameParams testParams = testTarget.ToPrepareRenameParams("Functions"); + PrepareRenameParams testParams = s.ToPrepareRenameParams("Functions"); RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); @@ -71,9 +73,9 @@ public async Task FindsFunction(RenameTestTarget testTarget) [Theory] [MemberData(nameof(VariableTestCases))] - public async Task FindsVariable(RenameTestTarget testTarget) + public async Task FindsVariable(RenameTestTarget s) { - PrepareRenameParams testParams = testTarget.ToPrepareRenameParams("Variables"); + PrepareRenameParams testParams = s.ToPrepareRenameParams("Variables"); RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); @@ -145,3 +147,56 @@ public class fakeConfigurationService : ILanguageServerConfiguration public ILanguageServerConfiguration RemoveConfigurationItems(IEnumerable configurationItems) => throw new NotImplementedException(); public bool TryGetScopedConfiguration(DocumentUri scopeUri, out IScopedConfiguration configuration) => throw new NotImplementedException(); } + +public static partial class RenameTestTargetExtensions +{ + /// + /// Extension Method to convert a RenameTestTarget to a RenameParams. Needed because RenameTestTarget is in a separate project. + /// + public static RenameParams ToRenameParams(this RenameTestTarget testCase) + => new() + { + Position = new ScriptPositionAdapter(Line: testCase.Line, Column: testCase.Column), + TextDocument = new TextDocumentIdentifier + { + Uri = DocumentUri.FromFileSystemPath( + TestUtilities.GetSharedPath($"Refactoring/Functions/{testCase.FileName}") + ) + }, + NewName = testCase.NewName + }; +} + +/// +/// This is necessary for the MS test explorer to display the test cases +/// Ref: +/// +public class RenameTestTargetSerializable : RenameTestTarget, IXunitSerializable +{ + public RenameTestTargetSerializable() : base() { } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(FileName), FileName); + info.AddValue(nameof(Line), Line); + info.AddValue(nameof(Column), Column); + info.AddValue(nameof(NewName), NewName); + } + + public void Deserialize(IXunitSerializationInfo info) + { + FileName = info.GetValue(nameof(FileName)); + Line = info.GetValue(nameof(Line)); + Column = info.GetValue(nameof(Column)); + NewName = info.GetValue(nameof(NewName)); + } + + public static RenameTestTargetSerializable FromRenameTestTarget(RenameTestTarget t) + => new RenameTestTargetSerializable() + { + FileName = t.FileName, + Column = t.Column, + Line = t.Line, + NewName = t.NewName + }; +} diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index e00b1d6b4..350b2620d 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -43,38 +43,38 @@ public RenameHandlerTests() } // Decided to keep this DAMP instead of DRY due to memberdata boundaries, duplicates with PrepareRenameHandler - public static TheoryData VariableTestCases() - => new(RefactorVariableTestCases.TestCases); + public static TheoryData VariableTestCases() + => new(RefactorVariableTestCases.TestCases.Select(RenameTestTargetSerializable.FromRenameTestTarget)); - public static TheoryData FunctionTestCases() - => new(RefactorFunctionTestCases.TestCases); + public static TheoryData FunctionTestCases() + => new(RefactorFunctionTestCases.TestCases.Select(RenameTestTargetSerializable.FromRenameTestTarget)); [Theory] [MemberData(nameof(VariableTestCases))] - public async void RenamedSymbol(RenameTestTarget request) + public async void RenamedSymbol(RenameTestTarget s) { - string fileName = request.FileName; + string fileName = s.FileName; ScriptFile scriptFile = GetTestScript(fileName); - WorkspaceEdit response = await testHandler.Handle(request.ToRenameParams(), CancellationToken.None); + WorkspaceEdit response = await testHandler.Handle(s.ToRenameParams(), CancellationToken.None); string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; - string actual = GetModifiedScript(scriptFile.Contents, response.Changes[request.ToRenameParams().TextDocument.Uri].ToArray()); + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[s.ToRenameParams().TextDocument.Uri].ToArray()); Assert.Equal(expected, actual); } [Theory] [MemberData(nameof(FunctionTestCases))] - public async void RenamedFunction(RenameTestTarget request) + public async void RenamedFunction(RenameTestTarget s) { - string fileName = request.FileName; + string fileName = s.FileName; ScriptFile scriptFile = GetTestScript(fileName); - WorkspaceEdit response = await testHandler.Handle(request.ToRenameParams(), CancellationToken.None); + WorkspaceEdit response = await testHandler.Handle(s.ToRenameParams(), CancellationToken.None); string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; - string actual = GetModifiedScript(scriptFile.Contents, response.Changes[request.ToRenameParams().TextDocument.Uri].ToArray()); + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[s.ToRenameParams().TextDocument.Uri].ToArray()); Assert.Equal(expected, actual); } @@ -82,19 +82,3 @@ public async void RenamedFunction(RenameTestTarget request) private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); } - -public static partial class RenameTestTargetExtensions -{ - public static RenameParams ToRenameParams(this RenameTestTarget testCase) - => new() - { - Position = new ScriptPositionAdapter(Line: testCase.Line, Column: testCase.Column), - TextDocument = new TextDocumentIdentifier - { - Uri = DocumentUri.FromFileSystemPath( - TestUtilities.GetSharedPath($"Refactoring/Functions/{testCase.FileName}") - ) - }, - NewName = testCase.NewName - }; -} From fa10f68fe7a2e3b2567792b025bd0700fa65f456 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 17 Sep 2024 23:22:22 -0700 Subject: [PATCH 160/203] Fixed all function Prepare tests --- .../TextDocument/Services/RenameService.cs | 69 ++++++++++++------- .../Functions/RefactorFunctionTestCases.cs | 31 +++++---- .../Refactoring/PrepareRenameHandlerTests.cs | 8 +-- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs index b381c34d0..e4cb1f5eb 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -71,14 +71,13 @@ ILanguageServerConfiguration config ScriptPositionAdapter position = request.Position; Ast? target = FindRenamableSymbol(scriptFile, position); - if (target is null) { return null; } - // Will implicitly convert to RangeOrPlaceholder and adjust to 0-based - return target switch - { - FunctionDefinitionAst funcAst => GetFunctionNameExtent(funcAst), - _ => new ScriptExtentAdapter(target.Extent) - }; + // Since 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. + RangeOrPlaceholderRange? renamable = target is null ? null : new RangeOrPlaceholderRange + ( + new RenameDefaultBehavior() { DefaultBehavior = true } + ); + return renamable; } public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) @@ -176,25 +175,36 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam { if (stringAst.Parent is not CommandAst parent) { return null; } if (parent.GetCommandName() != stringAst.Value) { return null; } + if (parent.CommandElements[0] != stringAst) { return null; } + // TODO: Potentially find if function was defined earlier in the file to avoid native executable renames and whatnot? + } + + // Only the function name is valid for rename, not other components + if (ast is FunctionDefinitionAst funcDefAst) + { + if (!GetFunctionNameExtent(funcDefAst).Contains(position)) + { + return null; + } } return ast; } + /// + /// Return an extent that only contains the position of the name of the function, for Client highlighting purposes. + /// private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) { string name = ast.Name; // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present int funcLength = "function ".Length; ScriptExtentAdapter funcExtent = new(ast.Extent); + funcExtent.Start = funcExtent.Start.Delta(0, funcLength); + funcExtent.End = funcExtent.Start.Delta(0, name.Length); - // Get a range that represents only the function name - return funcExtent with - { - Start = funcExtent.Start.Delta(0, funcLength), - End = funcExtent.Start.Delta(0, funcLength + name.Length) - }; + return funcExtent; } } @@ -219,41 +229,47 @@ public static class AstExtensions // This will be updated with each loop, and re-Find to dig deeper Ast? mostSpecificAst = null; + Ast? currentAst = ast; do { - ast = ast.Find(currentAst => + currentAst = currentAst.Find(thisAst => { - if (currentAst == mostSpecificAst) { return false; } + if (thisAst == mostSpecificAst) { return false; } int line = position.LineNumber; int column = position.ColumnNumber; // Performance optimization, skip statements that don't contain the position if ( - currentAst.Extent.EndLineNumber < line - || currentAst.Extent.StartLineNumber > line - || (currentAst.Extent.EndLineNumber == line && currentAst.Extent.EndColumnNumber < column) - || (currentAst.Extent.StartLineNumber == line && currentAst.Extent.StartColumnNumber > column) + thisAst.Extent.EndLineNumber < line + || thisAst.Extent.StartLineNumber > line + || (thisAst.Extent.EndLineNumber == line && thisAst.Extent.EndColumnNumber < column) + || (thisAst.Extent.StartLineNumber == line && thisAst.Extent.StartColumnNumber > column) ) { return false; } - if (allowedTypes is not null && !allowedTypes.Contains(currentAst.GetType())) + if (allowedTypes is not null && !allowedTypes.Contains(thisAst.GetType())) { return false; } - if (new ScriptExtentAdapter(currentAst.Extent).Contains(position)) + if (new ScriptExtentAdapter(thisAst.Extent).Contains(position)) { - mostSpecificAst = currentAst; - return true; //Stops the find + mostSpecificAst = thisAst; + return true; //Stops this particular find and looks more specifically } return false; }, true); - } while (ast is not null); + + if (currentAst is not null) + { + mostSpecificAst = currentAst; + } + } while (currentAst is not null); return mostSpecificAst; } @@ -490,7 +506,10 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public static implicit operator ScriptExtent(ScriptExtentAdapter adapter) => adapter; - public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter); + public static implicit operator RangeOrPlaceholderRange(ScriptExtentAdapter adapter) => new((Range)adapter) + { + DefaultBehavior = new() { DefaultBehavior = false } + }; public IScriptPosition StartScriptPosition => Start; public IScriptPosition EndScriptPosition => End; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs index 2ebbb06df..b30a03c9e 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -5,21 +5,24 @@ namespace PowerShellEditorServices.Test.Shared.Refactoring; public class RefactorFunctionTestCases { + /// + /// Defines where functions should be renamed. These numbers are 1-based. + /// public static RenameTestTarget[] TestCases = [ - new("FunctionsSingle.ps1", Line: 1, Column: 11 ), - new("FunctionMultipleOccurrences.ps1", Line: 1, Column: 5 ), - new("FunctionInnerIsNested.ps1", Line: 5, Column: 5, "bar"), - new("FunctionOuterHasNestedFunction.ps1", Line: 10, Column: 1 ), - new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5, "RenamedInnerFunction"), - new("FunctionWithInternalCalls.ps1", Line: 1, Column: 5 ), - new("FunctionCmdlet.ps1", Line: 10, Column: 1 ), - new("FunctionSameName.ps1", Line: 14, Column: 3, "RenamedSameNameFunction"), - new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), - new("FunctionLoop.ps1", Line: 5, Column: 5 ), - new("FunctionForeach.ps1", Line: 5, Column: 11 ), - new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), - new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ), - new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ) + new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ), + new("FunctionCmdlet.ps1", Line: 1, Column: 10 ), + new("FunctionForeach.ps1", Line: 5, Column: 11 ), + new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), + new("FunctionInnerIsNested.ps1", Line: 5, Column: 5 , "bar"), + new("FunctionLoop.ps1", Line: 5, Column: 5 ), + new("FunctionMultipleOccurrences.ps1", Line: 5, Column: 3 ), + new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ), + new("FunctionOuterHasNestedFunction.ps1", Line: 2, Column: 15 ), + new("FunctionSameName.ps1", Line: 3, Column: 14 , "RenamedSameNameFunction"), + new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), + new("FunctionsSingle.ps1", Line: 1, Column: 11 ), + new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5 , "RenamedInnerFunction"), + new("FunctionWithInternalCalls.ps1", Line: 3, Column: 6 ), ]; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 5f81013e0..683962871 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -67,8 +67,8 @@ public async Task FindsFunction(RenameTestTarget s) RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); - Assert.NotNull(result?.Range); - Assert.True(result.Range.Contains(testParams.Position)); + Assert.NotNull(result); + Assert.True(result?.DefaultBehavior?.DefaultBehavior); } [Theory] @@ -79,8 +79,8 @@ public async Task FindsVariable(RenameTestTarget s) RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); - Assert.NotNull(result?.Range); - Assert.True(result.Range.Contains(testParams.Position)); + Assert.NotNull(result); + Assert.True(result?.DefaultBehavior?.DefaultBehavior); } } From 46119c733f15daf4ba678de8e15fe03013831516 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 17 Sep 2024 23:34:39 -0700 Subject: [PATCH 161/203] Fixed all variable PrepareRenameHandler Tests --- .../Variables/RefactorVariableTestCases.cs | 50 +++++++++---------- .../Refactoring/PrepareRenameHandlerTests.cs | 2 + 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index 4c5c1f9c4..40588c6ee 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -5,30 +5,30 @@ public class RefactorVariableTestCases { public static RenameTestTarget[] TestCases = [ - new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1 ), - new ("VariableRedefinition.ps1", Line: 1, Column: 1 ), - new ("VariableNestedScopeFunction.ps1", Line: 1, Column: 1 ), - new ("VariableInLoop.ps1", Line: 1, Column: 1 ), - new ("VariableInPipeline.ps1", Line: 23, Column: 2 ), - new ("VariableInScriptblockScoped.ps1", Line: 36, Column: 3 ), - new ("VariablewWithinHastableExpression.ps1", Line: 46, Column: 3 ), - new ("VariableNestedFunctionScriptblock.ps1", Line: 20, Column: 4 ), - new ("VariableWithinCommandAstScriptBlock.ps1", Line: 75, Column: 3 ), - new ("VariableWithinForeachObject.ps1", Line: 1, Column: 2 ), - new ("VariableusedInWhileLoop.ps1", Line: 5, Column: 2 ), - new ("VariableInParam.ps1", Line: 16, Column: 24 ), - new ("VariableCommandParameter.ps1", Line: 9, Column: 10 ), - new ("VariableCommandParameter.ps1", Line: 17, Column: 3 ), - new ("VariableScriptWithParamBlock.ps1", Line: 30, Column: 1 ), - new ("VariableNonParam.ps1", Line: 1, Column: 7 ), - new ("VariableParameterCommandWithSameName.ps1", Line: 13, Column: 9 ), - new ("VariableCommandParameterSplatted.ps1", Line: 10, Column: 21 ), - new ("VariableCommandParameterSplatted.ps1", Line: 5, Column: 16 ), - new ("VariableInForeachDuplicateAssignment.ps1", Line: 18, Column: 6 ), - new ("VariableInForloopDuplicateAssignment.ps1", Line: 14, Column: 9 ), - new ("VariableNestedScopeFunctionRefactorInner.ps1", Line: 5, Column: 3 ), - new ("VariableSimpleFunctionParameter.ps1", Line: 9, Column: 6 ), - new ("VariableDotNotationFromInnerFunction.ps1", Line: 26, Column: 11 ), - new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1 ) + new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), + new ("VariableCommandParameter.ps1", Line: 3, Column: 17), + new ("VariableCommandParameter.ps1", Line: 10, Column: 9), + new ("VariableCommandParameterSplatted.ps1", Line: 19, Column: 10), + new ("VariableCommandParameterSplatted.ps1", Line: 8, Column: 6), + new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1), + new ("VariableDotNotationFromInnerFunction.ps1", Line: 11, Column: 26), + new ("VariableInForeachDuplicateAssignment.ps1", Line: 6, Column: 18), + new ("VariableInForloopDuplicateAssignment.ps1", Line: 9, Column: 14), + new ("VariableInLoop.ps1", Line: 1, Column: 1), + new ("VariableInParam.ps1", Line: 24, Column: 16), + new ("VariableInPipeline.ps1", Line: 2, Column: 23), + new ("VariableInScriptblockScoped.ps1", Line: 2, Column: 16), + new ("VariableNestedFunctionScriptblock.ps1", Line: 4, Column: 20), + new ("VariableNestedScopeFunction.ps1", Line: 1, Column: 1), + new ("VariableNestedScopeFunctionRefactorInner.ps1", Line: 3, Column: 5), + new ("VariableNonParam.ps1", Line: 7, Column: 1), + new ("VariableParameterCommandWithSameName.ps1", Line: 9, Column: 13), + new ("VariableRedefinition.ps1", Line: 1, Column: 1), + new ("VariableScriptWithParamBlock.ps1", Line: 1, Column: 30), + new ("VariableSimpleFunctionParameter.ps1", Line: 6, Column: 9), + new ("VariableusedInWhileLoop.ps1", Line: 2, Column: 5), + new ("VariableWithinCommandAstScriptBlock.ps1", Line: 3, Column: 75), + new ("VariableWithinForeachObject.ps1", Line: 2, Column: 1), + new ("VariablewWithinHastableExpression.ps1", Line: 3, Column: 46), ]; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 683962871..322e8f493 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -82,6 +82,8 @@ public async Task FindsVariable(RenameTestTarget s) Assert.NotNull(result); Assert.True(result?.DefaultBehavior?.DefaultBehavior); } + + // TODO: Bad Path Tests (strings, parameters, etc.) } public static partial class RenameTestTargetExtensions From 05e0b5515679c61df4e60ca1c7e50f5b04ad99f4 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 23 Sep 2024 13:29:20 +0200 Subject: [PATCH 162/203] First Stage work to move to a more stateless AstVisitor for renames --- ...onVistor.cs => IterativeFunctionRename.cs} | 15 +- .../Handlers/CompletionHandler.cs | 2 +- .../TextDocument/Services/RenameService.cs | 202 ++++++++++++++++-- .../Refactoring/PrepareRenameHandlerTests.cs | 4 +- .../Refactoring/RenameHandlerTests.cs | 43 ++-- 5 files changed, 216 insertions(+), 50 deletions(-) rename src/PowerShellEditorServices/Services/PowerShell/Refactoring/{IterativeFunctionVistor.cs => IterativeFunctionRename.cs} (95%) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs similarity index 95% rename from src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs rename to src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs index aa1e84609..441d3b4aa 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionVistor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs @@ -29,16 +29,16 @@ public IterativeFunctionRename(string OldName, string NewName, int StartLineNumb this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - Ast Node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptAst, - typeof(FunctionDefinitionAst), typeof(CommandAst)); + ScriptPosition position = new(null, StartLineNumber, StartColumnNumber, null); + Ast node = ScriptAst.FindAtPosition(position, [typeof(FunctionDefinitionAst), typeof(CommandAst)]); - if (Node != null) + if (node != null) { - if (Node is FunctionDefinitionAst FuncDef && FuncDef.Name.ToLower() == OldName.ToLower()) + if (node is FunctionDefinitionAst funcDef && funcDef.Name.ToLower() == OldName.ToLower()) { - TargetFunctionAst = FuncDef; + TargetFunctionAst = funcDef; } - if (Node is CommandAst commdef && commdef.GetCommandName().ToLower() == OldName.ToLower()) + if (node is CommandAst commdef && commdef.GetCommandName().ToLower() == OldName.ToLower()) { TargetFunctionAst = Utilities.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); if (TargetFunctionAst == null) @@ -57,6 +57,7 @@ public class NodeProcessingState public bool ShouldRename { get; set; } public IEnumerator ChildrenEnumerator { get; set; } } + public bool DetermineChildShouldRenameState(NodeProcessingState currentState, Ast child) { // The Child Has the name we are looking for @@ -75,7 +76,6 @@ public bool DetermineChildShouldRenameState(NodeProcessingState currentState, As DuplicateFunctionAst = funcDef; return false; } - } else if (child?.Parent?.Parent is ScriptBlockAst) { @@ -105,6 +105,7 @@ public bool DetermineChildShouldRenameState(NodeProcessingState currentState, As } return currentState.ShouldRename; } + public void Visit(Ast root) { Stack processingStack = new(); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index 153d5d330..a291ea409 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -270,7 +270,7 @@ internal CompletionItem CreateCompletionItem( { Validate.IsNotNull(nameof(result), result); - OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit textEdit = new() + TextEdit textEdit = new() { NewText = result.CompletionText, Range = new Range diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs index e4cb1f5eb..0fa27b79b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -109,28 +109,13 @@ ILanguageServerConfiguration config // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading - internal static TextEdit[] RenameFunction(Ast token, Ast scriptAst, RenameParams renameParams) + internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParams renameParams) { - ScriptPositionAdapter position = renameParams.Position; - - string tokenName = ""; - if (token is FunctionDefinitionAst funcDef) - { - tokenName = funcDef.Name; - } - else if (token.Parent is CommandAst CommAst) + if (target is not FunctionDefinitionAst or CommandAst) { - tokenName = CommAst.GetCommandName(); + throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); } - IterativeFunctionRename visitor = new( - tokenName, - renameParams.NewName, - position.Line, - position.Column, - scriptAst - ); - visitor.Visit(scriptAst); - return visitor.Modifications.ToArray(); + } internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) @@ -195,7 +180,7 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam /// /// Return an extent that only contains the position of the name of the function, for Client highlighting purposes. /// - private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) + public static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) { string name = ast.Name; // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present @@ -208,9 +193,132 @@ private static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst a } } +/// +/// A visitor that renames a function given a particular target. The Edits property contains the edits when complete. +/// You should use a new instance for each rename operation. +/// Skipverify can be used as a performance optimization when you are sure you are in scope. +/// +/// +public class RenameFunctionVisitor(Ast target, string oldName, string newName, bool skipVerify = false) : AstVisitor +{ + public List Edits { get; } = new(); + private Ast? CurrentDocument; + + // Wire up our visitor to the relevant AST types we are potentially renaming + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast) => Visit(ast); + public override AstVisitAction VisitCommand(CommandAst ast) => Visit(ast); + + public AstVisitAction Visit(Ast ast) + { + /// If this is our first run, we need to verify we are in scope. + if (!skipVerify && CurrentDocument is null) + { + if (ast.Find(ast => ast == target, true) is null) + { + throw new TargetSymbolNotFoundException("The target this visitor would rename is not present in the AST. This is a bug and you should file an issue"); + } + CurrentDocument = ast; + + // If our target was a command, we need to find the original function. + if (target is CommandAst command) + { + target = CurrentDocument.GetFunctionDefinition(command) + ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition."); + } + } + if (CurrentDocument != ast) + { + throw new TargetSymbolNotFoundException("The visitor should not be reused to rename a different document. It should be created new for each rename operation. This is a bug and you should file an issue"); + } + + if (ShouldRename(ast)) + { + Edits.Add(GetRenameFunctionEdit(ast)); + return AstVisitAction.Continue; + } + else + { + return AstVisitAction.SkipChildren; + } + + /// TODO: Is there a way we can know we are fully outside where the function might be referenced, and if so, call a AstVisitAction Abort as a perf optimization? + } + + public bool ShouldRename(Ast candidate) + { + // There should be only one function definition and if it is not our target, it may be a duplicately named function + if (candidate is FunctionDefinitionAst funcDef) + { + return funcDef == target; + } + + if (candidate is not CommandAst) + { + throw new InvalidOperationException($"ShouldRename for a function had an Unexpected Ast Type {candidate.GetType()}. This is a bug and you should file an issue."); + } + + // Determine if calls of the function are in the same scope as the function definition + if (candidate?.Parent?.Parent is ScriptBlockAst) + { + return target.Parent.Parent == candidate.Parent.Parent; + } + else if (candidate?.Parent is StatementBlockAst) + { + return candidate.Parent == target.Parent; + } + + // If we get this far, we hit an edge case + throw new InvalidOperationException("ShouldRename for a function could not determine the viability of a rename. This is a bug and you should file an issue."); + } + + private TextEdit GetRenameFunctionEdit(Ast candidate) + { + if (candidate is FunctionDefinitionAst funcDef) + { + if (funcDef != target) + { + throw new InvalidOperationException("GetRenameFunctionEdit was called on an Ast that was not the target. This is a bug and you should file an issue."); + } + + ScriptExtentAdapter functionNameExtent = RenameService.GetFunctionNameExtent(funcDef); + + return new TextEdit() + { + NewText = newName, + Range = functionNameExtent + }; + } + + // Should be CommandAst past this point. + if (candidate is not CommandAst command) + { + throw new InvalidOperationException($"Expected a command but got {candidate.GetType()}"); + } + + if (command.GetCommandName()?.ToLower() == oldName.ToLower() && + target.Extent.StartLineNumber <= command.Extent.StartLineNumber) + { + if (command.CommandElements[0] is not StringConstantExpressionAst funcName) + { + throw new InvalidOperationException("Command element should always have a string expresssion as its first item. This is a bug and you should report it."); + } + + return new TextEdit() + { + NewText = newName, + Range = new ScriptExtentAdapter(funcName.Extent) + }; + } + + throw new InvalidOperationException("GetRenameFunctionEdit was not provided a FuncitonDefinition or a CommandAst"); + } +} + public class RenameSymbolOptions { public bool CreateAlias { get; set; } + + } @@ -274,6 +382,55 @@ public static class AstExtensions return mostSpecificAst; } + public static FunctionDefinitionAst? GetFunctionDefinition(this Ast ast, CommandAst command) + { + string? name = command.GetCommandName(); + if (name is null) { return null; } + + List FunctionDefinitions = ast.FindAll(ast => + { + return ast is FunctionDefinitionAst funcDef && + funcDef.Name.ToLower() == name && + (funcDef.Extent.EndLineNumber < command.Extent.StartLineNumber || + (funcDef.Extent.EndColumnNumber <= command.Extent.StartColumnNumber && + funcDef.Extent.EndLineNumber <= command.Extent.StartLineNumber)); + }, true).Cast().ToList(); + + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Determine which function definition is the right one + FunctionDefinitionAst? CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (command?.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } } internal class Utilities @@ -453,9 +610,10 @@ public ScriptPositionAdapter(int Line, int Column) : this(new ScriptPosition(nul public ScriptPositionAdapter(ScriptPosition position) : this((IScriptPosition)position) { } public ScriptPositionAdapter(Position position) : this(position.Line + 1, position.Character + 1) { } + public static implicit operator ScriptPositionAdapter(Position position) => new(position); public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new - ( +( scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 ); @@ -522,7 +680,7 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public int StartLineNumber => extent.StartLineNumber; public string Text => extent.Text; - public bool Contains(IScriptPosition position) => Contains((ScriptPositionAdapter)position); + public bool Contains(IScriptPosition position) => Contains(new ScriptPositionAdapter(position)); public bool Contains(ScriptPositionAdapter position) { diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 322e8f493..b7c034c4a 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -155,14 +155,14 @@ public static partial class RenameTestTargetExtensions /// /// Extension Method to convert a RenameTestTarget to a RenameParams. Needed because RenameTestTarget is in a separate project. /// - public static RenameParams ToRenameParams(this RenameTestTarget testCase) + public static RenameParams ToRenameParams(this RenameTestTarget testCase, string subPath) => new() { Position = new ScriptPositionAdapter(Line: testCase.Line, Column: testCase.Column), TextDocument = new TextDocumentIdentifier { Uri = DocumentUri.FromFileSystemPath( - TestUtilities.GetSharedPath($"Refactoring/Functions/{testCase.FileName}") + TestUtilities.GetSharedPath($"Refactoring/{subPath}/{testCase.FileName}") ) }, NewName = testCase.NewName diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index 350b2620d..0a4a89b8a 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -9,7 +9,6 @@ using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using static PowerShellEditorServices.Test.Refactoring.RefactorUtilities; -using System.IO; using System.Linq; using System.Threading; using Xunit; @@ -50,35 +49,43 @@ public static TheoryData FunctionTestCases() => new(RefactorFunctionTestCases.TestCases.Select(RenameTestTargetSerializable.FromRenameTestTarget)); [Theory] - [MemberData(nameof(VariableTestCases))] - public async void RenamedSymbol(RenameTestTarget s) + [MemberData(nameof(FunctionTestCases))] + public async void RenamedFunction(RenameTestTarget s) { - string fileName = s.FileName; - ScriptFile scriptFile = GetTestScript(fileName); + RenameParams request = s.ToRenameParams("Functions"); + WorkspaceEdit response = await testHandler.Handle(request, CancellationToken.None); + DocumentUri testScriptUri = request.TextDocument.Uri; - WorkspaceEdit response = await testHandler.Handle(s.ToRenameParams(), CancellationToken.None); + string expected = workspace.GetFile + ( + testScriptUri.ToString().Substring(0, testScriptUri.ToString().Length - 4) + "Renamed.ps1" + ).Contents; + + ScriptFile scriptFile = workspace.GetFile(testScriptUri); - string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; - string actual = GetModifiedScript(scriptFile.Contents, response.Changes[s.ToRenameParams().TextDocument.Uri].ToArray()); + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[testScriptUri].ToArray()); + Assert.NotEmpty(response.Changes[testScriptUri]); Assert.Equal(expected, actual); } [Theory] - [MemberData(nameof(FunctionTestCases))] - public async void RenamedFunction(RenameTestTarget s) + [MemberData(nameof(VariableTestCases))] + public async void RenamedVariable(RenameTestTarget s) { - string fileName = s.FileName; - ScriptFile scriptFile = GetTestScript(fileName); + RenameParams request = s.ToRenameParams("Variables"); + WorkspaceEdit response = await testHandler.Handle(request, CancellationToken.None); + DocumentUri testScriptUri = request.TextDocument.Uri; - WorkspaceEdit response = await testHandler.Handle(s.ToRenameParams(), CancellationToken.None); + string expected = workspace.GetFile + ( + testScriptUri.ToString().Substring(0, testScriptUri.ToString().Length - 4) + "Renamed.ps1" + ).Contents; - string expected = GetTestScript(fileName.Substring(0, fileName.Length - 4) + "Renamed.ps1").Contents; - string actual = GetModifiedScript(scriptFile.Contents, response.Changes[s.ToRenameParams().TextDocument.Uri].ToArray()); + ScriptFile scriptFile = workspace.GetFile(testScriptUri); + + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[testScriptUri].ToArray()); Assert.Equal(expected, actual); } - - private ScriptFile GetTestScript(string fileName) => - workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Functions", fileName))); } From 5b4f54dcc45db337015b47f423935f004b3f5f30 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 11:39:42 +0200 Subject: [PATCH 163/203] Separate out AstExtensions and continue Functions Reimplement. FunctionsSingle test works at least --- .../Language/AstExtensions.cs | 171 ++++++++++++ .../Refactoring/IterativeFunctionRename.cs | 210 --------------- .../Handlers/CompletionHandler.cs | 2 +- .../TextDocument/Services/RenameService.cs | 246 ++++-------------- 4 files changed, 222 insertions(+), 407 deletions(-) create mode 100644 src/PowerShellEditorServices/Language/AstExtensions.cs delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs diff --git a/src/PowerShellEditorServices/Language/AstExtensions.cs b/src/PowerShellEditorServices/Language/AstExtensions.cs new file mode 100644 index 000000000..2dcb87dde --- /dev/null +++ b/src/PowerShellEditorServices/Language/AstExtensions.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Services; + +namespace Microsoft.PowerShell.EditorServices.Language; + +public static class AstExtensions +{ + /// + /// Finds the most specific Ast at the given script position, or returns null if none found.
+ /// For example, if the position is on a variable expression within a function definition, + /// the variable will be returned even if the function definition is found first. + ///
+ internal static Ast? FindAtPosition(this Ast ast, IScriptPosition position, Type[]? allowedTypes) + { + // Short circuit quickly if the position is not in the provided range, no need to traverse if not + // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened. + if (!new ScriptExtentAdapter(ast.Extent).Contains(position)) { return null; } + + // This will be updated with each loop, and re-Find to dig deeper + Ast? mostSpecificAst = null; + Ast? currentAst = ast; + + do + { + currentAst = currentAst.Find(thisAst => + { + if (thisAst == mostSpecificAst) { return false; } + + int line = position.LineNumber; + int column = position.ColumnNumber; + + // Performance optimization, skip statements that don't contain the position + if ( + thisAst.Extent.EndLineNumber < line + || thisAst.Extent.StartLineNumber > line + || (thisAst.Extent.EndLineNumber == line && thisAst.Extent.EndColumnNumber < column) + || (thisAst.Extent.StartLineNumber == line && thisAst.Extent.StartColumnNumber > column) + ) + { + return false; + } + + if (allowedTypes is not null && !allowedTypes.Contains(thisAst.GetType())) + { + return false; + } + + if (new ScriptExtentAdapter(thisAst.Extent).Contains(position)) + { + mostSpecificAst = thisAst; + return true; //Stops this particular find and looks more specifically + } + + return false; + }, true); + + if (currentAst is not null) + { + mostSpecificAst = currentAst; + } + } while (currentAst is not null); + + return mostSpecificAst; + } + + public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command) + { + string? name = command.GetCommandName(); + if (name is null) { return null; } + + List FunctionDefinitions = ast.FindAll(ast => + { + return ast is FunctionDefinitionAst funcDef && + funcDef.Name.ToLower() == name && + (funcDef.Extent.EndLineNumber < command.Extent.StartLineNumber || + (funcDef.Extent.EndColumnNumber <= command.Extent.StartColumnNumber && + funcDef.Extent.EndLineNumber <= command.Extent.StartLineNumber)); + }, true).Cast().ToList(); + + // return the function def if we only have one match + if (FunctionDefinitions.Count == 1) + { + return FunctionDefinitions[0]; + } + // Determine which function definition is the right one + FunctionDefinitionAst? CorrectDefinition = null; + for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) + { + FunctionDefinitionAst element = FunctionDefinitions[i]; + + Ast parent = element.Parent; + // walk backwards till we hit a functiondefinition if any + while (null != parent) + { + if (parent is FunctionDefinitionAst) + { + break; + } + parent = parent.Parent; + } + // we have hit the global scope of the script file + if (null == parent) + { + CorrectDefinition = element; + break; + } + + if (command?.Parent == parent) + { + CorrectDefinition = (FunctionDefinitionAst)parent; + } + } + return CorrectDefinition; + } + + + public static Ast[] FindParents(this Ast ast, params Type[] type) + { + List parents = new(); + Ast parent = ast; + while (parent is not null) + { + if (type.Contains(parent.GetType())) + { + parents.Add(parent); + } + parent = parent.Parent; + } + return parents.ToArray(); + } + + public static Ast GetHighestParent(this Ast ast) + => ast.Parent is null ? ast : ast.Parent.GetHighestParent(); + + public static Ast GetHighestParent(this Ast ast, params Type[] type) + => FindParents(ast, type).LastOrDefault() ?? ast; + + /// + /// Gets the closest parent that matches the specified type or null if none found. + /// + public static Ast? FindParent(this Ast ast, params Type[] type) + => FindParents(ast, type).FirstOrDefault(); + + /// + /// Gets the closest parent that matches the specified type or null if none found. + /// + public static T? FindParent(this Ast ast) where T : Ast + => ast.FindParent(typeof(T)) as T; + + public static bool HasParent(this Ast ast, Ast parent) + { + Ast? current = ast; + while (current is not null) + { + if (current == parent) + { + return true; + } + current = current.Parent; + } + return false; + } + +} diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs deleted file mode 100644 index 441d3b4aa..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeFunctionRename.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.IO; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Services; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; - -namespace Microsoft.PowerShell.EditorServices.Refactoring -{ - - internal class IterativeFunctionRename - { - private readonly string OldName; - private readonly string NewName; - public List Modifications = []; - internal int StartLineNumber; - internal int StartColumnNumber; - internal FunctionDefinitionAst TargetFunctionAst; - internal FunctionDefinitionAst DuplicateFunctionAst; - internal readonly Ast ScriptAst; - - public IterativeFunctionRename(string OldName, string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - this.OldName = OldName; - this.NewName = NewName; - this.StartLineNumber = StartLineNumber; - this.StartColumnNumber = StartColumnNumber; - this.ScriptAst = ScriptAst; - - ScriptPosition position = new(null, StartLineNumber, StartColumnNumber, null); - Ast node = ScriptAst.FindAtPosition(position, [typeof(FunctionDefinitionAst), typeof(CommandAst)]); - - if (node != null) - { - if (node is FunctionDefinitionAst funcDef && funcDef.Name.ToLower() == OldName.ToLower()) - { - TargetFunctionAst = funcDef; - } - if (node is CommandAst commdef && commdef.GetCommandName().ToLower() == OldName.ToLower()) - { - TargetFunctionAst = Utilities.GetFunctionDefByCommandAst(OldName, StartLineNumber, StartColumnNumber, ScriptAst); - if (TargetFunctionAst == null) - { - throw new FunctionDefinitionNotFoundException(); - } - this.StartColumnNumber = TargetFunctionAst.Extent.StartColumnNumber; - this.StartLineNumber = TargetFunctionAst.Extent.StartLineNumber; - } - } - } - - public class NodeProcessingState - { - public Ast Node { get; set; } - public bool ShouldRename { get; set; } - public IEnumerator ChildrenEnumerator { get; set; } - } - - public bool DetermineChildShouldRenameState(NodeProcessingState currentState, Ast child) - { - // The Child Has the name we are looking for - if (child is FunctionDefinitionAst funcDef && funcDef.Name.ToLower() == OldName.ToLower()) - { - // The Child is the function we are looking for - if (child.Extent.StartLineNumber == StartLineNumber && - child.Extent.StartColumnNumber == StartColumnNumber) - { - return true; - - } - // Otherwise its a duplicate named function - else - { - DuplicateFunctionAst = funcDef; - return false; - } - } - else if (child?.Parent?.Parent is ScriptBlockAst) - { - // The Child is in the same scriptblock as the Target Function - if (TargetFunctionAst.Parent.Parent == child?.Parent?.Parent) - { - return true; - } - // The Child is in the same ScriptBlock as the Duplicate Function - if (DuplicateFunctionAst?.Parent?.Parent == child?.Parent?.Parent) - { - return false; - } - } - else if (child?.Parent is StatementBlockAst) - { - - if (child?.Parent == TargetFunctionAst?.Parent) - { - return true; - } - - if (DuplicateFunctionAst?.Parent == child?.Parent) - { - return false; - } - } - return currentState.ShouldRename; - } - - public void Visit(Ast root) - { - Stack processingStack = new(); - - processingStack.Push(new NodeProcessingState { Node = root, ShouldRename = false }); - - while (processingStack.Count > 0) - { - NodeProcessingState currentState = processingStack.Peek(); - - if (currentState.ChildrenEnumerator == null) - { - // First time processing this node. Do the initial processing. - ProcessNode(currentState.Node, currentState.ShouldRename); // This line is crucial. - - // Get the children and set up the enumerator. - IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); - currentState.ChildrenEnumerator = children.GetEnumerator(); - } - - // Process the next child. - if (currentState.ChildrenEnumerator.MoveNext()) - { - Ast child = currentState.ChildrenEnumerator.Current; - bool childShouldRename = DetermineChildShouldRenameState(currentState, child); - processingStack.Push(new NodeProcessingState { Node = child, ShouldRename = childShouldRename }); - } - else - { - // All children have been processed, we're done with this node. - processingStack.Pop(); - } - } - } - - public void ProcessNode(Ast node, bool shouldRename) - { - - switch (node) - { - case FunctionDefinitionAst ast: - if (ast.Name.ToLower() == OldName.ToLower()) - { - if (ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber) - { - TargetFunctionAst = ast; - int functionPrefixLength = "function ".Length; - int functionNameStartColumn = ast.Extent.StartColumnNumber + functionPrefixLength; - - TextEdit change = new() - { - NewText = NewName, - // HACK: Because we cannot get a token extent of the function name itself, we have to adjust to find it here - // TOOD: Parse the upfront and use offsets probably to get the function name token - Range = new( - new ScriptPositionAdapter( - ast.Extent.StartLineNumber, - functionNameStartColumn - ), - new ScriptPositionAdapter( - ast.Extent.StartLineNumber, - functionNameStartColumn + OldName.Length - ) - ) - }; - - Modifications.Add(change); - //node.ShouldRename = true; - } - else - { - // Entering a duplicate functions scope and shouldnt rename - //node.ShouldRename = false; - DuplicateFunctionAst = ast; - } - } - break; - case CommandAst ast: - if (ast.GetCommandName()?.ToLower() == OldName.ToLower() && - TargetFunctionAst.Extent.StartLineNumber <= ast.Extent.StartLineNumber) - { - if (shouldRename) - { - // What we weant to rename is actually the first token of the command - if (ast.CommandElements[0] is not StringConstantExpressionAst funcName) - { - throw new InvalidDataException("Command element should always have a string expresssion as its first item. This is a bug and you should report it."); - } - TextEdit change = new() - { - NewText = NewName, - Range = new ScriptExtentAdapter(funcName.Extent) - }; - Modifications.Add(change); - } - } - break; - } - } - } -} diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index a291ea409..29e36ce25 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -374,7 +374,7 @@ private CompletionItem CreateProviderItemCompletion( } InsertTextFormat insertFormat; - OmniSharp.Extensions.LanguageServer.Protocol.Models.TextEdit edit; + TextEdit edit; CompletionItemKind itemKind; if (result.ResultType is CompletionResultType.ProviderContainer && SupportsSnippets diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs index 0fa27b79b..d894648ea 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Language; using Microsoft.PowerShell.EditorServices.Refactoring; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -95,7 +96,7 @@ ILanguageServerConfiguration config FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), // FIXME: Only throw if capability is not prepareprovider - _ => throw new HandlerErrorException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") + _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; return new WorkspaceEdit @@ -116,6 +117,8 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); } + RenameFunctionVisitor visitor = new(target, renameParams.NewName); + return visitor.VisitAndGetEdits(scriptAst); } internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) @@ -194,15 +197,15 @@ public static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst as } /// -/// A visitor that renames a function given a particular target. The Edits property contains the edits when complete. +/// A visitor that generates a list of TextEdits to a TextDocument to rename a PowerShell function /// You should use a new instance for each rename operation. /// Skipverify can be used as a performance optimization when you are sure you are in scope. /// -/// -public class RenameFunctionVisitor(Ast target, string oldName, string newName, bool skipVerify = false) : AstVisitor +public class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : AstVisitor { public List Edits { get; } = new(); private Ast? CurrentDocument; + private string OldName = string.Empty; // Wire up our visitor to the relevant AST types we are potentially renaming public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast) => Visit(ast); @@ -210,30 +213,34 @@ public class RenameFunctionVisitor(Ast target, string oldName, string newName, b public AstVisitAction Visit(Ast ast) { - /// If this is our first run, we need to verify we are in scope. + // If this is our first run, we need to verify we are in scope and gather our rename operation info if (!skipVerify && CurrentDocument is null) { if (ast.Find(ast => ast == target, true) is null) { throw new TargetSymbolNotFoundException("The target this visitor would rename is not present in the AST. This is a bug and you should file an issue"); } - CurrentDocument = ast; + CurrentDocument = ast.GetHighestParent(); - // If our target was a command, we need to find the original function. - if (target is CommandAst command) + FunctionDefinitionAst functionDef = target switch { - target = CurrentDocument.GetFunctionDefinition(command) - ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition."); - } - } - if (CurrentDocument != ast) + FunctionDefinitionAst f => f, + CommandAst command => CurrentDocument.FindFunctionDefinition(command) + ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), + _ => throw new Exception("Unsupported AST type encountered") + }; + + OldName = functionDef.Name; + }; + + if (CurrentDocument != ast.GetHighestParent()) { throw new TargetSymbolNotFoundException("The visitor should not be reused to rename a different document. It should be created new for each rename operation. This is a bug and you should file an issue"); } if (ShouldRename(ast)) { - Edits.Add(GetRenameFunctionEdit(ast)); + Edits.Add(GetRenameFunctionEdits(ast)); return AstVisitAction.Continue; } else @@ -241,10 +248,10 @@ public AstVisitAction Visit(Ast ast) return AstVisitAction.SkipChildren; } - /// TODO: Is there a way we can know we are fully outside where the function might be referenced, and if so, call a AstVisitAction Abort as a perf optimization? + // TODO: Is there a way we can know we are fully outside where the function might be referenced, and if so, call a AstVisitAction Abort as a perf optimization? } - public bool ShouldRename(Ast candidate) + private bool ShouldRename(Ast candidate) { // There should be only one function definition and if it is not our target, it may be a duplicately named function if (candidate is FunctionDefinitionAst funcDef) @@ -252,26 +259,39 @@ public bool ShouldRename(Ast candidate) return funcDef == target; } - if (candidate is not CommandAst) + // Should only be CommandAst (function calls) from this point forward in the visit. + if (candidate is not CommandAst command) { throw new InvalidOperationException($"ShouldRename for a function had an Unexpected Ast Type {candidate.GetType()}. This is a bug and you should file an issue."); } - // Determine if calls of the function are in the same scope as the function definition - if (candidate?.Parent?.Parent is ScriptBlockAst) + if (command.GetCommandName().ToLower() != OldName.ToLower()) { - return target.Parent.Parent == candidate.Parent.Parent; + return false; } - else if (candidate?.Parent is StatementBlockAst) + + // TODO: Use position comparisons here + // Command calls must always come after the function definitions + if ( + target.Extent.StartLineNumber > command.Extent.StartLineNumber + || ( + target.Extent.StartLineNumber == command.Extent.StartLineNumber + && target.Extent.StartColumnNumber >= command.Extent.StartColumnNumber + ) + ) { - return candidate.Parent == target.Parent; + return false; } + // If the command is defined in the same parent scope as the function + return command.HasParent(target.Parent); + + // If we get this far, we hit an edge case throw new InvalidOperationException("ShouldRename for a function could not determine the viability of a rename. This is a bug and you should file an issue."); } - private TextEdit GetRenameFunctionEdit(Ast candidate) + private TextEdit GetRenameFunctionEdits(Ast candidate) { if (candidate is FunctionDefinitionAst funcDef) { @@ -295,7 +315,7 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) throw new InvalidOperationException($"Expected a command but got {candidate.GetType()}"); } - if (command.GetCommandName()?.ToLower() == oldName.ToLower() && + if (command.GetCommandName()?.ToLower() == OldName.ToLower() && target.Extent.StartLineNumber <= command.Extent.StartLineNumber) { if (command.CommandElements[0] is not StringConstantExpressionAst funcName) @@ -312,125 +332,17 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) throw new InvalidOperationException("GetRenameFunctionEdit was not provided a FuncitonDefinition or a CommandAst"); } + + public TextEdit[] VisitAndGetEdits(Ast ast) + { + ast.Visit(this); + return Edits.ToArray(); + } } public class RenameSymbolOptions { public bool CreateAlias { get; set; } - - -} - - -public static class AstExtensions -{ - /// - /// Finds the most specific Ast at the given script position, or returns null if none found.
- /// For example, if the position is on a variable expression within a function definition, - /// the variable will be returned even if the function definition is found first. - ///
- internal static Ast? FindAtPosition(this Ast ast, IScriptPosition position, Type[]? allowedTypes) - { - // Short circuit quickly if the position is not in the provided range, no need to traverse if not - // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened. - if (!new ScriptExtentAdapter(ast.Extent).Contains(position)) { return null; } - - // This will be updated with each loop, and re-Find to dig deeper - Ast? mostSpecificAst = null; - Ast? currentAst = ast; - - do - { - currentAst = currentAst.Find(thisAst => - { - if (thisAst == mostSpecificAst) { return false; } - - int line = position.LineNumber; - int column = position.ColumnNumber; - - // Performance optimization, skip statements that don't contain the position - if ( - thisAst.Extent.EndLineNumber < line - || thisAst.Extent.StartLineNumber > line - || (thisAst.Extent.EndLineNumber == line && thisAst.Extent.EndColumnNumber < column) - || (thisAst.Extent.StartLineNumber == line && thisAst.Extent.StartColumnNumber > column) - ) - { - return false; - } - - if (allowedTypes is not null && !allowedTypes.Contains(thisAst.GetType())) - { - return false; - } - - if (new ScriptExtentAdapter(thisAst.Extent).Contains(position)) - { - mostSpecificAst = thisAst; - return true; //Stops this particular find and looks more specifically - } - - return false; - }, true); - - if (currentAst is not null) - { - mostSpecificAst = currentAst; - } - } while (currentAst is not null); - - return mostSpecificAst; - } - - public static FunctionDefinitionAst? GetFunctionDefinition(this Ast ast, CommandAst command) - { - string? name = command.GetCommandName(); - if (name is null) { return null; } - - List FunctionDefinitions = ast.FindAll(ast => - { - return ast is FunctionDefinitionAst funcDef && - funcDef.Name.ToLower() == name && - (funcDef.Extent.EndLineNumber < command.Extent.StartLineNumber || - (funcDef.Extent.EndColumnNumber <= command.Extent.StartColumnNumber && - funcDef.Extent.EndLineNumber <= command.Extent.StartLineNumber)); - }, true).Cast().ToList(); - - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Determine which function definition is the right one - FunctionDefinitionAst? CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (command?.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; - } } internal class Utilities @@ -464,64 +376,6 @@ internal class Utilities parent = parent.Parent; } return null; - - } - - public static FunctionDefinitionAst? GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - // Look up the targeted object - CommandAst? TargetCommand = (CommandAst?)GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptFile - , typeof(CommandAst)); - - if (TargetCommand?.GetCommandName().ToLower() != OldName.ToLower()) - { - TargetCommand = null; - } - - string? FunctionName = TargetCommand?.GetCommandName(); - - List FunctionDefinitions = ScriptFile.FindAll(ast => - { - return ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower() && - (FuncDef.Extent.EndLineNumber < TargetCommand?.Extent.StartLineNumber || - (FuncDef.Extent.EndColumnNumber <= TargetCommand?.Extent.StartColumnNumber && - FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Determine which function definition is the right one - FunctionDefinitionAst? CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (TargetCommand?.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; } public static bool AssertContainsDotSourced(Ast ScriptAst) From 3f81bcd48b469109d06b22227762b73f18a20e81 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 14:31:06 +0200 Subject: [PATCH 164/203] Move renameservice --- .../{Services => }/RenameService.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) rename src/PowerShellEditorServices/Services/TextDocument/{Services => }/RenameService.cs (95%) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs similarity index 95% rename from src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs rename to src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index d894648ea..04ff33c1c 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Services/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -112,7 +112,7 @@ ILanguageServerConfiguration config internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParams renameParams) { - if (target is not FunctionDefinitionAst or CommandAst) + if (target is not (FunctionDefinitionAst or CommandAst)) { throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); } @@ -151,22 +151,9 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam // Filters just the ASTs that are candidates for rename typeof(FunctionDefinitionAst), typeof(VariableExpressionAst), - typeof(CommandParameterAst), - typeof(ParameterAst), - typeof(StringConstantExpressionAst), typeof(CommandAst) ]); - // Special detection for Function calls that dont follow verb-noun syntax e.g. DoThing - // It's not foolproof but should work in most cases where it is explicit (e.g. not & $x) - if (ast is StringConstantExpressionAst stringAst) - { - if (stringAst.Parent is not CommandAst parent) { return null; } - if (parent.GetCommandName() != stringAst.Value) { return null; } - if (parent.CommandElements[0] != stringAst) { return null; } - // TODO: Potentially find if function was defined earlier in the file to avoid native executable renames and whatnot? - } - // Only the function name is valid for rename, not other components if (ast is FunctionDefinitionAst funcDefAst) { @@ -176,6 +163,20 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam } } + // Only the command name (function call) portion is renamable + if (ast is CommandAst command) + { + if (command.CommandElements[0] is not StringConstantExpressionAst name) + { + return null; + } + + if (!new ScriptExtentAdapter(name.Extent).Contains(position)) + { + return null; + } + } + return ast; } @@ -216,18 +217,18 @@ public AstVisitAction Visit(Ast ast) // If this is our first run, we need to verify we are in scope and gather our rename operation info if (!skipVerify && CurrentDocument is null) { - if (ast.Find(ast => ast == target, true) is null) + CurrentDocument = ast.GetHighestParent(); + if (CurrentDocument.Find(ast => ast == target, true) is null) { throw new TargetSymbolNotFoundException("The target this visitor would rename is not present in the AST. This is a bug and you should file an issue"); } - CurrentDocument = ast.GetHighestParent(); FunctionDefinitionAst functionDef = target switch { FunctionDefinitionAst f => f, CommandAst command => CurrentDocument.FindFunctionDefinition(command) ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), - _ => throw new Exception("Unsupported AST type encountered") + _ => throw new Exception($"Unsupported AST type {target.GetType()} encountered") }; OldName = functionDef.Name; @@ -286,7 +287,6 @@ private bool ShouldRename(Ast candidate) // If the command is defined in the same parent scope as the function return command.HasParent(target.Parent); - // If we get this far, we hit an edge case throw new InvalidOperationException("ShouldRename for a function could not determine the viability of a rename. This is a bug and you should file an issue."); } From 00ea69f4eebb01b6d574a0015e8ce6e881725d07 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 16:20:45 +0200 Subject: [PATCH 165/203] Breakout and simplify matching logic, fixes more test cases --- .../Language/AstExtensions.cs | 66 +++++++++---------- .../Services/TextDocument/RenameService.cs | 60 +++++------------ 2 files changed, 48 insertions(+), 78 deletions(-) diff --git a/src/PowerShellEditorServices/Language/AstExtensions.cs b/src/PowerShellEditorServices/Language/AstExtensions.cs index 2dcb87dde..cf0e7746a 100644 --- a/src/PowerShellEditorServices/Language/AstExtensions.cs +++ b/src/PowerShellEditorServices/Language/AstExtensions.cs @@ -72,54 +72,48 @@ public static class AstExtensions public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command) { - string? name = command.GetCommandName(); + string? name = command.GetCommandName().ToLower(); if (name is null) { return null; } - List FunctionDefinitions = ast.FindAll(ast => + FunctionDefinitionAst[] candidateFuncDefs = ast.FindAll(ast => { - return ast is FunctionDefinitionAst funcDef && - funcDef.Name.ToLower() == name && - (funcDef.Extent.EndLineNumber < command.Extent.StartLineNumber || - (funcDef.Extent.EndColumnNumber <= command.Extent.StartColumnNumber && - funcDef.Extent.EndLineNumber <= command.Extent.StartLineNumber)); - }, true).Cast().ToList(); - - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Determine which function definition is the right one - FunctionDefinitionAst? CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; + if (ast is not FunctionDefinitionAst funcDef) + { + return false; + } - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) + if (funcDef.Name.ToLower() != name) { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; + return false; } - // we have hit the global scope of the script file - if (null == parent) + + // If the function is recursive (calls itself), its parent is a match unless a more specific in-scope function definition comes next (this is a "bad practice" edge case) + // TODO: Consider a simple "contains" match + if (command.HasParent(funcDef)) { - CorrectDefinition = element; - break; + return true; } - if (command?.Parent == parent) + if + ( + // TODO: Replace with a position match + funcDef.Extent.EndLineNumber > command.Extent.StartLineNumber + || + ( + funcDef.Extent.EndLineNumber == command.Extent.StartLineNumber + && funcDef.Extent.EndColumnNumber >= command.Extent.StartColumnNumber + ) + ) { - CorrectDefinition = (FunctionDefinitionAst)parent; + return false; } - } - return CorrectDefinition; - } + return command.HasParent(funcDef.Parent); // The command is in the same scope as the function definition + }, true).Cast().ToArray(); + + // There should only be one match most of the time, the only other cases is when a function is defined multiple times (bad practice). If there are multiple definitions, the candidate "closest" to the command, which would be the last one found, is the appropriate one + return candidateFuncDefs.LastOrDefault(); + } public static Ast[] FindParents(this Ast ast, params Type[] type) { diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 04ff33c1c..fcc181bac 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -206,7 +206,7 @@ public class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = { public List Edits { get; } = new(); private Ast? CurrentDocument; - private string OldName = string.Empty; + private FunctionDefinitionAst? FunctionToRename; // Wire up our visitor to the relevant AST types we are potentially renaming public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast) => Visit(ast); @@ -223,15 +223,13 @@ public AstVisitAction Visit(Ast ast) throw new TargetSymbolNotFoundException("The target this visitor would rename is not present in the AST. This is a bug and you should file an issue"); } - FunctionDefinitionAst functionDef = target switch + FunctionToRename = target switch { FunctionDefinitionAst f => f, CommandAst command => CurrentDocument.FindFunctionDefinition(command) ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), _ => throw new Exception($"Unsupported AST type {target.GetType()} encountered") }; - - OldName = functionDef.Name; }; if (CurrentDocument != ast.GetHighestParent()) @@ -241,7 +239,7 @@ public AstVisitAction Visit(Ast ast) if (ShouldRename(ast)) { - Edits.Add(GetRenameFunctionEdits(ast)); + Edits.Add(GetRenameFunctionEdit(ast)); return AstVisitAction.Continue; } else @@ -254,10 +252,10 @@ public AstVisitAction Visit(Ast ast) private bool ShouldRename(Ast candidate) { - // There should be only one function definition and if it is not our target, it may be a duplicately named function + // Rename our original function definition. There may be duplicate definitions of the same name if (candidate is FunctionDefinitionAst funcDef) { - return funcDef == target; + return funcDef == FunctionToRename; } // Should only be CommandAst (function calls) from this point forward in the visit. @@ -266,36 +264,20 @@ private bool ShouldRename(Ast candidate) throw new InvalidOperationException($"ShouldRename for a function had an Unexpected Ast Type {candidate.GetType()}. This is a bug and you should file an issue."); } - if (command.GetCommandName().ToLower() != OldName.ToLower()) - { - return false; - } - - // TODO: Use position comparisons here - // Command calls must always come after the function definitions - if ( - target.Extent.StartLineNumber > command.Extent.StartLineNumber - || ( - target.Extent.StartLineNumber == command.Extent.StartLineNumber - && target.Extent.StartColumnNumber >= command.Extent.StartColumnNumber - ) - ) + if (CurrentDocument is null) { - return false; + throw new InvalidOperationException("CurrentDoc should always be set by now from first Visit. This is a bug and you should file an issue."); } - // If the command is defined in the same parent scope as the function - return command.HasParent(target.Parent); - - // If we get this far, we hit an edge case - throw new InvalidOperationException("ShouldRename for a function could not determine the viability of a rename. This is a bug and you should file an issue."); + // Match up the command to its function definition + return CurrentDocument.FindFunctionDefinition(command) == FunctionToRename; } - private TextEdit GetRenameFunctionEdits(Ast candidate) + private TextEdit GetRenameFunctionEdit(Ast candidate) { if (candidate is FunctionDefinitionAst funcDef) { - if (funcDef != target) + if (funcDef != FunctionToRename) { throw new InvalidOperationException("GetRenameFunctionEdit was called on an Ast that was not the target. This is a bug and you should file an issue."); } @@ -315,22 +297,16 @@ private TextEdit GetRenameFunctionEdits(Ast candidate) throw new InvalidOperationException($"Expected a command but got {candidate.GetType()}"); } - if (command.GetCommandName()?.ToLower() == OldName.ToLower() && - target.Extent.StartLineNumber <= command.Extent.StartLineNumber) + if (command.CommandElements[0] is not StringConstantExpressionAst funcName) { - if (command.CommandElements[0] is not StringConstantExpressionAst funcName) - { - throw new InvalidOperationException("Command element should always have a string expresssion as its first item. This is a bug and you should report it."); - } - - return new TextEdit() - { - NewText = newName, - Range = new ScriptExtentAdapter(funcName.Extent) - }; + throw new InvalidOperationException("Command element should always have a string expresssion as its first item. This is a bug and you should report it."); } - throw new InvalidOperationException("GetRenameFunctionEdit was not provided a FuncitonDefinition or a CommandAst"); + return new TextEdit() + { + NewText = newName, + Range = new ScriptExtentAdapter(funcName.Extent) + }; } public TextEdit[] VisitAndGetEdits(Ast ast) From 8b8f4850e984d17c5e9b4dad3c9b5c82d7bcc0c2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 16:50:55 +0200 Subject: [PATCH 166/203] SkipChildren doesn't work for nested function situations. More tests fixed --- .../Services/TextDocument/RenameService.cs | 6 +----- .../Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 | 6 +++--- .../Refactoring/Functions/FunctionWithInnerFunction.ps1 | 2 +- .../Functions/FunctionWithInnerFunctionRenamed.ps1 | 6 +++--- .../Refactoring/Functions/RefactorFunctionTestCases.cs | 6 +++--- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index fcc181bac..9b427c2f1 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -240,12 +240,8 @@ public AstVisitAction Visit(Ast ast) if (ShouldRename(ast)) { Edits.Add(GetRenameFunctionEdit(ast)); - return AstVisitAction.Continue; - } - else - { - return AstVisitAction.SkipChildren; } + return AstVisitAction.Continue; // TODO: Is there a way we can know we are fully outside where the function might be referenced, and if so, call a AstVisitAction Abort as a perf optimization? } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 index 2231571ef..18a30767e 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 @@ -1,8 +1,8 @@ function outer { - function bar { - Write-Host "Inside nested foo" + function Renamed { + Write-Host 'Inside nested foo' } - bar + Renamed } function foo { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 index 966fdccb7..1e77268e4 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunction.ps1 @@ -1,6 +1,6 @@ function OuterFunction { function NewInnerFunction { - Write-Host "This is the inner function" + Write-Host 'This is the inner function' } NewInnerFunction } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 index 47e51012e..177d5940b 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionWithInnerFunctionRenamed.ps1 @@ -1,7 +1,7 @@ function OuterFunction { - function RenamedInnerFunction { - Write-Host "This is the inner function" + function Renamed { + Write-Host 'This is the inner function' } - RenamedInnerFunction + Renamed } OuterFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs index b30a03c9e..d7b58941c 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -14,15 +14,15 @@ public class RefactorFunctionTestCases new("FunctionCmdlet.ps1", Line: 1, Column: 10 ), new("FunctionForeach.ps1", Line: 5, Column: 11 ), new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), - new("FunctionInnerIsNested.ps1", Line: 5, Column: 5 , "bar"), + new("FunctionInnerIsNested.ps1", Line: 5, Column: 5 ), new("FunctionLoop.ps1", Line: 5, Column: 5 ), new("FunctionMultipleOccurrences.ps1", Line: 5, Column: 3 ), new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ), new("FunctionOuterHasNestedFunction.ps1", Line: 2, Column: 15 ), - new("FunctionSameName.ps1", Line: 3, Column: 14 , "RenamedSameNameFunction"), + new("FunctionSameName.ps1", Line: 3, Column: 14 , "RenamedSameNameFunction"), new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), new("FunctionsSingle.ps1", Line: 1, Column: 11 ), - new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5 , "RenamedInnerFunction"), + new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5 ), new("FunctionWithInternalCalls.ps1", Line: 3, Column: 6 ), ]; } From d187a39c7a1d7f0e8774963bdc6288ebe5ad3d9a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 17:07:09 +0200 Subject: [PATCH 167/203] All rename function tests fixed --- src/PowerShellEditorServices/Language/AstExtensions.cs | 2 +- .../Refactoring/Functions/FunctionInnerIsNested.ps1 | 4 ++-- .../Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 | 2 +- .../Refactoring/Functions/RefactorFunctionTestCases.cs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Language/AstExtensions.cs b/src/PowerShellEditorServices/Language/AstExtensions.cs index cf0e7746a..4a56e196e 100644 --- a/src/PowerShellEditorServices/Language/AstExtensions.cs +++ b/src/PowerShellEditorServices/Language/AstExtensions.cs @@ -72,7 +72,7 @@ public static class AstExtensions public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command) { - string? name = command.GetCommandName().ToLower(); + string? name = command.GetCommandName()?.ToLower(); if (name is null) { return null; } FunctionDefinitionAst[] candidateFuncDefs = ast.FindAll(ast => diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 index 8e99c337b..fe67c234d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNested.ps1 @@ -1,12 +1,12 @@ function outer { function foo { - Write-Host "Inside nested foo" + Write-Host 'Inside nested foo' } foo } function foo { - Write-Host "Inside top-level foo" + Write-Host 'Inside top-level foo' } outer diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 index 18a30767e..8e698a3f1 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionInnerIsNestedRenamed.ps1 @@ -6,7 +6,7 @@ function outer { } function foo { - Write-Host "Inside top-level foo" + Write-Host 'Inside top-level foo' } outer diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs index d7b58941c..3ef34a999 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -12,13 +12,13 @@ public class RefactorFunctionTestCases [ new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ), new("FunctionCmdlet.ps1", Line: 1, Column: 10 ), - new("FunctionForeach.ps1", Line: 5, Column: 11 ), - new("FunctionForeachObject.ps1", Line: 5, Column: 11 ), + new("FunctionForeach.ps1", Line: 11, Column: 5 ), + new("FunctionForeachObject.ps1", Line: 11, Column: 5 ), new("FunctionInnerIsNested.ps1", Line: 5, Column: 5 ), new("FunctionLoop.ps1", Line: 5, Column: 5 ), new("FunctionMultipleOccurrences.ps1", Line: 5, Column: 3 ), new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ), - new("FunctionOuterHasNestedFunction.ps1", Line: 2, Column: 15 ), + new("FunctionOuterHasNestedFunction.ps1", Line: 1, Column: 10 ), new("FunctionSameName.ps1", Line: 3, Column: 14 , "RenamedSameNameFunction"), new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), new("FunctionsSingle.ps1", Line: 1, Column: 11 ), From 089d611e47a90d62a8454f971d3fe9ff0b942b76 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 18:30:19 +0200 Subject: [PATCH 168/203] Add disclaimer scaffolding (needs config) --- .../Server/PsesLanguageServer.cs | 2 + .../Services/TextDocument/RenameService.cs | 90 ++++++++++++++----- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 8b62e85eb..8e646563d 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -91,6 +91,8 @@ public async Task StartAsync() .ClearProviders() .AddPsesLanguageServerLogging() .SetMinimumLevel(_minimumLogLevel)) + // TODO: Consider replacing all WithHandler with AddSingleton + .WithConfigurationSection("powershell") .WithHandler() .WithHandler() .WithHandler() diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 9b427c2f1..fcd859860 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -8,6 +8,7 @@ using System.Management.Automation.Language; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Language; using Microsoft.PowerShell.EditorServices.Refactoring; @@ -40,40 +41,26 @@ internal class RenameService( ILanguageServerConfiguration config ) : IRenameService { + private bool disclaimerDeclined; + public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { - // FIXME: Config actually needs to be read and implemented, this is to make the referencing satisfied - // config.ToString(); - // ShowMessageRequestParams reqParams = new() - // { - // Type = MessageType.Warning, - // Message = "Test Send", - // Actions = new MessageActionItem[] { - // new MessageActionItem() { Title = "I Accept" }, - // new MessageActionItem() { Title = "I Accept [Workspace]" }, - // new MessageActionItem() { Title = "Decline" } - // } - // }; - - // MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); - // if (result.Title == "Test Action") - // { - // // FIXME: Need to accept - // Console.WriteLine("yay"); - // } + if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to take rename actions inside the dotsourced file. + // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to expect rename actions to propogate. if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) { throw new HandlerErrorException("Dot Source detected, this is currently not supported"); } + // TODO: FindRenamableSymbol may create false positives for renaming, so we probably should go ahead and execute a full rename and return true if edits are found. + ScriptPositionAdapter position = request.Position; Ast? target = FindRenamableSymbol(scriptFile, position); - // Since 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. + // Since LSP 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. RangeOrPlaceholderRange? renamable = target is null ? null : new RangeOrPlaceholderRange ( new RenameDefaultBehavior() { DefaultBehavior = true } @@ -83,6 +70,7 @@ ILanguageServerConfiguration config public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) { + if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); ScriptPositionAdapter position = request.Position; @@ -195,6 +183,66 @@ public static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst as return funcExtent; } + + /// + /// Prompts the user to accept the rename disclaimer. + /// + /// true if accepted, false if rejected + private async Task AcceptRenameDisclaimer(CancellationToken cancellationToken) + { + // User has declined for the session so we don't want this popping up a bunch. + if (disclaimerDeclined) { return false; } + + // FIXME: This should be referencing an options type that is initialized with the Service or is a getter. + if (config.GetSection("powershell").GetValue("acceptRenameDisclaimer")) { return true; } + + // TODO: Localization + const string acceptAnswer = "I Accept"; + const string acceptWorkspaceAnswer = "I Accept [Workspace]"; + const string acceptSessionAnswer = "I Accept [Session]"; + const string declineAnswer = "Decline"; + ShowMessageRequestParams reqParams = new() + { + Type = MessageType.Warning, + Message = "Test Send", + Actions = new MessageActionItem[] { + new MessageActionItem() { Title = acceptAnswer }, + new MessageActionItem() { Title = acceptWorkspaceAnswer }, + new MessageActionItem() { Title = acceptSessionAnswer }, + new MessageActionItem() { Title = declineAnswer } + } + }; + + MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); + if (result.Title == declineAnswer) + { + ShowMessageParams msgParams = new() + { + Message = "PowerShell Rename functionality will be disabled for this session and you will not be prompted again until restart.", + Type = MessageType.Info + }; + lsp.SendNotification(msgParams); + disclaimerDeclined = true; + return !disclaimerDeclined; + } + if (result.Title == acceptAnswer) + { + // FIXME: Set the appropriate setting + return true; + } + if (result.Title == acceptWorkspaceAnswer) + { + // FIXME: Set the appropriate setting + return true; + } + if (result.Title == acceptSessionAnswer) + { + // FIXME: Set the appropriate setting + return true; + } + + throw new InvalidOperationException("Unknown Disclaimer Response received. This is a bug and you should report it."); + } } /// From b92debfbc5de949a62c1d67e669bdb360dcc16c6 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 19:01:27 +0200 Subject: [PATCH 169/203] Add more config setup and lock down some classes --- .../Services/TextDocument/RenameService.cs | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index fcd859860..6a2f85571 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -19,6 +19,13 @@ namespace Microsoft.PowerShell.EditorServices.Services; +internal class RenameServiceOptions +{ + internal bool createFunctionAlias { get; set; } + internal bool createVariableAlias { get; set; } + internal bool acceptDisclaimer { get; set; } +} + public interface IRenameService { /// @@ -38,15 +45,17 @@ public interface IRenameService internal class RenameService( WorkspaceService workspaceService, ILanguageServerFacade lsp, - ILanguageServerConfiguration config + ILanguageServerConfiguration config, + string configSection = "powershell.rename" ) : IRenameService { private bool disclaimerDeclined; + private readonly RenameServiceOptions options = new(); public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { + config.GetSection(configSection).Bind(options); if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to expect rename actions to propogate. @@ -70,6 +79,7 @@ ILanguageServerConfiguration config public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) { + config.GetSection(configSection).Bind(options); if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); @@ -172,7 +182,7 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam /// /// Return an extent that only contains the position of the name of the function, for Client highlighting purposes. /// - public static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) + internal static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) { string name = ast.Name; // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present @@ -194,17 +204,19 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo if (disclaimerDeclined) { return false; } // FIXME: This should be referencing an options type that is initialized with the Service or is a getter. - if (config.GetSection("powershell").GetValue("acceptRenameDisclaimer")) { return true; } + if (options.acceptDisclaimer) { return true; } // TODO: Localization + const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. Please review the notice and understand the limitations and risks."; const string acceptAnswer = "I Accept"; const string acceptWorkspaceAnswer = "I Accept [Workspace]"; const string acceptSessionAnswer = "I Accept [Session]"; const string declineAnswer = "Decline"; + ShowMessageRequestParams reqParams = new() { Type = MessageType.Warning, - Message = "Test Send", + Message = renameDisclaimer, Actions = new MessageActionItem[] { new MessageActionItem() { Title = acceptAnswer }, new MessageActionItem() { Title = acceptWorkspaceAnswer }, @@ -216,9 +228,11 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); if (result.Title == declineAnswer) { + const string renameDisabledNotice = "PowerShell Rename functionality will be disabled for this session and you will not be prompted again until restart."; + ShowMessageParams msgParams = new() { - Message = "PowerShell Rename functionality will be disabled for this session and you will not be prompted again until restart.", + Message = renameDisabledNotice, Type = MessageType.Info }; lsp.SendNotification(msgParams); @@ -250,9 +264,9 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo /// You should use a new instance for each rename operation. /// Skipverify can be used as a performance optimization when you are sure you are in scope. /// -public class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : AstVisitor +internal class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : AstVisitor { - public List Edits { get; } = new(); + internal List Edits { get; } = new(); private Ast? CurrentDocument; private FunctionDefinitionAst? FunctionToRename; @@ -353,18 +367,13 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) }; } - public TextEdit[] VisitAndGetEdits(Ast ast) + internal TextEdit[] VisitAndGetEdits(Ast ast) { ast.Visit(this); return Edits.ToArray(); } } -public class RenameSymbolOptions -{ - public bool CreateAlias { get; set; } -} - internal class Utilities { public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) @@ -519,8 +528,8 @@ public int CompareTo(ScriptPositionAdapter other) /// internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent { - public ScriptPositionAdapter Start = new(extent.StartScriptPosition); - public ScriptPositionAdapter End = new(extent.EndScriptPosition); + internal ScriptPositionAdapter Start = new(extent.StartScriptPosition); + internal ScriptPositionAdapter End = new(extent.EndScriptPosition); public static implicit operator ScriptExtentAdapter(ScriptExtent extent) => new(extent); From db22ff2e58080af2b608c0b09719302b76d222a4 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 24 Sep 2024 18:12:58 -0400 Subject: [PATCH 170/203] Add configuration and adjust opt-in message due to server-side LSP config limitation --- .../Refactoring/IterativeVariableVisitor.cs | 8 +-- .../Services/TextDocument/RenameService.cs | 64 +++++++++++-------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index 14cf50a7a..c9b7f7984 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -24,15 +24,15 @@ internal class IterativeVariableRename internal bool isParam; internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; - internal RenameSymbolOptions options; + internal RenameServiceOptions options; - public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, RenameSymbolOptions options = null) + public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, RenameServiceOptions options) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.options = options ?? new RenameSymbolOptions { CreateAlias = false }; + this.options = options; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -404,7 +404,7 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - options.CreateAlias) + options.createVariableAlias) { TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 6a2f85571..ba1026f2d 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -19,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Services; -internal class RenameServiceOptions +public class RenameServiceOptions { internal bool createFunctionAlias { get; set; } internal bool createVariableAlias { get; set; } @@ -50,11 +50,15 @@ internal class RenameService( ) : IRenameService { private bool disclaimerDeclined; - private readonly RenameServiceOptions options = new(); + private bool disclaimerAccepted; + + private readonly RenameServiceOptions settings = new(); + + internal void RefreshSettings() => config.GetSection(configSection).Bind(settings); public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { - config.GetSection(configSection).Bind(options); + if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); @@ -79,7 +83,6 @@ internal class RenameService( public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) { - config.GetSection(configSection).Bind(options); if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); @@ -119,7 +122,7 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam return visitor.VisitAndGetEdits(scriptAst); } - internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) + internal TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) { if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { @@ -129,11 +132,10 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst, - null //FIXME: Pass through Alias config + settings ); visitor.Visit(scriptAst); return visitor.Modifications.ToArray(); - } return []; } @@ -200,28 +202,32 @@ internal static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst /// true if accepted, false if rejected private async Task AcceptRenameDisclaimer(CancellationToken cancellationToken) { + // Fetch the latest settings from the client, in case they have changed. + config.GetSection(configSection).Bind(settings); + // User has declined for the session so we don't want this popping up a bunch. if (disclaimerDeclined) { return false; } - // FIXME: This should be referencing an options type that is initialized with the Service or is a getter. - if (options.acceptDisclaimer) { return true; } + if (settings.acceptDisclaimer || disclaimerAccepted) { return true; } // TODO: Localization const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. Please review the notice and understand the limitations and risks."; const string acceptAnswer = "I Accept"; - const string acceptWorkspaceAnswer = "I Accept [Workspace]"; - const string acceptSessionAnswer = "I Accept [Session]"; + // const string acceptWorkspaceAnswer = "I Accept [Workspace]"; + // const string acceptSessionAnswer = "I Accept [Session]"; const string declineAnswer = "Decline"; + // TODO: Unfortunately the LSP spec has no spec for the server to change a client setting, so + // We have a suboptimal experience until we implement a custom feature for this. ShowMessageRequestParams reqParams = new() { Type = MessageType.Warning, Message = renameDisclaimer, Actions = new MessageActionItem[] { new MessageActionItem() { Title = acceptAnswer }, - new MessageActionItem() { Title = acceptWorkspaceAnswer }, - new MessageActionItem() { Title = acceptSessionAnswer }, new MessageActionItem() { Title = declineAnswer } + // new MessageActionItem() { Title = acceptWorkspaceAnswer }, + // new MessageActionItem() { Title = acceptSessionAnswer }, } }; @@ -241,19 +247,27 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo } if (result.Title == acceptAnswer) { - // FIXME: Set the appropriate setting - return true; - } - if (result.Title == acceptWorkspaceAnswer) - { - // FIXME: Set the appropriate setting - return true; - } - if (result.Title == acceptSessionAnswer) - { - // FIXME: Set the appropriate setting - return true; + const string acceptDisclaimerNotice = "PowerShell rename functionality has been enabled for this session. To avoid this prompt in the future, set the powershell.rename.acceptDisclaimer to true in your settings."; + ShowMessageParams msgParams = new() + { + Message = acceptDisclaimerNotice, + Type = MessageType.Info + }; + lsp.SendNotification(msgParams); + + disclaimerAccepted = true; + return disclaimerAccepted; } + // if (result.Title == acceptWorkspaceAnswer) + // { + // // FIXME: Set the appropriate setting + // return true; + // } + // if (result.Title == acceptSessionAnswer) + // { + // // FIXME: Set the appropriate setting + // return true; + // } throw new InvalidOperationException("Unknown Disclaimer Response received. This is a bug and you should report it."); } From ddd65b34af684fadc588e150f29e3c8ea822b292 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 25 Sep 2024 01:16:07 -0400 Subject: [PATCH 171/203] Add mocks for configuration for testing, still need to fix some parameter tests --- .../Server/PsesLanguageServer.cs | 2 +- .../Refactoring/IterativeVariableVisitor.cs | 8 +- .../Services/TextDocument/RenameService.cs | 84 +++++++++---------- .../Refactoring/PrepareRenameHandlerTests.cs | 15 ++-- .../Refactoring/RenameHandlerTests.cs | 3 +- 5 files changed, 54 insertions(+), 58 deletions(-) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 8e646563d..042e4e8fa 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -92,7 +92,7 @@ public async Task StartAsync() .AddPsesLanguageServerLogging() .SetMinimumLevel(_minimumLogLevel)) // TODO: Consider replacing all WithHandler with AddSingleton - .WithConfigurationSection("powershell") + .WithConfigurationSection("powershell.rename") .WithHandler() .WithHandler() .WithHandler() diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index c9b7f7984..fa556c18a 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -24,15 +24,15 @@ internal class IterativeVariableRename internal bool isParam; internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; - internal RenameServiceOptions options; + internal bool CreateAlias; - public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, RenameServiceOptions options) + public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateAlias) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.options = options; + this.CreateAlias = CreateAlias; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -404,7 +404,7 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - options.createVariableAlias) + CreateAlias) { TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Modifications.Add(aliasChange); diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index ba1026f2d..6b7c9bc58 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -19,11 +19,14 @@ namespace Microsoft.PowerShell.EditorServices.Services; +/// +/// Used with Configuration Bind to sync the settings to what is set on the client. +/// public class RenameServiceOptions { - internal bool createFunctionAlias { get; set; } - internal bool createVariableAlias { get; set; } - internal bool acceptDisclaimer { get; set; } + public bool createFunctionAlias { get; set; } + public bool createVariableAlias { get; set; } + public bool acceptDisclaimer { get; set; } } public interface IRenameService @@ -46,44 +49,44 @@ internal class RenameService( WorkspaceService workspaceService, ILanguageServerFacade lsp, ILanguageServerConfiguration config, + bool disclaimerDeclinedForSession = false, + bool disclaimerAcceptedForSession = false, string configSection = "powershell.rename" ) : IRenameService { - private bool disclaimerDeclined; - private bool disclaimerAccepted; - - private readonly RenameServiceOptions settings = new(); - internal void RefreshSettings() => config.GetSection(configSection).Bind(settings); + private async Task GetScopedSettings(DocumentUri uri, CancellationToken cancellationToken = default) + { + IScopedConfiguration scopedConfig = await config.GetScopedConfiguration(uri, cancellationToken).ConfigureAwait(false); + return scopedConfig.GetSection(configSection).Get() ?? new RenameServiceOptions(); + } public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { - - if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } - ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); - - // TODO: Is this too aggressive? We can still rename inside a var/function even if dotsourcing is in use in a file, we just need to be clear it's not supported to expect rename actions to propogate. - if (Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)) + RenameParams renameRequest = new() { - throw new HandlerErrorException("Dot Source detected, this is currently not supported"); - } - - // TODO: FindRenamableSymbol may create false positives for renaming, so we probably should go ahead and execute a full rename and return true if edits are found. - - ScriptPositionAdapter position = request.Position; - Ast? target = FindRenamableSymbol(scriptFile, position); + NewName = "PREPARERENAMETEST", //A placeholder just to gather edits + Position = request.Position, + TextDocument = request.TextDocument + }; + // TODO: Should we cache these resuls and just fetch them on the actual rename, and move the bulk to an implementation method? + WorkspaceEdit? renameResponse = await RenameSymbol(renameRequest, cancellationToken).ConfigureAwait(false); // Since LSP 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. - RangeOrPlaceholderRange? renamable = target is null ? null : new RangeOrPlaceholderRange - ( - new RenameDefaultBehavior() { DefaultBehavior = true } - ); - return renamable; + return (renameResponse?.Changes?[request.TextDocument.Uri].ToArray().Length > 0) + ? new RangeOrPlaceholderRange + ( + new RenameDefaultBehavior() { DefaultBehavior = true } + ) + : null; } public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) { - if (!await AcceptRenameDisclaimer(cancellationToken).ConfigureAwait(false)) { return null; } + // We want scoped settings because a workspace setting might be relevant here. + RenameServiceOptions options = await GetScopedSettings(request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); + + if (!await AcceptRenameDisclaimer(options.acceptDisclaimer, cancellationToken).ConfigureAwait(false)) { return null; } ScriptFile scriptFile = workspaceService.GetFile(request.TextDocument.Uri); ScriptPositionAdapter position = request.Position; @@ -95,7 +98,7 @@ internal class RenameService( TextEdit[] changes = tokenToRename switch { FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), - VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), + VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), // FIXME: Only throw if capability is not prepareprovider _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; @@ -122,17 +125,16 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam return visitor.VisitAndGetEdits(scriptAst); } - internal TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) + internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createAlias) { if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) { - IterativeVariableRename visitor = new( requestParams.NewName, symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst, - settings + createAlias ); visitor.Visit(scriptAst); return visitor.Modifications.ToArray(); @@ -200,15 +202,10 @@ internal static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst /// Prompts the user to accept the rename disclaimer. /// /// true if accepted, false if rejected - private async Task AcceptRenameDisclaimer(CancellationToken cancellationToken) + private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, CancellationToken cancellationToken) { - // Fetch the latest settings from the client, in case they have changed. - config.GetSection(configSection).Bind(settings); - - // User has declined for the session so we don't want this popping up a bunch. - if (disclaimerDeclined) { return false; } - - if (settings.acceptDisclaimer || disclaimerAccepted) { return true; } + if (disclaimerDeclinedForSession) { return false; } + if (acceptDisclaimerOption || disclaimerAcceptedForSession) { return true; } // TODO: Localization const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. Please review the notice and understand the limitations and risks."; @@ -242,8 +239,8 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo Type = MessageType.Info }; lsp.SendNotification(msgParams); - disclaimerDeclined = true; - return !disclaimerDeclined; + disclaimerDeclinedForSession = true; + return !disclaimerDeclinedForSession; } if (result.Title == acceptAnswer) { @@ -255,8 +252,8 @@ private async Task AcceptRenameDisclaimer(CancellationToken cancellationTo }; lsp.SendNotification(msgParams); - disclaimerAccepted = true; - return disclaimerAccepted; + disclaimerAcceptedForSession = true; + return disclaimerAcceptedForSession; } // if (result.Title == acceptWorkspaceAnswer) // { @@ -514,7 +511,6 @@ public ScriptPositionAdapter(Position position) : this(position.Line + 1, positi scriptPosition.position.LineNumber - 1, scriptPosition.position.ColumnNumber - 1 ); - public static implicit operator ScriptPositionAdapter(ScriptPosition position) => new(position); public static implicit operator ScriptPosition(ScriptPositionAdapter position) => position; diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index b7c034c4a..4ecbb5f20 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -9,7 +9,6 @@ using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Primitives; using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Test.Shared; @@ -44,7 +43,8 @@ public PrepareRenameHandlerTests() ( workspace, new fakeLspSendMessageRequestFacade("I Accept"), - new fakeConfigurationService() + new EmptyConfiguration(), + disclaimerAcceptedForSession: true //Suppresses prompts ) ); } @@ -134,18 +134,17 @@ public async Task SendRequest(IRequest request, public bool TryGetRequest(long id, out string method, out TaskCompletionSource pendingTask) => throw new NotImplementedException(); } -public class fakeConfigurationService : ILanguageServerConfiguration + + +public class EmptyConfiguration : ConfigurationRoot, ILanguageServerConfiguration, IScopedConfiguration { - public string this[string key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public EmptyConfiguration() : base([]) { } public bool IsSupported => throw new NotImplementedException(); public ILanguageServerConfiguration AddConfigurationItems(IEnumerable configurationItems) => throw new NotImplementedException(); - public IEnumerable GetChildren() => throw new NotImplementedException(); public Task GetConfiguration(params ConfigurationItem[] items) => throw new NotImplementedException(); - public IChangeToken GetReloadToken() => throw new NotImplementedException(); - public Task GetScopedConfiguration(DocumentUri scopeUri, CancellationToken cancellationToken) => throw new NotImplementedException(); - public IConfigurationSection GetSection(string key) => throw new NotImplementedException(); + public Task GetScopedConfiguration(DocumentUri scopeUri, CancellationToken cancellationToken) => Task.FromResult((IScopedConfiguration)this); public ILanguageServerConfiguration RemoveConfigurationItems(IEnumerable configurationItems) => throw new NotImplementedException(); public bool TryGetScopedConfiguration(DocumentUri scopeUri, out IScopedConfiguration configuration) => throw new NotImplementedException(); } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index 0a4a89b8a..19c8869ce 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -36,7 +36,8 @@ public RenameHandlerTests() ( workspace, new fakeLspSendMessageRequestFacade("I Accept"), - new fakeConfigurationService() + new EmptyConfiguration(), + disclaimerAcceptedForSession: true //Disables UI prompts ) ); } From 7bb469319e8ed8ecb4306721ede1bec5be2f3b54 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 25 Sep 2024 01:30:59 -0400 Subject: [PATCH 172/203] Bonus Assertions --- .../Refactoring/Variables/RefactorVariableTestCases.cs | 4 ++-- .../Refactoring/RenameHandlerTests.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index 40588c6ee..e8ad88fe2 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -8,8 +8,8 @@ public class RefactorVariableTestCases new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), new ("VariableCommandParameter.ps1", Line: 10, Column: 9), - new ("VariableCommandParameterSplatted.ps1", Line: 19, Column: 10), - new ("VariableCommandParameterSplatted.ps1", Line: 8, Column: 6), + new ("VariableCommandParameterSplatted.ps1", Line: 16, Column: 5), + new ("VariableCommandParameterSplatted.ps1", Line: 21, Column: 11), new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1), new ("VariableDotNotationFromInnerFunction.ps1", Line: 11, Column: 26), new ("VariableInForeachDuplicateAssignment.ps1", Line: 6, Column: 18), diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index 19c8869ce..e9e022c50 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -64,9 +64,10 @@ public async void RenamedFunction(RenameTestTarget s) ScriptFile scriptFile = workspace.GetFile(testScriptUri); + Assert.NotEmpty(response.Changes[testScriptUri]); + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[testScriptUri].ToArray()); - Assert.NotEmpty(response.Changes[testScriptUri]); Assert.Equal(expected, actual); } @@ -85,6 +86,9 @@ public async void RenamedVariable(RenameTestTarget s) ScriptFile scriptFile = workspace.GetFile(testScriptUri); + Assert.NotNull(response); + Assert.NotEmpty(response.Changes[testScriptUri]); + string actual = GetModifiedScript(scriptFile.Contents, response.Changes[testScriptUri].ToArray()); Assert.Equal(expected, actual); From 0f30aca4d466bb365568735f9b262bc35a27e797 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 25 Sep 2024 20:46:16 -0700 Subject: [PATCH 173/203] Fix all Tests --- .../Refactoring/Variables/RefactorVariableTestCases.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index e8ad88fe2..6b8ae7818 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -7,9 +7,9 @@ public class RefactorVariableTestCases [ new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), - new ("VariableCommandParameter.ps1", Line: 10, Column: 9), - new ("VariableCommandParameterSplatted.ps1", Line: 16, Column: 5), - new ("VariableCommandParameterSplatted.ps1", Line: 21, Column: 11), + new ("VariableCommandParameter.ps1", Line: 10, Column: 10), + new ("VariableCommandParameterSplatted.ps1", Line: 3, Column: 19 ), + new ("VariableCommandParameterSplatted.ps1", Line: 21, Column: 12), new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1), new ("VariableDotNotationFromInnerFunction.ps1", Line: 11, Column: 26), new ("VariableInForeachDuplicateAssignment.ps1", Line: 6, Column: 18), From 6a0ca49f9efce4cf2ad6e09581d3ea3849774c48 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 08:50:52 -0700 Subject: [PATCH 174/203] Actually fix tests (missing some pattern matching on AST types) --- .../Services/TextDocument/RenameService.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 6b7c9bc58..78fcb494b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -97,9 +97,14 @@ private async Task GetScopedSettings(DocumentUri uri, Canc // TODO: Potentially future cross-file support TextEdit[] changes = tokenToRename switch { - FunctionDefinitionAst or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), - VariableExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), - // FIXME: Only throw if capability is not prepareprovider + FunctionDefinitionAst + or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), + + VariableExpressionAst + or ParameterAst + or CommandParameterAst + or AssignmentStatementAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), + _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; @@ -150,10 +155,15 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam { Ast? ast = scriptFile.ScriptAst.FindAtPosition(position, [ - // Filters just the ASTs that are candidates for rename + // Functions typeof(FunctionDefinitionAst), + typeof(CommandAst), + + // Variables typeof(VariableExpressionAst), - typeof(CommandAst) + typeof(ParameterAst), + typeof(CommandParameterAst), + typeof(AssignmentStatementAst), ]); // Only the function name is valid for rename, not other components From 35c24dd9ad7617248e8fc8f8453c5ef48cb34ff2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 08:52:22 -0700 Subject: [PATCH 175/203] Small format adjust --- .../Services/TextDocument/RenameService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 78fcb494b..c8b6533b4 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -98,12 +98,14 @@ private async Task GetScopedSettings(DocumentUri uri, Canc TextEdit[] changes = tokenToRename switch { FunctionDefinitionAst - or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), + or CommandAst + => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), VariableExpressionAst or ParameterAst or CommandParameterAst - or AssignmentStatementAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), + or AssignmentStatementAst + => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; From 31fcf32c64edfcb280db76168738576a7c19f2ac Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 09:04:59 -0700 Subject: [PATCH 176/203] Format Update --- .../PowerShell/Refactoring/IterativeVariableVisitor.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs index fa556c18a..247b588d6 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs @@ -194,7 +194,7 @@ internal static Ast GetAstParentScope(Ast node) // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match // if so this is probably a variable defined within a foreach loop else if (parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && - ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) + ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) { parent = ForEachStmnt; } @@ -252,7 +252,9 @@ internal static bool WithinTargetsScope(Ast Target, Ast Child) if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) { - }else{ + } + else + { break; } } From abadcc06a864cd351d1ca1789eb9145298981de8 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 12:09:58 -0700 Subject: [PATCH 177/203] Move IterativeVariableVisitor into RenameService class --- .../Refactoring/IterativeVariableVisitor.cs | 523 ------------------ 1 file changed, 523 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs deleted file mode 100644 index 247b588d6..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/IterativeVariableVisitor.cs +++ /dev/null @@ -1,523 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation.Language; -using System.Linq; -using System; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using Microsoft.PowerShell.EditorServices.Services; - -namespace Microsoft.PowerShell.EditorServices.Refactoring -{ - - internal class IterativeVariableRename - { - private readonly string OldName; - private readonly string NewName; - internal bool ShouldRename; - public List Modifications = []; - internal int StartLineNumber; - internal int StartColumnNumber; - internal VariableExpressionAst TargetVariableAst; - internal readonly Ast ScriptAst; - internal bool isParam; - internal bool AliasSet; - internal FunctionDefinitionAst TargetFunction; - internal bool CreateAlias; - - public IterativeVariableRename(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateAlias) - { - this.NewName = NewName; - this.StartLineNumber = StartLineNumber; - this.StartColumnNumber = StartColumnNumber; - this.ScriptAst = ScriptAst; - this.CreateAlias = CreateAlias; - - VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); - if (Node != null) - { - if (Node.Parent is ParameterAst) - { - isParam = true; - Ast parent = Node; - // Look for a target function that the parameterAst will be within if it exists - parent = Utilities.GetAstParentOfType(parent, typeof(FunctionDefinitionAst)); - if (parent != null) - { - TargetFunction = (FunctionDefinitionAst)parent; - } - } - TargetVariableAst = Node; - OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); - this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; - this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; - } - } - - public static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - - // Look up the target object - Ast node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, - ScriptAst, typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); - - string name = node switch - { - CommandParameterAst commdef => commdef.ParameterName, - VariableExpressionAst varDef => varDef.VariablePath.UserPath, - // Key within a Hashtable - StringConstantExpressionAst strExp => strExp.Value, - _ => throw new TargetSymbolNotFoundException() - }; - - VariableExpressionAst splatAssignment = null; - // A rename of a parameter has been initiated from a splat - if (node is StringConstantExpressionAst) - { - Ast parent = node; - parent = Utilities.GetAstParentOfType(parent, typeof(AssignmentStatementAst)); - if (parent is not null and AssignmentStatementAst assignmentStatementAst) - { - splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( - ast => ast is VariableExpressionAst, false); - } - } - - Ast TargetParent = GetAstParentScope(node); - - // Is the Variable sitting within a ParameterBlockAst that is within a Function Definition - // If so we don't need to look further as this is most likley the AssignmentStatement we are looking for - Ast paramParent = Utilities.GetAstParentOfType(node, typeof(ParamBlockAst)); - if (TargetParent is FunctionDefinitionAst && null != paramParent) - { - return node; - } - - // Find all variables and parameter assignments with the same name before - // The node found above - List VariableAssignments = ScriptAst.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && - // Look Backwards from the node above - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the def if we have no matches - if (VariableAssignments.Count == 0) - { - return node; - } - Ast CorrectDefinition = null; - for (int i = VariableAssignments.Count - 1; i >= 0; i--) - { - VariableExpressionAst element = VariableAssignments[i]; - - Ast parent = GetAstParentScope(element); - // closest assignment statement is within the scope of the node - if (TargetParent == parent) - { - CorrectDefinition = element; - break; - } - else if (node.Parent is AssignmentStatementAst) - { - // the node is probably the first assignment statement within the scope - CorrectDefinition = node; - break; - } - // node is proably just a reference to an assignment statement or Parameter within the global scope or higher - if (node.Parent is not AssignmentStatementAst) - { - if (null == parent || null == parent.Parent) - { - // we have hit the global scope of the script file - CorrectDefinition = element; - break; - } - - if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst or StringConstantExpressionAst) - { - if (node is StringConstantExpressionAst) - { - List SplatReferences = ScriptAst.FindAll(ast => - { - return ast is VariableExpressionAst varDef && - varDef.Splatted && - varDef.Parent is CommandAst && - varDef.VariablePath.UserPath.ToLower() == splatAssignment.VariablePath.UserPath.ToLower(); - }, true).Cast().ToList(); - - if (SplatReferences.Count >= 1) - { - CommandAst splatFirstRefComm = (CommandAst)SplatReferences.First().Parent; - if (funcDef.Name == splatFirstRefComm.GetCommandName() - && funcDef.Parent.Parent == TargetParent) - { - CorrectDefinition = element; - break; - } - } - } - - if (node.Parent is CommandAst commDef) - { - if (funcDef.Name == commDef.GetCommandName() - && funcDef.Parent.Parent == TargetParent) - { - CorrectDefinition = element; - break; - } - } - } - if (WithinTargetsScope(element, node)) - { - CorrectDefinition = element; - } - } - } - return CorrectDefinition ?? node; - } - - internal static Ast GetAstParentScope(Ast node) - { - Ast parent = node; - // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst), typeof(ForStatementAst)); - if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) - { - parent = parent.Parent; - } - // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match - // if so this is probably a variable defined within a foreach loop - else if (parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && - ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) - { - parent = ForEachStmnt; - } - // Check if the parent of the VariableExpressionAst is a ForStatementAst then check if the variable names match - // if so this is probably a variable defined within a foreach loop - else if (parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && - ForStmnt.Initializer is AssignmentStatementAst AssignStmnt && AssignStmnt.Left is VariableExpressionAst VarExpStmnt && - VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath) - { - parent = ForStmnt; - } - - return parent; - } - - internal static bool IsVariableExpressionAssignedInTargetScope(VariableExpressionAst node, Ast scope) - { - bool r = false; - - List VariableAssignments = node.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && - // Look Backwards from the node above - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && - // Must be within the the designated scope - VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; - }, true).Cast().ToList(); - - if (VariableAssignments.Count > 0) - { - r = true; - } - // Node is probably the first Assignment Statement within scope - if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) - { - r = true; - } - - return r; - } - - internal static bool WithinTargetsScope(Ast Target, Ast Child) - { - bool r = false; - Ast childParent = Child.Parent; - Ast TargetScope = GetAstParentScope(Target); - while (childParent != null) - { - if (childParent is FunctionDefinitionAst FuncDefAst) - { - if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) - { - - } - else - { - break; - } - } - if (childParent == TargetScope) - { - break; - } - childParent = childParent.Parent; - } - if (childParent == TargetScope) - { - r = true; - } - return r; - } - - public class NodeProcessingState - { - public Ast Node { get; set; } - public IEnumerator ChildrenEnumerator { get; set; } - } - - public void Visit(Ast root) - { - Stack processingStack = new(); - - processingStack.Push(new NodeProcessingState { Node = root }); - - while (processingStack.Count > 0) - { - NodeProcessingState currentState = processingStack.Peek(); - - if (currentState.ChildrenEnumerator == null) - { - // First time processing this node. Do the initial processing. - ProcessNode(currentState.Node); // This line is crucial. - - // Get the children and set up the enumerator. - IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); - currentState.ChildrenEnumerator = children.GetEnumerator(); - } - - // Process the next child. - if (currentState.ChildrenEnumerator.MoveNext()) - { - Ast child = currentState.ChildrenEnumerator.Current; - processingStack.Push(new NodeProcessingState { Node = child }); - } - else - { - // All children have been processed, we're done with this node. - processingStack.Pop(); - } - } - } - - public void ProcessNode(Ast node) - { - - switch (node) - { - case CommandAst commandAst: - ProcessCommandAst(commandAst); - break; - case CommandParameterAst commandParameterAst: - ProcessCommandParameterAst(commandParameterAst); - break; - case VariableExpressionAst variableExpressionAst: - ProcessVariableExpressionAst(variableExpressionAst); - break; - } - } - - private void ProcessCommandAst(CommandAst commandAst) - { - // Is the Target Variable a Parameter and is this commandAst the target function - if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) - { - // Check to see if this is a splatted call to the target function. - Ast Splatted = null; - foreach (Ast element in commandAst.CommandElements) - { - if (element is VariableExpressionAst varAst && varAst.Splatted) - { - Splatted = varAst; - break; - } - } - if (Splatted != null) - { - NewSplattedModification(Splatted); - } - else - { - // The Target Variable is a Parameter and the commandAst is the Target Function - ShouldRename = true; - } - } - } - - private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressionAst) - { - if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) - { - // Is this the Target Variable - if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && - variableExpressionAst.Extent.StartLineNumber == StartLineNumber) - { - ShouldRename = true; - TargetVariableAst = variableExpressionAst; - } - // Is this a Command Ast within scope - else if (variableExpressionAst.Parent is CommandAst commandAst) - { - if (WithinTargetsScope(TargetVariableAst, commandAst)) - { - ShouldRename = true; - } - // The TargetVariable is defined within a function - // This commandAst is not within that function's scope so we should not rename - if (GetAstParentScope(TargetVariableAst) is FunctionDefinitionAst && !WithinTargetsScope(TargetVariableAst, commandAst)) - { - ShouldRename = false; - } - - } - // Is this a Variable Assignment thats not within scope - else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && - assignment.Operator == TokenKind.Equals) - { - if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) - { - ShouldRename = false; - } - - } - // Else is the variable within scope - else - { - ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); - } - if (ShouldRename) - { - // have some modifications to account for the dollar sign prefix powershell uses for variables - TextEdit Change = new() - { - NewText = NewName.Contains("$") ? NewName : "$" + NewName, - Range = new ScriptExtentAdapter(variableExpressionAst.Extent), - }; - // If the variables parent is a parameterAst Add a modification - if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - CreateAlias) - { - TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); - Modifications.Add(aliasChange); - AliasSet = true; - } - Modifications.Add(Change); - - } - } - } - - private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) - { - if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) - { - if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && - commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) - { - ShouldRename = true; - } - - if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && - commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) - { - TextEdit Change = new() - { - NewText = NewName.Contains("-") ? NewName : "-" + NewName, - Range = new ScriptExtentAdapter(commandParameterAst.Extent) - }; - Modifications.Add(Change); - } - else - { - ShouldRename = false; - } - } - } - - internal void NewSplattedModification(Ast Splatted) - { - // This Function should be passed a splatted VariableExpressionAst which - // is used by a CommandAst that is the TargetFunction. - - // Find the splats top assignment / definition - Ast SplatAssignment = GetVariableTopAssignment( - Splatted.Extent.StartLineNumber, - Splatted.Extent.StartColumnNumber, - ScriptAst); - // Look for the Parameter within the Splats HashTable - if (SplatAssignment.Parent is AssignmentStatementAst assignmentStatementAst && - assignmentStatementAst.Right is CommandExpressionAst commExpAst && - commExpAst.Expression is HashtableAst hashTableAst) - { - foreach (Tuple element in hashTableAst.KeyValuePairs) - { - if (element.Item1 is StringConstantExpressionAst strConstAst && - strConstAst.Value.ToLower() == OldName.ToLower()) - { - TextEdit Change = new() - { - NewText = NewName, - Range = new ScriptExtentAdapter(strConstAst.Extent) - }; - - Modifications.Add(Change); - break; - } - - } - } - } - - internal TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) - { - // Check if an Alias AttributeAst already exists and append the new Alias to the existing list - // Otherwise Create a new Alias Attribute - // Add the modifications to the changes - // The Attribute will be appended before the variable or in the existing location of the original alias - TextEdit aliasChange = new(); - // FIXME: Understand this more, if this returns more than one result, why does it overwrite the aliasChange? - foreach (Ast Attr in paramAst.Attributes) - { - if (Attr is AttributeAst AttrAst) - { - // Alias Already Exists - if (AttrAst.TypeName.FullName == "Alias") - { - string existingEntries = AttrAst.Extent.Text - .Substring("[Alias(".Length); - existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); - string nentries = existingEntries + $", \"{OldName}\""; - - aliasChange = aliasChange with - { - NewText = $"[Alias({nentries})]", - Range = new ScriptExtentAdapter(AttrAst.Extent) - }; - } - } - } - if (aliasChange.NewText == null) - { - aliasChange = aliasChange with - { - NewText = $"[Alias(\"{OldName}\")]", - Range = new ScriptExtentAdapter(paramAst.Extent) - }; - } - - return aliasChange; - } - - } -} From 37538b630cfe08af7b5ef738527763e98cb2c6bd Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 12:27:32 -0700 Subject: [PATCH 178/203] Remove unnecessary intermediate tests --- .../Refactoring/RefactorUtilitiesTests.cs | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs deleted file mode 100644 index fea824839..000000000 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilitiesTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// FIXME: Fix these tests (if it is even worth doing so) -// using System.IO; -// using System.Threading.Tasks; -// using Microsoft.Extensions.Logging.Abstractions; -// using Microsoft.PowerShell.EditorServices.Services; -// using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -// using Microsoft.PowerShell.EditorServices.Services.TextDocument; -// using Microsoft.PowerShell.EditorServices.Test; -// using Microsoft.PowerShell.EditorServices.Test.Shared; -// using Xunit; -// using System.Management.Automation.Language; -// using Microsoft.PowerShell.EditorServices.Refactoring; - -// namespace PowerShellEditorServices.Test.Refactoring -// { -// [Trait("Category", "RefactorUtilities")] -// public class RefactorUtilitiesTests : IAsyncLifetime -// { -// private PsesInternalHost psesHost; -// private WorkspaceService workspace; - -// public async Task InitializeAsync() -// { -// psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance); -// workspace = new WorkspaceService(NullLoggerFactory.Instance); -// } - -// public async Task DisposeAsync() => await Task.Run(psesHost.StopAsync); -// private ScriptFile GetTestScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Refactoring", "Utilities", fileName))); - - -// public class GetAstShouldDetectTestData : TheoryData -// { -// public GetAstShouldDetectTestData() -// { -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionAst), 15, 1); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableExpressionStartAst), 15, 1); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinParameterAst), 3, 17); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetHashTableKey), 16, 5); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetVariableWithinCommandAst), 6, 28); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetCommandParameterAst), 21, 10); -// Add(new RenameSymbolParamsSerialized(RenameUtilitiesData.GetFunctionDefinitionAst), 1, 1); -// } -// } - -// [Theory] -// [ClassData(typeof(GetAstShouldDetectTestData))] -// public void GetAstShouldDetect(RenameSymbolParamsSerialized s, int l, int c) -// { -// ScriptFile scriptFile = GetTestScript(s.FileName); -// Ast symbol = Utilities.GetAst(s.Line, s.Column, scriptFile.ScriptAst); -// // Assert the Line and Column is what is expected -// Assert.Equal(l, symbol.Extent.StartLineNumber); -// Assert.Equal(c, symbol.Extent.StartColumnNumber); -// } - -// [Fact] -// public void GetVariableUnderFunctionDef() -// { -// RenameSymbolParams request = new() -// { -// Column = 5, -// Line = 2, -// RenameTo = "Renamed", -// FileName = "TestDetectionUnderFunctionDef.ps1" -// }; -// ScriptFile scriptFile = GetTestScript(request.FileName); - -// Ast symbol = Utilities.GetAst(request.Line, request.Column, scriptFile.ScriptAst); -// Assert.IsType(symbol); -// Assert.Equal(2, symbol.Extent.StartLineNumber); -// Assert.Equal(5, symbol.Extent.StartColumnNumber); - -// } -// [Fact] -// public void AssertContainsDotSourcingTrue() -// { -// ScriptFile scriptFile = GetTestScript("TestDotSourcingTrue.ps1"); -// Assert.True(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); -// } -// [Fact] -// public void AssertContainsDotSourcingFalse() -// { -// ScriptFile scriptFile = GetTestScript("TestDotSourcingFalse.ps1"); -// Assert.False(Utilities.AssertContainsDotSourced(scriptFile.ScriptAst)); -// } -// } -// } From 9467aa6a9582b084a3e904335fa12a0fb6ccf2f6 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 12:54:45 -0700 Subject: [PATCH 179/203] Internalize rename visitor into renameservice --- .../Services/TextDocument/RenameService.cs | 560 +++++++++++++++++- 1 file changed, 539 insertions(+), 21 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index c8b6533b4..4826839e1 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -29,17 +29,17 @@ public class RenameServiceOptions public bool acceptDisclaimer { get; set; } } -public interface IRenameService +internal interface IRenameService { /// /// Implementation of textDocument/prepareRename /// - public Task PrepareRenameSymbol(PrepareRenameParams prepareRenameParams, CancellationToken cancellationToken); + internal Task PrepareRenameSymbol(PrepareRenameParams prepareRenameParams, CancellationToken cancellationToken); /// /// Implementation of textDocument/rename /// - public Task RenameSymbol(RenameParams renameParams, CancellationToken cancellationToken); + internal Task RenameSymbol(RenameParams renameParams, CancellationToken cancellationToken); } /// @@ -55,12 +55,6 @@ internal class RenameService( ) : IRenameService { - private async Task GetScopedSettings(DocumentUri uri, CancellationToken cancellationToken = default) - { - IScopedConfiguration scopedConfig = await config.GetScopedConfiguration(uri, cancellationToken).ConfigureAwait(false); - return scopedConfig.GetSection(configSection).Get() ?? new RenameServiceOptions(); - } - public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { RenameParams renameRequest = new() @@ -125,7 +119,7 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam { if (target is not (FunctionDefinitionAst or CommandAst)) { - throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This should not happen as PrepareRename should have already checked for viability. File an issue if you see this."); + throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This is a bug, file an issue if you see this."); } RenameFunctionVisitor visitor = new(target, renameParams.NewName); @@ -134,19 +128,20 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createAlias) { - if (symbol is VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst) + if (symbol is not (VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst)) { - IterativeVariableRename visitor = new( - requestParams.NewName, - symbol.Extent.StartLineNumber, - symbol.Extent.StartColumnNumber, - scriptAst, - createAlias - ); - visitor.Visit(scriptAst); - return visitor.Modifications.ToArray(); + throw new HandlerErrorException($"Asked to rename a variable but the target is not a viable variable type: {symbol.GetType()}. This is a bug, file an issue if you see this."); } - return []; + + RenameVariableVisitor visitor = new( + requestParams.NewName, + symbol.Extent.StartLineNumber, + symbol.Extent.StartColumnNumber, + scriptAst, + createAlias + ); + return visitor.VisitAndGetEdits(); + } /// @@ -280,6 +275,12 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can throw new InvalidOperationException("Unknown Disclaimer Response received. This is a bug and you should report it."); } + + private async Task GetScopedSettings(DocumentUri uri, CancellationToken cancellationToken = default) + { + IScopedConfiguration scopedConfig = await config.GetScopedConfiguration(uri, cancellationToken).ConfigureAwait(false); + return scopedConfig.GetSection(configSection).Get() ?? new RenameServiceOptions(); + } } /// @@ -397,6 +398,523 @@ internal TextEdit[] VisitAndGetEdits(Ast ast) } } +#nullable disable +internal class RenameVariableVisitor : AstVisitor +{ + private readonly string OldName; + private readonly string NewName; + internal bool ShouldRename; + internal List Edits = []; + internal int StartLineNumber; + internal int StartColumnNumber; + internal VariableExpressionAst TargetVariableAst; + internal readonly Ast ScriptAst; + internal bool isParam; + internal bool AliasSet; + internal FunctionDefinitionAst TargetFunction; + internal bool CreateAlias; + + public RenameVariableVisitor(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateAlias) + { + this.NewName = NewName; + this.StartLineNumber = StartLineNumber; + this.StartColumnNumber = StartColumnNumber; + this.ScriptAst = ScriptAst; + this.CreateAlias = CreateAlias; + + VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); + if (Node != null) + { + if (Node.Parent is ParameterAst) + { + isParam = true; + Ast parent = Node; + // Look for a target function that the parameterAst will be within if it exists + parent = Utilities.GetAstParentOfType(parent, typeof(FunctionDefinitionAst)); + if (parent != null) + { + TargetFunction = (FunctionDefinitionAst)parent; + } + } + TargetVariableAst = Node; + OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); + this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; + this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; + } + } + + private static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) + { + + // Look up the target object + Ast node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, + ScriptAst, typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); + + string name = node switch + { + CommandParameterAst commdef => commdef.ParameterName, + VariableExpressionAst varDef => varDef.VariablePath.UserPath, + // Key within a Hashtable + StringConstantExpressionAst strExp => strExp.Value, + _ => throw new TargetSymbolNotFoundException() + }; + + VariableExpressionAst splatAssignment = null; + // A rename of a parameter has been initiated from a splat + if (node is StringConstantExpressionAst) + { + Ast parent = node; + parent = Utilities.GetAstParentOfType(parent, typeof(AssignmentStatementAst)); + if (parent is not null and AssignmentStatementAst assignmentStatementAst) + { + splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( + ast => ast is VariableExpressionAst, false); + } + } + + Ast TargetParent = GetAstParentScope(node); + + // Is the Variable sitting within a ParameterBlockAst that is within a Function Definition + // If so we don't need to look further as this is most likley the AssignmentStatement we are looking for + Ast paramParent = Utilities.GetAstParentOfType(node, typeof(ParamBlockAst)); + if (TargetParent is FunctionDefinitionAst && null != paramParent) + { + return node; + } + + // Find all variables and parameter assignments with the same name before + // The node found above + List VariableAssignments = ScriptAst.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && + // Look Backwards from the node above + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); + }, true).Cast().ToList(); + // return the def if we have no matches + if (VariableAssignments.Count == 0) + { + return node; + } + Ast CorrectDefinition = null; + for (int i = VariableAssignments.Count - 1; i >= 0; i--) + { + VariableExpressionAst element = VariableAssignments[i]; + + Ast parent = GetAstParentScope(element); + // closest assignment statement is within the scope of the node + if (TargetParent == parent) + { + CorrectDefinition = element; + break; + } + else if (node.Parent is AssignmentStatementAst) + { + // the node is probably the first assignment statement within the scope + CorrectDefinition = node; + break; + } + // node is proably just a reference to an assignment statement or Parameter within the global scope or higher + if (node.Parent is not AssignmentStatementAst) + { + if (null == parent || null == parent.Parent) + { + // we have hit the global scope of the script file + CorrectDefinition = element; + break; + } + + if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst or StringConstantExpressionAst) + { + if (node is StringConstantExpressionAst) + { + List SplatReferences = ScriptAst.FindAll(ast => + { + return ast is VariableExpressionAst varDef && + varDef.Splatted && + varDef.Parent is CommandAst && + varDef.VariablePath.UserPath.ToLower() == splatAssignment.VariablePath.UserPath.ToLower(); + }, true).Cast().ToList(); + + if (SplatReferences.Count >= 1) + { + CommandAst splatFirstRefComm = (CommandAst)SplatReferences.First().Parent; + if (funcDef.Name == splatFirstRefComm.GetCommandName() + && funcDef.Parent.Parent == TargetParent) + { + CorrectDefinition = element; + break; + } + } + } + + if (node.Parent is CommandAst commDef) + { + if (funcDef.Name == commDef.GetCommandName() + && funcDef.Parent.Parent == TargetParent) + { + CorrectDefinition = element; + break; + } + } + } + if (WithinTargetsScope(element, node)) + { + CorrectDefinition = element; + } + } + } + return CorrectDefinition ?? node; + } + + private static Ast GetAstParentScope(Ast node) + { + Ast parent = node; + // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition + parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst), typeof(ForStatementAst)); + if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) + { + parent = parent.Parent; + } + // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match + // if so this is probably a variable defined within a foreach loop + else if (parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && + ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) + { + parent = ForEachStmnt; + } + // Check if the parent of the VariableExpressionAst is a ForStatementAst then check if the variable names match + // if so this is probably a variable defined within a foreach loop + else if (parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && + ForStmnt.Initializer is AssignmentStatementAst AssignStmnt && AssignStmnt.Left is VariableExpressionAst VarExpStmnt && + VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath) + { + parent = ForStmnt; + } + + return parent; + } + + private static bool IsVariableExpressionAssignedInTargetScope(VariableExpressionAst node, Ast scope) + { + bool r = false; + + List VariableAssignments = node.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && + // Look Backwards from the node above + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && + // Must be within the the designated scope + VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; + }, true).Cast().ToList(); + + if (VariableAssignments.Count > 0) + { + r = true; + } + // Node is probably the first Assignment Statement within scope + if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) + { + r = true; + } + + return r; + } + + private static bool WithinTargetsScope(Ast Target, Ast Child) + { + bool r = false; + Ast childParent = Child.Parent; + Ast TargetScope = GetAstParentScope(Target); + while (childParent != null) + { + if (childParent is FunctionDefinitionAst FuncDefAst) + { + if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) + { + + } + else + { + break; + } + } + if (childParent == TargetScope) + { + break; + } + childParent = childParent.Parent; + } + if (childParent == TargetScope) + { + r = true; + } + return r; + } + + private class NodeProcessingState + { + public Ast Node { get; set; } + public IEnumerator ChildrenEnumerator { get; set; } + } + + internal void Visit(Ast root) + { + Stack processingStack = new(); + + processingStack.Push(new NodeProcessingState { Node = root }); + + while (processingStack.Count > 0) + { + NodeProcessingState currentState = processingStack.Peek(); + + if (currentState.ChildrenEnumerator == null) + { + // First time processing this node. Do the initial processing. + ProcessNode(currentState.Node); // This line is crucial. + + // Get the children and set up the enumerator. + IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); + currentState.ChildrenEnumerator = children.GetEnumerator(); + } + + // Process the next child. + if (currentState.ChildrenEnumerator.MoveNext()) + { + Ast child = currentState.ChildrenEnumerator.Current; + processingStack.Push(new NodeProcessingState { Node = child }); + } + else + { + // All children have been processed, we're done with this node. + processingStack.Pop(); + } + } + } + + private void ProcessNode(Ast node) + { + + switch (node) + { + case CommandAst commandAst: + ProcessCommandAst(commandAst); + break; + case CommandParameterAst commandParameterAst: + ProcessCommandParameterAst(commandParameterAst); + break; + case VariableExpressionAst variableExpressionAst: + ProcessVariableExpressionAst(variableExpressionAst); + break; + } + } + + private void ProcessCommandAst(CommandAst commandAst) + { + // Is the Target Variable a Parameter and is this commandAst the target function + if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) + { + // Check to see if this is a splatted call to the target function. + Ast Splatted = null; + foreach (Ast element in commandAst.CommandElements) + { + if (element is VariableExpressionAst varAst && varAst.Splatted) + { + Splatted = varAst; + break; + } + } + if (Splatted != null) + { + NewSplattedModification(Splatted); + } + else + { + // The Target Variable is a Parameter and the commandAst is the Target Function + ShouldRename = true; + } + } + } + + private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressionAst) + { + if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) + { + // Is this the Target Variable + if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && + variableExpressionAst.Extent.StartLineNumber == StartLineNumber) + { + ShouldRename = true; + TargetVariableAst = variableExpressionAst; + } + // Is this a Command Ast within scope + else if (variableExpressionAst.Parent is CommandAst commandAst) + { + if (WithinTargetsScope(TargetVariableAst, commandAst)) + { + ShouldRename = true; + } + // The TargetVariable is defined within a function + // This commandAst is not within that function's scope so we should not rename + if (GetAstParentScope(TargetVariableAst) is FunctionDefinitionAst && !WithinTargetsScope(TargetVariableAst, commandAst)) + { + ShouldRename = false; + } + + } + // Is this a Variable Assignment thats not within scope + else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && + assignment.Operator == TokenKind.Equals) + { + if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) + { + ShouldRename = false; + } + + } + // Else is the variable within scope + else + { + ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); + } + if (ShouldRename) + { + // have some modifications to account for the dollar sign prefix powershell uses for variables + TextEdit Change = new() + { + NewText = NewName.Contains("$") ? NewName : "$" + NewName, + Range = new ScriptExtentAdapter(variableExpressionAst.Extent), + }; + // If the variables parent is a parameterAst Add a modification + if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && + CreateAlias) + { + TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); + Edits.Add(aliasChange); + AliasSet = true; + } + Edits.Add(Change); + + } + } + } + + private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) + { + if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) + { + if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && + commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) + { + ShouldRename = true; + } + + if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && + commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) + { + TextEdit Change = new() + { + NewText = NewName.Contains("-") ? NewName : "-" + NewName, + Range = new ScriptExtentAdapter(commandParameterAst.Extent) + }; + Edits.Add(Change); + } + else + { + ShouldRename = false; + } + } + } + + private void NewSplattedModification(Ast Splatted) + { + // This Function should be passed a splatted VariableExpressionAst which + // is used by a CommandAst that is the TargetFunction. + + // Find the splats top assignment / definition + Ast SplatAssignment = GetVariableTopAssignment( + Splatted.Extent.StartLineNumber, + Splatted.Extent.StartColumnNumber, + ScriptAst); + // Look for the Parameter within the Splats HashTable + if (SplatAssignment.Parent is AssignmentStatementAst assignmentStatementAst && + assignmentStatementAst.Right is CommandExpressionAst commExpAst && + commExpAst.Expression is HashtableAst hashTableAst) + { + foreach (Tuple element in hashTableAst.KeyValuePairs) + { + if (element.Item1 is StringConstantExpressionAst strConstAst && + strConstAst.Value.ToLower() == OldName.ToLower()) + { + TextEdit Change = new() + { + NewText = NewName, + Range = new ScriptExtentAdapter(strConstAst.Extent) + }; + + Edits.Add(Change); + break; + } + + } + } + } + + private TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) + { + // Check if an Alias AttributeAst already exists and append the new Alias to the existing list + // Otherwise Create a new Alias Attribute + // Add the modifications to the changes + // The Attribute will be appended before the variable or in the existing location of the original alias + TextEdit aliasChange = new(); + // FIXME: Understand this more, if this returns more than one result, why does it overwrite the aliasChange? + foreach (Ast Attr in paramAst.Attributes) + { + if (Attr is AttributeAst AttrAst) + { + // Alias Already Exists + if (AttrAst.TypeName.FullName == "Alias") + { + string existingEntries = AttrAst.Extent.Text + .Substring("[Alias(".Length); + existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); + string nentries = existingEntries + $", \"{OldName}\""; + + aliasChange = aliasChange with + { + NewText = $"[Alias({nentries})]", + Range = new ScriptExtentAdapter(AttrAst.Extent) + }; + } + } + } + if (aliasChange.NewText == null) + { + aliasChange = aliasChange with + { + NewText = $"[Alias(\"{OldName}\")]", + Range = new ScriptExtentAdapter(paramAst.Extent) + }; + } + + return aliasChange; + } + + internal TextEdit[] VisitAndGetEdits() + { + Visit(ScriptAst); + return Edits.ToArray(); + } +} +#nullable enable + internal class Utilities { public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) From 90cf02ea8c56285149167439968c17075687cfbb Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 15:18:22 -0700 Subject: [PATCH 180/203] Perform initial abstraction of visitors to a base class --- .../Services/TextDocument/RenameService.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 4826839e1..d256fb754 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -283,14 +283,18 @@ private async Task GetScopedSettings(DocumentUri uri, Canc } } +internal abstract class RenameVisitorBase() : AstVisitor +{ + internal List Edits { get; } = new(); +} + /// /// A visitor that generates a list of TextEdits to a TextDocument to rename a PowerShell function /// You should use a new instance for each rename operation. /// Skipverify can be used as a performance optimization when you are sure you are in scope. /// -internal class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : AstVisitor +internal class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : RenameVisitorBase { - internal List Edits { get; } = new(); private Ast? CurrentDocument; private FunctionDefinitionAst? FunctionToRename; @@ -399,12 +403,11 @@ internal TextEdit[] VisitAndGetEdits(Ast ast) } #nullable disable -internal class RenameVariableVisitor : AstVisitor +internal class RenameVariableVisitor : RenameVisitorBase { private readonly string OldName; private readonly string NewName; internal bool ShouldRename; - internal List Edits = []; internal int StartLineNumber; internal int StartColumnNumber; internal VariableExpressionAst TargetVariableAst; From 1af2a874fbe66db13f50ccdc5c14f0b1e87b6534 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 16:09:21 -0700 Subject: [PATCH 181/203] Add disclaimer link --- README.md | 20 +++++++++++++++++++ .../Services/TextDocument/RenameService.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 48341ab68..9e890545f 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,26 @@ The debugging functionality in PowerShell Editor Services is available in the fo - [powershell.nvim for Neovim](https://github.com/TheLeoP/powershell.nvim) - [intellij-powershell](https://github.com/ant-druha/intellij-powershell) +### Rename Disclaimer + +PowerShell is not a statically typed language. As such, the renaming of functions, parameters, and other symbols can only be done on a best effort basis. While this is sufficient for the majority of use cases, it cannot be relied upon to find all instances of a symbol and rename them across an entire code base such as in C# or TypeScript. + +There are several edge case scenarios which may exist where rename is difficult or impossible, or unable to be determined due to the dynamic scoping nature of PowerShell. + +The focus of the rename support is on quick updates to variables or functions within a self-contained script file. It is not intended for module developers to find and rename a symbol across multiple files, which is very difficult to do as the relationships are primarily only computed at runtime and not possible to be statically analyzed. + +🤚🤚 Unsupported Scenarios + +❌ Renaming can only be done within a single file. Renaming symbols across multiple files is not supported. +❌ Files containing dotsourcing are currently not supported. +❌ Functions or variables must have a corresponding definition within their scope to be renamed. If we cannot find the original definition of a variable or function, the rename will not be supported. + +👍👍 [Implemented and Tested Rename Scenarios](https://github.com/PowerShell/PowerShellEditorServices/blob/main/test/PowerShellEditorServices.Test.Shared/Refactoring) + +📄📄 Filing a Rename Issue + +If there is a rename scenario you feel can be reasonably supported in PowerShell, please file a bug report in the PowerShellEditorServices repository with the "Expected" and "Actual" being the before and after rename. We will evaluate it and accept or reject it and give reasons why. Items that fall under the Unsupported Scenarios above will be summarily rejected, however that does not mean that they may not be supported in the future if we come up with a reasonably safe way to implement a scenario. + ## API Usage Please note that we only consider the following as stable APIs that can be relied on: diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index d256fb754..e7f0b64fb 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -215,7 +215,7 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can if (acceptDisclaimerOption || disclaimerAcceptedForSession) { return true; } // TODO: Localization - const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. Please review the notice and understand the limitations and risks."; + const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. [Please review the notice](https://github.com/PowerShell/PowerShellEditorServices?tab=readme-ov-file#rename-disclaimer) and accept the limitations and risks."; const string acceptAnswer = "I Accept"; // const string acceptWorkspaceAnswer = "I Accept [Workspace]"; // const string acceptSessionAnswer = "I Accept [Session]"; From 8575c41fb1f31ebbb5b815a933994fcdd907ccbf Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 16:15:09 -0700 Subject: [PATCH 182/203] Apply formatting to test fixtures --- .../Variables/VariableCommandParameter.ps1 | 2 +- .../VariableCommandParameterRenamed.ps1 | 2 +- .../VariableCommandParameterSplatted.ps1 | 16 ++++++++-------- .../VariableCommandParameterSplattedRenamed.ps1 | 16 ++++++++-------- .../Refactoring/Variables/VariableInParam.ps1 | 6 +++--- .../Variables/VariableInParamRenamed.ps1 | 6 +++--- .../Variables/VariableInScriptblock.ps1 | 2 +- .../Variables/VariableInScriptblockRenamed.ps1 | 2 +- .../Variables/VariableInScriptblockScoped.ps1 | 4 ++-- .../VariableInScriptblockScopedRenamed.ps1 | 4 ++-- .../VariableNestedFunctionScriptblock.ps1 | 4 ++-- .../VariableNestedFunctionScriptblockRenamed.ps1 | 4 ++-- .../Refactoring/Variables/VariableNonParam.ps1 | 8 ++++---- .../Variables/VariableNonParamRenamed.ps1 | 8 ++++---- .../Variables/VariableScriptWithParamBlock.ps1 | 4 ++-- .../VariableScriptWithParamBlockRenamed.ps1 | 4 ++-- .../VariableSimpleFunctionParameter.ps1 | 6 +++--- .../VariableSimpleFunctionParameterRenamed.ps1 | 6 +++--- 18 files changed, 52 insertions(+), 52 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 index 18eeb1e03..49ca3a191 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameter.ps1 @@ -7,4 +7,4 @@ function Get-foo { return $string[$pos] } -Get-foo -string "Hello" -pos -1 +Get-foo -string 'Hello' -pos -1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 index e74504a4d..a3cd4fed5 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterRenamed.ps1 @@ -7,4 +7,4 @@ function Get-foo { return $Renamed[$pos] } -Get-foo -Renamed "Hello" -pos -1 +Get-foo -Renamed 'Hello' -pos -1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 index d12a8652f..79dc6e7ee 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplatted.ps1 @@ -3,19 +3,19 @@ function New-User { [string]$Username, [string]$password ) - write-host $username + $password + Write-Host $username + $password - $splat= @{ - Username = "JohnDeer" - Password = "SomePassword" + $splat = @{ + Username = 'JohnDeer' + Password = 'SomePassword' } New-User @splat } -$UserDetailsSplat= @{ - Username = "JohnDoe" - Password = "SomePassword" +$UserDetailsSplat = @{ + Username = 'JohnDoe' + Password = 'SomePassword' } New-User @UserDetailsSplat -New-User -Username "JohnDoe" -Password "SomePassword" +New-User -Username 'JohnDoe' -Password 'SomePassword' diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 index f89b69118..176f51023 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableCommandParameterSplattedRenamed.ps1 @@ -3,19 +3,19 @@ function New-User { [string]$Renamed, [string]$password ) - write-host $Renamed + $password + Write-Host $Renamed + $password - $splat= @{ - Renamed = "JohnDeer" - Password = "SomePassword" + $splat = @{ + Renamed = 'JohnDeer' + Password = 'SomePassword' } New-User @splat } -$UserDetailsSplat= @{ - Renamed = "JohnDoe" - Password = "SomePassword" +$UserDetailsSplat = @{ + Renamed = 'JohnDoe' + Password = 'SomePassword' } New-User @UserDetailsSplat -New-User -Renamed "JohnDoe" -Password "SomePassword" +New-User -Renamed 'JohnDoe' -Password 'SomePassword' diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 index 478990bfd..436c6fbc8 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParam.ps1 @@ -1,4 +1,4 @@ -param([int]$Count=50, [int]$DelayMilliseconds=200) +param([int]$Count = 50, [int]$DelayMilliseconds = 200) function Write-Item($itemCount) { $i = 1 @@ -20,9 +20,9 @@ function Write-Item($itemCount) { # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. function Do-Work($workCount) { - Write-Output "Doing work..." + Write-Output 'Doing work...' Write-Item $workcount - Write-Host "Done!" + Write-Host 'Done!' } Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 index 2a810e887..8127b6ced 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInParamRenamed.ps1 @@ -1,4 +1,4 @@ -param([int]$Count=50, [int]$DelayMilliseconds=200) +param([int]$Count = 50, [int]$DelayMilliseconds = 200) function Write-Item($itemCount) { $i = 1 @@ -20,9 +20,9 @@ function Write-Item($itemCount) { # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. function Do-Work($Renamed) { - Write-Output "Doing work..." + Write-Output 'Doing work...' Write-Item $Renamed - Write-Host "Done!" + Write-Host 'Done!' } Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 index 9c6609aa2..3ddce4ece 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblock.ps1 @@ -1,3 +1,3 @@ -$var = "Hello" +$var = 'Hello' $action = { Write-Output $var } &$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 index 5dcbd9a67..35ac2282a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockRenamed.ps1 @@ -1,3 +1,3 @@ -$Renamed = "Hello" +$Renamed = 'Hello' $action = { Write-Output $Renamed } &$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 index 76439a890..c37f20f5d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScoped.ps1 @@ -1,3 +1,3 @@ -$var = "Hello" -$action = { $var="No";Write-Output $var } +$var = 'Hello' +$action = { $var = 'No'; Write-Output $var } &$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 index 54e1d31e4..06e0db7a6 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInScriptblockScopedRenamed.ps1 @@ -1,3 +1,3 @@ -$var = "Hello" -$action = { $Renamed="No";Write-Output $Renamed } +$var = 'Hello' +$action = { $Renamed = 'No'; Write-Output $Renamed } &$action diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 index 393b2bdfd..32efd9617 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblock.ps1 @@ -1,7 +1,7 @@ function Sample{ - $var = "Hello" + $var = 'Hello' $sb = { - write-host $var + Write-Host $var } & $sb $var diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 index 70a51b6b6..3d8fb1184 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNestedFunctionScriptblockRenamed.ps1 @@ -1,7 +1,7 @@ function Sample{ - $Renamed = "Hello" + $Renamed = 'Hello' $sb = { - write-host $Renamed + Write-Host $Renamed } & $sb $Renamed diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 index 78119ac37..eaf921681 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParam.ps1 @@ -1,8 +1,8 @@ $params = @{ - HtmlBodyContent = "Testing JavaScript and CSS paths..." - JavaScriptPaths = ".\Assets\script.js" - StyleSheetPaths = ".\Assets\style.css" + HtmlBodyContent = 'Testing JavaScript and CSS paths...' + JavaScriptPaths = '.\Assets\script.js' + StyleSheetPaths = '.\Assets\style.css' } -$view = New-VSCodeHtmlContentView -Title "Test View" -ShowInColumn Two +$view = New-VSCodeHtmlContentView -Title 'Test View' -ShowInColumn Two Set-VSCodeHtmlContentView -View $view @params diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 index e6858827b..31740427f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableNonParamRenamed.ps1 @@ -1,8 +1,8 @@ $params = @{ - HtmlBodyContent = "Testing JavaScript and CSS paths..." - JavaScriptPaths = ".\Assets\script.js" - StyleSheetPaths = ".\Assets\style.css" + HtmlBodyContent = 'Testing JavaScript and CSS paths...' + JavaScriptPaths = '.\Assets\script.js' + StyleSheetPaths = '.\Assets\style.css' } -$Renamed = New-VSCodeHtmlContentView -Title "Test View" -ShowInColumn Two +$Renamed = New-VSCodeHtmlContentView -Title 'Test View' -ShowInColumn Two Set-VSCodeHtmlContentView -View $Renamed @params diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 index ff874d121..1a14d2d8b 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlock.ps1 @@ -20,9 +20,9 @@ function Write-Item($itemCount) { # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. function Do-Work($workCount) { - Write-Output "Doing work..." + Write-Output 'Doing work...' Write-Item $workcount - Write-Host "Done!" + Write-Host 'Done!' } Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 index ba0ae7702..aa9e325d0 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableScriptWithParamBlockRenamed.ps1 @@ -20,9 +20,9 @@ function Write-Item($itemCount) { # Hover over the function name below to see the PSScriptAnalyzer warning that "Do-Work" # doesn't use an approved verb. function Do-Work($workCount) { - Write-Output "Doing work..." + Write-Output 'Doing work...' Write-Item $workcount - Write-Host "Done!" + Write-Host 'Done!' } Do-Work $Count diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 index 8e2a4ef5d..ca370b580 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameter.ps1 @@ -5,14 +5,14 @@ function testing_files { param ( $x ) - write-host "Printing $x" + Write-Host "Printing $x" } foreach ($number in $x) { testing_files $number function testing_files { - write-host "------------------" + Write-Host '------------------' } } -testing_files "99" +testing_files '99' diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 index 12af8cd08..0e022321f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleFunctionParameterRenamed.ps1 @@ -5,14 +5,14 @@ function testing_files { param ( $Renamed ) - write-host "Printing $Renamed" + Write-Host "Printing $Renamed" } foreach ($number in $x) { testing_files $number function testing_files { - write-host "------------------" + Write-Host '------------------' } } -testing_files "99" +testing_files '99' From 1d8e57ad2653943d7689077b4bc4c60a1886b259 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 17:08:16 -0700 Subject: [PATCH 183/203] Fix issue with dependency injection weirdly setting Disclaimer to true and fix when dialog is closed rather than just button --- .../Services/TextDocument/RenameService.cs | 29 ++++++++++--------- .../Refactoring/PrepareRenameHandlerTests.cs | 6 ++-- .../Refactoring/RenameHandlerTests.cs | 6 ++-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index e7f0b64fb..aa540ab71 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -48,12 +48,12 @@ internal interface IRenameService internal class RenameService( WorkspaceService workspaceService, ILanguageServerFacade lsp, - ILanguageServerConfiguration config, - bool disclaimerDeclinedForSession = false, - bool disclaimerAcceptedForSession = false, - string configSection = "powershell.rename" + ILanguageServerConfiguration config ) : IRenameService { + internal bool DisclaimerAcceptedForSession; //This is exposed to allow testing non-interactively + private bool DisclaimerDeclinedForSession; + private const string ConfigSection = "powershell.rename"; public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { @@ -211,8 +211,10 @@ internal static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst /// true if accepted, false if rejected private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, CancellationToken cancellationToken) { - if (disclaimerDeclinedForSession) { return false; } - if (acceptDisclaimerOption || disclaimerAcceptedForSession) { return true; } + const string disclaimerDeclinedMessage = "PowerShell rename has been disabled for this session as the disclaimer message was declined. Please restart the extension if you wish to use rename and accept the disclaimer."; + + if (DisclaimerDeclinedForSession) { throw new HandlerErrorException(disclaimerDeclinedMessage); } + if (acceptDisclaimerOption || DisclaimerAcceptedForSession) { return true; } // TODO: Localization const string renameDisclaimer = "PowerShell rename functionality is only supported in a limited set of circumstances. [Please review the notice](https://github.com/PowerShell/PowerShellEditorServices?tab=readme-ov-file#rename-disclaimer) and accept the limitations and risks."; @@ -235,8 +237,9 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can } }; - MessageActionItem result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); - if (result.Title == declineAnswer) + MessageActionItem? result = await lsp.SendRequest(reqParams, cancellationToken).ConfigureAwait(false); + // null happens if the user closes the dialog rather than making a selection. + if (result is null || result.Title == declineAnswer) { const string renameDisabledNotice = "PowerShell Rename functionality will be disabled for this session and you will not be prompted again until restart."; @@ -246,8 +249,8 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can Type = MessageType.Info }; lsp.SendNotification(msgParams); - disclaimerDeclinedForSession = true; - return !disclaimerDeclinedForSession; + DisclaimerDeclinedForSession = true; + throw new HandlerErrorException(disclaimerDeclinedMessage); } if (result.Title == acceptAnswer) { @@ -259,8 +262,8 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can }; lsp.SendNotification(msgParams); - disclaimerAcceptedForSession = true; - return disclaimerAcceptedForSession; + DisclaimerAcceptedForSession = true; + return DisclaimerAcceptedForSession; } // if (result.Title == acceptWorkspaceAnswer) // { @@ -279,7 +282,7 @@ private async Task AcceptRenameDisclaimer(bool acceptDisclaimerOption, Can private async Task GetScopedSettings(DocumentUri uri, CancellationToken cancellationToken = default) { IScopedConfiguration scopedConfig = await config.GetScopedConfiguration(uri, cancellationToken).ConfigureAwait(false); - return scopedConfig.GetSection(configSection).Get() ?? new RenameServiceOptions(); + return scopedConfig.GetSection(ConfigSection).Get() ?? new RenameServiceOptions(); } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 4ecbb5f20..264f3157b 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -43,9 +43,11 @@ public PrepareRenameHandlerTests() ( workspace, new fakeLspSendMessageRequestFacade("I Accept"), - new EmptyConfiguration(), - disclaimerAcceptedForSession: true //Suppresses prompts + new EmptyConfiguration() ) + { + DisclaimerAcceptedForSession = true //Disables UI prompts + } ); } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index e9e022c50..d42ad9d6e 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -36,9 +36,11 @@ public RenameHandlerTests() ( workspace, new fakeLspSendMessageRequestFacade("I Accept"), - new EmptyConfiguration(), - disclaimerAcceptedForSession: true //Disables UI prompts + new EmptyConfiguration() ) + { + DisclaimerAcceptedForSession = true //Disables UI prompts + } ); } From fdf05a9a628b3aea54028741958cee9c0ee81bc2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 26 Sep 2024 18:56:54 -0700 Subject: [PATCH 184/203] Change name of Alias Setting --- .../Services/TextDocument/RenameService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index aa540ab71..466220891 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -25,7 +25,7 @@ namespace Microsoft.PowerShell.EditorServices.Services; public class RenameServiceOptions { public bool createFunctionAlias { get; set; } - public bool createVariableAlias { get; set; } + public bool createParameterAlias { get; set; } public bool acceptDisclaimer { get; set; } } @@ -99,7 +99,7 @@ or CommandAst or ParameterAst or CommandParameterAst or AssignmentStatementAst - => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createVariableAlias), + => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createParameterAlias), _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; @@ -126,7 +126,7 @@ internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParam return visitor.VisitAndGetEdits(scriptAst); } - internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createAlias) + internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createParameterAlias) { if (symbol is not (VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst)) { @@ -138,7 +138,7 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam symbol.Extent.StartLineNumber, symbol.Extent.StartColumnNumber, scriptAst, - createAlias + createParameterAlias ); return visitor.VisitAndGetEdits(); @@ -418,15 +418,15 @@ internal class RenameVariableVisitor : RenameVisitorBase internal bool isParam; internal bool AliasSet; internal FunctionDefinitionAst TargetFunction; - internal bool CreateAlias; + internal bool CreateParameterAlias; - public RenameVariableVisitor(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateAlias) + public RenameVariableVisitor(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateParameterAlias) { this.NewName = NewName; this.StartLineNumber = StartLineNumber; this.StartColumnNumber = StartColumnNumber; this.ScriptAst = ScriptAst; - this.CreateAlias = CreateAlias; + this.CreateParameterAlias = CreateParameterAlias; VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); if (Node != null) @@ -800,7 +800,7 @@ private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressi }; // If the variables parent is a parameterAst Add a modification if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - CreateAlias) + CreateParameterAlias) { TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); Edits.Add(aliasChange); From dbc6f69747f042fa3eb20627691ee8c1166fc559 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 07:30:39 -0700 Subject: [PATCH 185/203] Make the PrepareRenameSymbol return more legible --- .../Services/TextDocument/RenameService.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 466220891..7a29f5b68 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -67,11 +67,9 @@ ILanguageServerConfiguration config WorkspaceEdit? renameResponse = await RenameSymbol(renameRequest, cancellationToken).ConfigureAwait(false); // Since LSP 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. + RangeOrPlaceholderRange renameSupported = new(new RenameDefaultBehavior() { DefaultBehavior = true }); return (renameResponse?.Changes?[request.TextDocument.Uri].ToArray().Length > 0) - ? new RangeOrPlaceholderRange - ( - new RenameDefaultBehavior() { DefaultBehavior = true } - ) + ? renameSupported : null; } From a3e85576e7528dfeb1d1f21b84d714711b742cf3 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 08:23:14 -0700 Subject: [PATCH 186/203] Add support for negative test cases --- .../Services/TextDocument/RenameService.cs | 5 ++- .../Functions/FunctionSameNameRenamed.ps1 | 6 +-- .../Functions/RefactorFunctionTestCases.cs | 2 +- .../Refactoring/RenameTestTarget.cs | 8 +++- .../Variables/RefactorVariableTestCases.cs | 2 + .../Refactoring/PrepareRenameHandlerTests.cs | 37 +++++++++++++++++-- .../Refactoring/RenameHandlerTests.cs | 33 ++++++++++++++++- 7 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 7a29f5b68..b15a3ee2e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -63,7 +63,8 @@ ILanguageServerConfiguration config Position = request.Position, TextDocument = request.TextDocument }; - // TODO: Should we cache these resuls and just fetch them on the actual rename, and move the bulk to an implementation method? + + // TODO: As a performance optimization, should we cache these results and just fetch them on the actual rename, and move the bulk to an implementation method? Seems pretty fast right now but may slow down on large documents. Need to add a large document test example. WorkspaceEdit? renameResponse = await RenameSymbol(renameRequest, cancellationToken).ConfigureAwait(false); // Since LSP 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. @@ -318,7 +319,7 @@ public AstVisitAction Visit(Ast ast) { FunctionDefinitionAst f => f, CommandAst command => CurrentDocument.FindFunctionDefinition(command) - ?? throw new TargetSymbolNotFoundException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), + ?? throw new HandlerErrorException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), _ => throw new Exception($"Unsupported AST type {target.GetType()} encountered") }; }; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 index 669266740..e5b036e94 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 @@ -1,8 +1,8 @@ function SameNameFunction { Write-Host "This is the outer function" - function RenamedSameNameFunction { - Write-Host "This is the inner function" + function Renamed { + Write-Host 'This is the inner function' } - RenamedSameNameFunction + Renamed } SameNameFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs index 3ef34a999..3583c631f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs @@ -19,7 +19,7 @@ public class RefactorFunctionTestCases new("FunctionMultipleOccurrences.ps1", Line: 5, Column: 3 ), new("FunctionNestedRedefinition.ps1", Line: 13, Column: 15 ), new("FunctionOuterHasNestedFunction.ps1", Line: 1, Column: 10 ), - new("FunctionSameName.ps1", Line: 3, Column: 14 , "RenamedSameNameFunction"), + new("FunctionSameName.ps1", Line: 3, Column: 14 ), new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), new("FunctionsSingle.ps1", Line: 1, Column: 11 ), new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5 ), diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs index fc08347af..787418962 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs @@ -27,18 +27,22 @@ public class RenameTestTarget /// public string NewName = "Renamed"; + public bool ShouldFail; + /// The test case file name e.g. testScript.ps1 /// The line where the cursor should be positioned for the rename /// The column/character indent where ther cursor should be positioned for the rename /// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified - public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed") + /// This test case should not succeed and return either null or a handler error + public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed", bool ShouldFail = false) { this.FileName = FileName; this.Line = Line; this.Column = Column; this.NewName = NewName; + this.ShouldFail = ShouldFail; } public RenameTestTarget() { } - public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)}"; + public override string ToString() => $"{FileName} L{Line} C{Column} N:{NewName} F:{ShouldFail}"; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index 6b8ae7818..0396b8a22 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -6,6 +6,8 @@ public class RefactorVariableTestCases public static RenameTestTarget[] TestCases = [ new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), + new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1, NewName: "$Renamed"), + new ("SimpleVariableAssignment.ps1", Line: 2, Column: 1, NewName: "Wrong", ShouldFail: true), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), new ("VariableCommandParameter.ps1", Line: 10, Column: 10), new ("VariableCommandParameterSplatted.ps1", Line: 3, Column: 19 ), diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 264f3157b..55fab99b6 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -67,7 +67,21 @@ public async Task FindsFunction(RenameTestTarget s) { PrepareRenameParams testParams = s.ToPrepareRenameParams("Functions"); - RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); + RangeOrPlaceholderRange? result; + try + { + result = await testHandler.Handle(testParams, CancellationToken.None); + } + catch (HandlerErrorException) + { + Assert.True(s.ShouldFail); + return; + } + if (s.ShouldFail) + { + Assert.Null(result); + return; + } Assert.NotNull(result); Assert.True(result?.DefaultBehavior?.DefaultBehavior); @@ -79,7 +93,21 @@ public async Task FindsVariable(RenameTestTarget s) { PrepareRenameParams testParams = s.ToPrepareRenameParams("Variables"); - RangeOrPlaceholderRange? result = await testHandler.Handle(testParams, CancellationToken.None); + RangeOrPlaceholderRange? result; + try + { + result = await testHandler.Handle(testParams, CancellationToken.None); + } + catch (HandlerErrorException) + { + Assert.True(s.ShouldFail); + return; + } + if (s.ShouldFail) + { + Assert.Null(result); + return; + } Assert.NotNull(result); Assert.True(result?.DefaultBehavior?.DefaultBehavior); @@ -184,6 +212,7 @@ public void Serialize(IXunitSerializationInfo info) info.AddValue(nameof(Line), Line); info.AddValue(nameof(Column), Column); info.AddValue(nameof(NewName), NewName); + info.AddValue(nameof(ShouldFail), ShouldFail); } public void Deserialize(IXunitSerializationInfo info) @@ -192,6 +221,7 @@ public void Deserialize(IXunitSerializationInfo info) Line = info.GetValue(nameof(Line)); Column = info.GetValue(nameof(Column)); NewName = info.GetValue(nameof(NewName)); + ShouldFail = info.GetValue(nameof(ShouldFail)); } public static RenameTestTargetSerializable FromRenameTestTarget(RenameTestTarget t) @@ -200,6 +230,7 @@ public static RenameTestTargetSerializable FromRenameTestTarget(RenameTestTarget FileName = t.FileName, Column = t.Column, Line = t.Line, - NewName = t.NewName + NewName = t.NewName, + ShouldFail = t.ShouldFail }; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index d42ad9d6e..d22e35c26 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -56,7 +56,22 @@ public static TheoryData FunctionTestCases() public async void RenamedFunction(RenameTestTarget s) { RenameParams request = s.ToRenameParams("Functions"); - WorkspaceEdit response = await testHandler.Handle(request, CancellationToken.None); + WorkspaceEdit response; + try + { + response = await testHandler.Handle(request, CancellationToken.None); + } + catch (HandlerErrorException) + { + Assert.True(s.ShouldFail); + return; + } + if (s.ShouldFail) + { + Assert.Null(response); + return; + } + DocumentUri testScriptUri = request.TextDocument.Uri; string expected = workspace.GetFile @@ -78,7 +93,21 @@ public async void RenamedFunction(RenameTestTarget s) public async void RenamedVariable(RenameTestTarget s) { RenameParams request = s.ToRenameParams("Variables"); - WorkspaceEdit response = await testHandler.Handle(request, CancellationToken.None); + WorkspaceEdit response; + try + { + response = await testHandler.Handle(request, CancellationToken.None); + } + catch (HandlerErrorException) + { + Assert.True(s.ShouldFail); + return; + } + if (s.ShouldFail) + { + Assert.Null(response); + return; + } DocumentUri testScriptUri = request.TextDocument.Uri; string expected = workspace.GetFile From a8d72e6edbf0656b6c4ea03855dc3977f9a3c1e9 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 08:30:04 -0700 Subject: [PATCH 187/203] I thought I fixed all these... --- .../Refactoring/Functions/FunctionSameName.ps1 | 4 ++-- .../Refactoring/Functions/FunctionSameNameRenamed.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 index 726ea6d56..9849ee15a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameName.ps1 @@ -1,7 +1,7 @@ function SameNameFunction { - Write-Host "This is the outer function" + Write-Host 'This is the outer function' function SameNameFunction { - Write-Host "This is the inner function" + Write-Host 'This is the inner function' } SameNameFunction } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 index e5b036e94..e32595a64 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSameNameRenamed.ps1 @@ -1,5 +1,5 @@ function SameNameFunction { - Write-Host "This is the outer function" + Write-Host 'This is the outer function' function Renamed { Write-Host 'This is the inner function' } From 062117336016c1449ababc66f020290189a4d4ea Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 08:36:10 -0700 Subject: [PATCH 188/203] Small test fixes and naming updates --- .../Refactoring/RenameTestTarget.cs | 2 +- .../Refactoring/Variables/RefactorVariableTestCases.cs | 4 ++-- ...bleExpression.ps1 => VariableWithinHastableExpression.ps1} | 0 ...enamed.ps1 => VariableWithinHastableExpressionRenamed.ps1} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VariablewWithinHastableExpression.ps1 => VariableWithinHastableExpression.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{VariablewWithinHastableExpressionRenamed.ps1 => VariableWithinHastableExpressionRenamed.ps1} (100%) diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs index 787418962..5c8c48d5f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs @@ -44,5 +44,5 @@ public RenameTestTarget(string FileName, int Line, int Column, string NewName = } public RenameTestTarget() { } - public override string ToString() => $"{FileName} L{Line} C{Column} N:{NewName} F:{ShouldFail}"; + public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)} {Line}:{Column} N:{NewName} F:{ShouldFail}"; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index 0396b8a22..f0d35214f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -5,8 +5,8 @@ public class RefactorVariableTestCases { public static RenameTestTarget[] TestCases = [ - new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1, NewName: "$Renamed"), + new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), new ("SimpleVariableAssignment.ps1", Line: 2, Column: 1, NewName: "Wrong", ShouldFail: true), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), new ("VariableCommandParameter.ps1", Line: 10, Column: 10), @@ -31,6 +31,6 @@ public class RefactorVariableTestCases new ("VariableusedInWhileLoop.ps1", Line: 2, Column: 5), new ("VariableWithinCommandAstScriptBlock.ps1", Line: 3, Column: 75), new ("VariableWithinForeachObject.ps1", Line: 2, Column: 1), - new ("VariablewWithinHastableExpression.ps1", Line: 3, Column: 46), + new ("VariableWithinHastableExpression.ps1", Line: 3, Column: 46), ]; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinHastableExpression.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpression.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinHastableExpression.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinHastableExpressionRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariablewWithinHastableExpressionRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableWithinHastableExpressionRenamed.ps1 From 0abc7133848f17504cca0c18c893047fc5042f78 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 10:37:05 -0700 Subject: [PATCH 189/203] Move AstExtensions to Utility --- .../{Language => Utility}/AstExtensions.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/PowerShellEditorServices/{Language => Utility}/AstExtensions.cs (100%) diff --git a/src/PowerShellEditorServices/Language/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs similarity index 100% rename from src/PowerShellEditorServices/Language/AstExtensions.cs rename to src/PowerShellEditorServices/Utility/AstExtensions.cs From 703c804770dfd3e3d0e482948a386ca7abd0da06 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 27 Sep 2024 10:41:10 -0700 Subject: [PATCH 190/203] Remove utilities as they have been moved into RenameService or AstExtensions --- .../PowerShell/Refactoring/Utilities.cs | 167 ------------------ 1 file changed, 167 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs diff --git a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs b/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs deleted file mode 100644 index 2f441d02b..000000000 --- a/src/PowerShellEditorServices/Services/PowerShell/Refactoring/Utilities.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Refactoring -{ - internal class Utilities - { - public static Ast GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) - { - Ast result = null; - result = ScriptAst.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - type.Contains(ast.GetType()); - }, true); - if (result == null) - { - throw new TargetSymbolNotFoundException(); - } - return result; - } - - public static Ast GetAstParentOfType(Ast ast, params Type[] type) - { - Ast parent = ast; - // walk backwards till we hit a parent of the specified type or return null - while (null != parent) - { - if (type.Contains(parent.GetType())) - { - return parent; - } - parent = parent.Parent; - } - return null; - - } - - public static FunctionDefinitionAst GetFunctionDefByCommandAst(string OldName, int StartLineNumber, int StartColumnNumber, Ast ScriptFile) - { - // Look up the targeted object - CommandAst TargetCommand = (CommandAst)Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, ScriptFile - , typeof(CommandAst)); - - if (TargetCommand.GetCommandName().ToLower() != OldName.ToLower()) - { - TargetCommand = null; - } - - string FunctionName = TargetCommand.GetCommandName(); - - List FunctionDefinitions = ScriptFile.FindAll(ast => - { - return ast is FunctionDefinitionAst FuncDef && - FuncDef.Name.ToLower() == OldName.ToLower() && - (FuncDef.Extent.EndLineNumber < TargetCommand.Extent.StartLineNumber || - (FuncDef.Extent.EndColumnNumber <= TargetCommand.Extent.StartColumnNumber && - FuncDef.Extent.EndLineNumber <= TargetCommand.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the function def if we only have one match - if (FunctionDefinitions.Count == 1) - { - return FunctionDefinitions[0]; - } - // Determine which function definition is the right one - FunctionDefinitionAst CorrectDefinition = null; - for (int i = FunctionDefinitions.Count - 1; i >= 0; i--) - { - FunctionDefinitionAst element = FunctionDefinitions[i]; - - Ast parent = element.Parent; - // walk backwards till we hit a functiondefinition if any - while (null != parent) - { - if (parent is FunctionDefinitionAst) - { - break; - } - parent = parent.Parent; - } - // we have hit the global scope of the script file - if (null == parent) - { - CorrectDefinition = element; - break; - } - - if (TargetCommand.Parent == parent) - { - CorrectDefinition = (FunctionDefinitionAst)parent; - } - } - return CorrectDefinition; - } - - public static bool AssertContainsDotSourced(Ast ScriptAst) - { - Ast dotsourced = ScriptAst.Find(ast => - { - return ast is CommandAst commandAst && commandAst.InvocationOperator == TokenKind.Dot; - }, true); - if (dotsourced != null) - { - return true; - } - return false; - } - - public static Ast GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) - { - Ast token = null; - - token = Ast.Find(ast => - { - return StartLineNumber == ast.Extent.StartLineNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - - if (token is NamedBlockAst) - { - // NamedBlockAST starts on the same line as potentially another AST, - // its likley a user is not after the NamedBlockAst but what it contains - IEnumerable stacked_tokens = token.FindAll(ast => - { - return StartLineNumber == ast.Extent.StartLineNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber - && StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - - if (stacked_tokens.Count() > 1) - { - return stacked_tokens.LastOrDefault(); - } - - return token.Parent; - } - - if (null == token) - { - IEnumerable LineT = Ast.FindAll(ast => - { - return StartLineNumber == ast.Extent.StartLineNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - return LineT.OfType()?.LastOrDefault(); - } - - IEnumerable tokens = token.FindAll(ast => - { - return ast.Extent.EndColumnNumber >= StartColumnNumber - && StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - if (tokens.Count() > 1) - { - token = tokens.LastOrDefault(); - } - return token; - } - } -} From 2800ab2982bc3c5b02a79305945ba7ad5388284d Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 28 Sep 2024 20:36:39 -0700 Subject: [PATCH 191/203] Rewrote Variable Handler, all tests passing except a stringconstant splat reference --- .../Services/TextDocument/RenameService.cs | 672 ++---------------- .../Utility/AstExtensions.cs | 452 +++++++++++- .../Variables/ParameterUndefinedFunction.ps1 | 1 + .../Variables/RefactorVariableTestCases.cs | 5 +- .../Variables/VariableInPipeline.ps1 | 1 + .../Variables/VariableInPipelineRenamed.ps1 | 1 + .../Refactoring/PrepareRenameHandlerTests.cs | 4 +- .../Refactoring/RenameHandlerTests.cs | 4 +- 8 files changed, 518 insertions(+), 622 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/ParameterUndefinedFunction.ps1 diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index b15a3ee2e..7efa09665 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -132,15 +132,17 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam throw new HandlerErrorException($"Asked to rename a variable but the target is not a viable variable type: {symbol.GetType()}. This is a bug, file an issue if you see this."); } - RenameVariableVisitor visitor = new( - requestParams.NewName, - symbol.Extent.StartLineNumber, - symbol.Extent.StartColumnNumber, - scriptAst, - createParameterAlias + // RenameVariableVisitor visitor = new( + // requestParams.NewName, + // symbol.Extent.StartLineNumber, + // symbol.Extent.StartColumnNumber, + // scriptAst, + // createParameterAlias + // ); + NewRenameVariableVisitor visitor = new( + symbol, requestParams.NewName ); - return visitor.VisitAndGetEdits(); - + return visitor.VisitAndGetEdits(scriptAst); } /// @@ -149,7 +151,7 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam /// Ast of the token or null if no renamable symbol was found internal static Ast? FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) { - Ast? ast = scriptFile.ScriptAst.FindAtPosition(position, + Ast? ast = scriptFile.ScriptAst.FindClosest(position, [ // Functions typeof(FunctionDefinitionAst), @@ -157,9 +159,8 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam // Variables typeof(VariableExpressionAst), - typeof(ParameterAst), - typeof(CommandParameterAst), - typeof(AssignmentStatementAst), + typeof(CommandParameterAst) + // FIXME: Splat parameter in hashtable ]); // Only the function name is valid for rename, not other components @@ -285,9 +286,19 @@ private async Task GetScopedSettings(DocumentUri uri, Canc } } -internal abstract class RenameVisitorBase() : AstVisitor +internal abstract class RenameVisitorBase : AstVisitor { internal List Edits { get; } = new(); + internal Ast? CurrentDocument { get; set; } + + /// + /// A convenience method to get text edits from a specified AST. + /// + internal virtual TextEdit[] VisitAndGetEdits(Ast ast) + { + ast.Visit(this); + return Edits.ToArray(); + } } /// @@ -297,14 +308,13 @@ internal abstract class RenameVisitorBase() : AstVisitor /// internal class RenameFunctionVisitor(Ast target, string newName, bool skipVerify = false) : RenameVisitorBase { - private Ast? CurrentDocument; private FunctionDefinitionAst? FunctionToRename; // Wire up our visitor to the relevant AST types we are potentially renaming public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast) => Visit(ast); public override AstVisitAction VisitCommand(CommandAst ast) => Visit(ast); - public AstVisitAction Visit(Ast ast) + internal AstVisitAction Visit(Ast ast) { // If this is our first run, we need to verify we are in scope and gather our rename operation info if (!skipVerify && CurrentDocument is null) @@ -338,7 +348,7 @@ public AstVisitAction Visit(Ast ast) // TODO: Is there a way we can know we are fully outside where the function might be referenced, and if so, call a AstVisitAction Abort as a perf optimization? } - private bool ShouldRename(Ast candidate) + internal bool ShouldRename(Ast candidate) { // Rename our original function definition. There may be duplicate definitions of the same name if (candidate is FunctionDefinitionAst funcDef) @@ -396,630 +406,86 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) Range = new ScriptExtentAdapter(funcName.Extent) }; } - - internal TextEdit[] VisitAndGetEdits(Ast ast) - { - ast.Visit(this); - return Edits.ToArray(); - } } -#nullable disable -internal class RenameVariableVisitor : RenameVisitorBase +internal class NewRenameVariableVisitor(Ast target, string newName, bool skipVerify = false) : RenameVisitorBase { - private readonly string OldName; - private readonly string NewName; - internal bool ShouldRename; - internal int StartLineNumber; - internal int StartColumnNumber; - internal VariableExpressionAst TargetVariableAst; - internal readonly Ast ScriptAst; - internal bool isParam; - internal bool AliasSet; - internal FunctionDefinitionAst TargetFunction; - internal bool CreateParameterAlias; - - public RenameVariableVisitor(string NewName, int StartLineNumber, int StartColumnNumber, Ast ScriptAst, bool CreateParameterAlias) - { - this.NewName = NewName; - this.StartLineNumber = StartLineNumber; - this.StartColumnNumber = StartColumnNumber; - this.ScriptAst = ScriptAst; - this.CreateParameterAlias = CreateParameterAlias; - - VariableExpressionAst Node = (VariableExpressionAst)GetVariableTopAssignment(StartLineNumber, StartColumnNumber, ScriptAst); - if (Node != null) - { - if (Node.Parent is ParameterAst) - { - isParam = true; - Ast parent = Node; - // Look for a target function that the parameterAst will be within if it exists - parent = Utilities.GetAstParentOfType(parent, typeof(FunctionDefinitionAst)); - if (parent != null) - { - TargetFunction = (FunctionDefinitionAst)parent; - } - } - TargetVariableAst = Node; - OldName = TargetVariableAst.VariablePath.UserPath.Replace("$", ""); - this.StartColumnNumber = TargetVariableAst.Extent.StartColumnNumber; - this.StartLineNumber = TargetVariableAst.Extent.StartLineNumber; - } - } - - private static Ast GetVariableTopAssignment(int StartLineNumber, int StartColumnNumber, Ast ScriptAst) - { - - // Look up the target object - Ast node = Utilities.GetAstAtPositionOfType(StartLineNumber, StartColumnNumber, - ScriptAst, typeof(VariableExpressionAst), typeof(CommandParameterAst), typeof(StringConstantExpressionAst)); + // Used to store the original definition of the variable to use as a reference. + internal Ast? VariableDefinition; - string name = node switch - { - CommandParameterAst commdef => commdef.ParameterName, - VariableExpressionAst varDef => varDef.VariablePath.UserPath, - // Key within a Hashtable - StringConstantExpressionAst strExp => strExp.Value, - _ => throw new TargetSymbolNotFoundException() - }; - - VariableExpressionAst splatAssignment = null; - // A rename of a parameter has been initiated from a splat - if (node is StringConstantExpressionAst) - { - Ast parent = node; - parent = Utilities.GetAstParentOfType(parent, typeof(AssignmentStatementAst)); - if (parent is not null and AssignmentStatementAst assignmentStatementAst) - { - splatAssignment = (VariableExpressionAst)assignmentStatementAst.Left.Find( - ast => ast is VariableExpressionAst, false); - } - } - - Ast TargetParent = GetAstParentScope(node); - - // Is the Variable sitting within a ParameterBlockAst that is within a Function Definition - // If so we don't need to look further as this is most likley the AssignmentStatement we are looking for - Ast paramParent = Utilities.GetAstParentOfType(node, typeof(ParamBlockAst)); - if (TargetParent is FunctionDefinitionAst && null != paramParent) - { - return node; - } - - // Find all variables and parameter assignments with the same name before - // The node found above - List VariableAssignments = ScriptAst.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == name.ToLower() && - // Look Backwards from the node above - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)); - }, true).Cast().ToList(); - // return the def if we have no matches - if (VariableAssignments.Count == 0) - { - return node; - } - Ast CorrectDefinition = null; - for (int i = VariableAssignments.Count - 1; i >= 0; i--) - { - VariableExpressionAst element = VariableAssignments[i]; - - Ast parent = GetAstParentScope(element); - // closest assignment statement is within the scope of the node - if (TargetParent == parent) - { - CorrectDefinition = element; - break; - } - else if (node.Parent is AssignmentStatementAst) - { - // the node is probably the first assignment statement within the scope - CorrectDefinition = node; - break; - } - // node is proably just a reference to an assignment statement or Parameter within the global scope or higher - if (node.Parent is not AssignmentStatementAst) - { - if (null == parent || null == parent.Parent) - { - // we have hit the global scope of the script file - CorrectDefinition = element; - break; - } - - if (parent is FunctionDefinitionAst funcDef && node is CommandParameterAst or StringConstantExpressionAst) - { - if (node is StringConstantExpressionAst) - { - List SplatReferences = ScriptAst.FindAll(ast => - { - return ast is VariableExpressionAst varDef && - varDef.Splatted && - varDef.Parent is CommandAst && - varDef.VariablePath.UserPath.ToLower() == splatAssignment.VariablePath.UserPath.ToLower(); - }, true).Cast().ToList(); - - if (SplatReferences.Count >= 1) - { - CommandAst splatFirstRefComm = (CommandAst)SplatReferences.First().Parent; - if (funcDef.Name == splatFirstRefComm.GetCommandName() - && funcDef.Parent.Parent == TargetParent) - { - CorrectDefinition = element; - break; - } - } - } - - if (node.Parent is CommandAst commDef) - { - if (funcDef.Name == commDef.GetCommandName() - && funcDef.Parent.Parent == TargetParent) - { - CorrectDefinition = element; - break; - } - } - } - if (WithinTargetsScope(element, node)) - { - CorrectDefinition = element; - } - } - } - return CorrectDefinition ?? node; - } + // Validate and cleanup the newName definition. User may have left off the $ + // TODO: Full AST parsing to validate the name + private readonly string NewName = newName.TrimStart('$').TrimStart('-'); - private static Ast GetAstParentScope(Ast node) - { - Ast parent = node; - // Walk backwards up the tree looking for a ScriptBLock of a FunctionDefinition - parent = Utilities.GetAstParentOfType(parent, typeof(ScriptBlockAst), typeof(FunctionDefinitionAst), typeof(ForEachStatementAst), typeof(ForStatementAst)); - if (parent is ScriptBlockAst && parent.Parent != null && parent.Parent is FunctionDefinitionAst) - { - parent = parent.Parent; - } - // Check if the parent of the VariableExpressionAst is a ForEachStatementAst then check if the variable names match - // if so this is probably a variable defined within a foreach loop - else if (parent is ForEachStatementAst ForEachStmnt && node is VariableExpressionAst VarExp && - ForEachStmnt.Variable.VariablePath.UserPath == VarExp.VariablePath.UserPath) - { - parent = ForEachStmnt; - } - // Check if the parent of the VariableExpressionAst is a ForStatementAst then check if the variable names match - // if so this is probably a variable defined within a foreach loop - else if (parent is ForStatementAst ForStmnt && node is VariableExpressionAst ForVarExp && - ForStmnt.Initializer is AssignmentStatementAst AssignStmnt && AssignStmnt.Left is VariableExpressionAst VarExpStmnt && - VarExpStmnt.VariablePath.UserPath == ForVarExp.VariablePath.UserPath) - { - parent = ForStmnt; - } - - return parent; - } - - private static bool IsVariableExpressionAssignedInTargetScope(VariableExpressionAst node, Ast scope) - { - bool r = false; - - List VariableAssignments = node.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && - // Look Backwards from the node above - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && - // Must be within the the designated scope - VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; - }, true).Cast().ToList(); - - if (VariableAssignments.Count > 0) - { - r = true; - } - // Node is probably the first Assignment Statement within scope - if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) - { - r = true; - } - - return r; - } - - private static bool WithinTargetsScope(Ast Target, Ast Child) - { - bool r = false; - Ast childParent = Child.Parent; - Ast TargetScope = GetAstParentScope(Target); - while (childParent != null) - { - if (childParent is FunctionDefinitionAst FuncDefAst) - { - if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) - { - - } - else - { - break; - } - } - if (childParent == TargetScope) - { - break; - } - childParent = childParent.Parent; - } - if (childParent == TargetScope) - { - r = true; - } - return r; - } - - private class NodeProcessingState - { - public Ast Node { get; set; } - public IEnumerator ChildrenEnumerator { get; set; } - } - - internal void Visit(Ast root) - { - Stack processingStack = new(); - - processingStack.Push(new NodeProcessingState { Node = root }); - - while (processingStack.Count > 0) - { - NodeProcessingState currentState = processingStack.Peek(); - - if (currentState.ChildrenEnumerator == null) - { - // First time processing this node. Do the initial processing. - ProcessNode(currentState.Node); // This line is crucial. - - // Get the children and set up the enumerator. - IEnumerable children = currentState.Node.FindAll(ast => ast.Parent == currentState.Node, searchNestedScriptBlocks: true); - currentState.ChildrenEnumerator = children.GetEnumerator(); - } - - // Process the next child. - if (currentState.ChildrenEnumerator.MoveNext()) - { - Ast child = currentState.ChildrenEnumerator.Current; - processingStack.Push(new NodeProcessingState { Node = child }); - } - else - { - // All children have been processed, we're done with this node. - processingStack.Pop(); - } - } - } - - private void ProcessNode(Ast node) - { - - switch (node) - { - case CommandAst commandAst: - ProcessCommandAst(commandAst); - break; - case CommandParameterAst commandParameterAst: - ProcessCommandParameterAst(commandParameterAst); - break; - case VariableExpressionAst variableExpressionAst: - ProcessVariableExpressionAst(variableExpressionAst); - break; - } - } - - private void ProcessCommandAst(CommandAst commandAst) - { - // Is the Target Variable a Parameter and is this commandAst the target function - if (isParam && commandAst.GetCommandName()?.ToLower() == TargetFunction?.Name.ToLower()) - { - // Check to see if this is a splatted call to the target function. - Ast Splatted = null; - foreach (Ast element in commandAst.CommandElements) - { - if (element is VariableExpressionAst varAst && varAst.Splatted) - { - Splatted = varAst; - break; - } - } - if (Splatted != null) - { - NewSplattedModification(Splatted); - } - else - { - // The Target Variable is a Parameter and the commandAst is the Target Function - ShouldRename = true; - } - } - } - - private void ProcessVariableExpressionAst(VariableExpressionAst variableExpressionAst) - { - if (variableExpressionAst.VariablePath.UserPath.ToLower() == OldName.ToLower()) - { - // Is this the Target Variable - if (variableExpressionAst.Extent.StartColumnNumber == StartColumnNumber && - variableExpressionAst.Extent.StartLineNumber == StartLineNumber) - { - ShouldRename = true; - TargetVariableAst = variableExpressionAst; - } - // Is this a Command Ast within scope - else if (variableExpressionAst.Parent is CommandAst commandAst) - { - if (WithinTargetsScope(TargetVariableAst, commandAst)) - { - ShouldRename = true; - } - // The TargetVariable is defined within a function - // This commandAst is not within that function's scope so we should not rename - if (GetAstParentScope(TargetVariableAst) is FunctionDefinitionAst && !WithinTargetsScope(TargetVariableAst, commandAst)) - { - ShouldRename = false; - } - - } - // Is this a Variable Assignment thats not within scope - else if (variableExpressionAst.Parent is AssignmentStatementAst assignment && - assignment.Operator == TokenKind.Equals) - { - if (!WithinTargetsScope(TargetVariableAst, variableExpressionAst)) - { - ShouldRename = false; - } - - } - // Else is the variable within scope - else - { - ShouldRename = WithinTargetsScope(TargetVariableAst, variableExpressionAst); - } - if (ShouldRename) - { - // have some modifications to account for the dollar sign prefix powershell uses for variables - TextEdit Change = new() - { - NewText = NewName.Contains("$") ? NewName : "$" + NewName, - Range = new ScriptExtentAdapter(variableExpressionAst.Extent), - }; - // If the variables parent is a parameterAst Add a modification - if (variableExpressionAst.Parent is ParameterAst paramAst && !AliasSet && - CreateParameterAlias) - { - TextEdit aliasChange = NewParameterAliasChange(variableExpressionAst, paramAst); - Edits.Add(aliasChange); - AliasSet = true; - } - Edits.Add(Change); - - } - } - } + // Wire up our visitor to the relevant AST types we are potentially renaming + public override AstVisitAction VisitVariableExpression(VariableExpressionAst ast) => Visit(ast); + public override AstVisitAction VisitCommandParameter(CommandParameterAst ast) => Visit(ast); + public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst ast) => Visit(ast); - private void ProcessCommandParameterAst(CommandParameterAst commandParameterAst) + internal AstVisitAction Visit(Ast ast) { - if (commandParameterAst.ParameterName.ToLower() == OldName.ToLower()) + // If this is our first visit, we need to initialize and verify the scope, otherwise verify we are still on the same document. + if (!skipVerify && CurrentDocument is null || VariableDefinition is null) { - if (commandParameterAst.Extent.StartLineNumber == StartLineNumber && - commandParameterAst.Extent.StartColumnNumber == StartColumnNumber) + CurrentDocument = ast.GetHighestParent(); + if (CurrentDocument.Find(ast => ast == target, true) is null) { - ShouldRename = true; + throw new TargetSymbolNotFoundException("The target this visitor would rename is not present in the AST. This is a bug and you should file an issue"); } - if (TargetFunction != null && commandParameterAst.Parent is CommandAst commandAst && - commandAst.GetCommandName().ToLower() == TargetFunction.Name.ToLower() && isParam && ShouldRename) - { - TextEdit Change = new() - { - NewText = NewName.Contains("-") ? NewName : "-" + NewName, - Range = new ScriptExtentAdapter(commandParameterAst.Extent) - }; - Edits.Add(Change); - } - else + // Get the original assignment of our variable, this makes finding rename targets easier in subsequent visits as well as allows us to short-circuit quickly. + VariableDefinition = target.GetTopVariableAssignment(); + if (VariableDefinition is null) { - ShouldRename = false; + throw new HandlerErrorException("The element to rename does not have a definition. Renaming an element is only supported when the element is defined within the same scope"); } } - } - - private void NewSplattedModification(Ast Splatted) - { - // This Function should be passed a splatted VariableExpressionAst which - // is used by a CommandAst that is the TargetFunction. - - // Find the splats top assignment / definition - Ast SplatAssignment = GetVariableTopAssignment( - Splatted.Extent.StartLineNumber, - Splatted.Extent.StartColumnNumber, - ScriptAst); - // Look for the Parameter within the Splats HashTable - if (SplatAssignment.Parent is AssignmentStatementAst assignmentStatementAst && - assignmentStatementAst.Right is CommandExpressionAst commExpAst && - commExpAst.Expression is HashtableAst hashTableAst) + else if (CurrentDocument != ast.GetHighestParent()) { - foreach (Tuple element in hashTableAst.KeyValuePairs) - { - if (element.Item1 is StringConstantExpressionAst strConstAst && - strConstAst.Value.ToLower() == OldName.ToLower()) - { - TextEdit Change = new() - { - NewText = NewName, - Range = new ScriptExtentAdapter(strConstAst.Extent) - }; - - Edits.Add(Change); - break; - } - - } + throw new TargetSymbolNotFoundException("The visitor should not be reused to rename a different document. It should be created new for each rename operation. This is a bug and you should file an issue"); } - } - private TextEdit NewParameterAliasChange(VariableExpressionAst variableExpressionAst, ParameterAst paramAst) - { - // Check if an Alias AttributeAst already exists and append the new Alias to the existing list - // Otherwise Create a new Alias Attribute - // Add the modifications to the changes - // The Attribute will be appended before the variable or in the existing location of the original alias - TextEdit aliasChange = new(); - // FIXME: Understand this more, if this returns more than one result, why does it overwrite the aliasChange? - foreach (Ast Attr in paramAst.Attributes) - { - if (Attr is AttributeAst AttrAst) - { - // Alias Already Exists - if (AttrAst.TypeName.FullName == "Alias") - { - string existingEntries = AttrAst.Extent.Text - .Substring("[Alias(".Length); - existingEntries = existingEntries.Substring(0, existingEntries.Length - ")]".Length); - string nentries = existingEntries + $", \"{OldName}\""; - - aliasChange = aliasChange with - { - NewText = $"[Alias({nentries})]", - Range = new ScriptExtentAdapter(AttrAst.Extent) - }; - } - } - } - if (aliasChange.NewText == null) + if (ShouldRename(ast)) { - aliasChange = aliasChange with - { - NewText = $"[Alias(\"{OldName}\")]", - Range = new ScriptExtentAdapter(paramAst.Extent) - }; + Edits.Add(GetRenameVariableEdit(ast)); } - return aliasChange; - } - - internal TextEdit[] VisitAndGetEdits() - { - Visit(ScriptAst); - return Edits.ToArray(); + return AstVisitAction.Continue; } -} -#nullable enable -internal class Utilities -{ - public static Ast? GetAstAtPositionOfType(int StartLineNumber, int StartColumnNumber, Ast ScriptAst, params Type[] type) + private bool ShouldRename(Ast candidate) { - Ast? result = null; - result = ScriptAst.Find(ast => - { - return ast.Extent.StartLineNumber == StartLineNumber && - ast.Extent.StartColumnNumber == StartColumnNumber && - type.Contains(ast.GetType()); - }, true); - if (result == null) + if (VariableDefinition is null) { - throw new TargetSymbolNotFoundException(); + throw new InvalidOperationException("VariableDefinition should always be set by now from first Visit. This is a bug and you should file an issue."); } - return result; - } - public static Ast? GetAstParentOfType(Ast ast, params Type[] type) - { - Ast parent = ast; - // walk backwards till we hit a parent of the specified type or return null - while (null != parent) - { - if (type.Contains(parent.GetType())) - { - return parent; - } - parent = parent.Parent; - } - return null; - } + if (candidate == VariableDefinition) { return true; } + if (VariableDefinition.IsAfter(candidate)) { return false; } + if (candidate.GetTopVariableAssignment() == VariableDefinition) { return true; } - public static bool AssertContainsDotSourced(Ast ScriptAst) - { - Ast dotsourced = ScriptAst.Find(ast => - { - return ast is CommandAst commandAst && commandAst.InvocationOperator == TokenKind.Dot; - }, true); - if (dotsourced != null) - { - return true; - } return false; } - public static Ast? GetAst(int StartLineNumber, int StartColumnNumber, Ast Ast) + private TextEdit GetRenameVariableEdit(Ast ast) { - Ast? token = null; - - token = Ast.Find(ast => - { - return StartLineNumber == ast.Extent.StartLineNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - - if (token is NamedBlockAst) + return ast switch { - // NamedBlockAST starts on the same line as potentially another AST, - // its likley a user is not after the NamedBlockAst but what it contains - IEnumerable stacked_tokens = token.FindAll(ast => - { - return StartLineNumber == ast.Extent.StartLineNumber && - ast.Extent.EndColumnNumber >= StartColumnNumber - && StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - - if (stacked_tokens.Count() > 1) + VariableExpressionAst var => new TextEdit { - return stacked_tokens.LastOrDefault(); - } - - return token.Parent; - } - - if (null == token) - { - IEnumerable LineT = Ast.FindAll(ast => + NewText = '$' + NewName, + Range = new ScriptExtentAdapter(var.Extent) + }, + CommandParameterAst param => new TextEdit { - return StartLineNumber == ast.Extent.StartLineNumber && - StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - return LineT.OfType()?.LastOrDefault(); - } - - IEnumerable tokens = token.FindAll(ast => - { - return ast.Extent.EndColumnNumber >= StartColumnNumber - && StartColumnNumber >= ast.Extent.StartColumnNumber; - }, true); - if (tokens.Count() > 1) - { - token = tokens.LastOrDefault(); - } - return token; + NewText = '-' + NewName, + Range = new ScriptExtentAdapter(param.Extent) + }, + _ => throw new InvalidOperationException($"GetRenameVariableEdit was called on an Ast that was not the target. This is a bug and you should file an issue.") + }; } } - /// /// Represents a position in a script file that adapts and implicitly converts based on context. PowerShell script lines/columns start at 1, but LSP textdocument lines/columns start at 0. The default line/column constructor is 1-based. /// diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index 4a56e196e..d4836b7e9 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -12,12 +12,82 @@ namespace Microsoft.PowerShell.EditorServices.Language; public static class AstExtensions { + + internal static bool Contains(this Ast ast, Ast other) => ast.Find(ast => ast == other, true) != null; + internal static bool Contains(this Ast ast, IScriptPosition position) => new ScriptExtentAdapter(ast.Extent).Contains(position); + + internal static bool IsAfter(this Ast ast, Ast other) + { + return + ast.Extent.StartLineNumber > other.Extent.EndLineNumber + || + ( + ast.Extent.StartLineNumber == other.Extent.EndLineNumber + && ast.Extent.StartColumnNumber > other.Extent.EndColumnNumber + ); + } + + internal static bool IsBefore(this Ast ast, Ast other) + { + return + ast.Extent.EndLineNumber < other.Extent.StartLineNumber + || + ( + ast.Extent.EndLineNumber == other.Extent.StartLineNumber + && ast.Extent.EndColumnNumber < other.Extent.StartColumnNumber + ); + } + + internal static bool StartsBefore(this Ast ast, Ast other) + { + return + ast.Extent.StartLineNumber < other.Extent.StartLineNumber + || + ( + ast.Extent.StartLineNumber == other.Extent.StartLineNumber + && ast.Extent.StartColumnNumber < other.Extent.StartColumnNumber + ); + } + + internal static bool StartsAfter(this Ast ast, Ast other) + { + return + ast.Extent.StartLineNumber < other.Extent.StartLineNumber + || + ( + ast.Extent.StartLineNumber == other.Extent.StartLineNumber + && ast.Extent.StartColumnNumber < other.Extent.StartColumnNumber + ); + } + + internal static Ast? FindBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) + { + Ast? scope = crossScopeBoundaries + ? target.FindParents(typeof(ScriptBlockAst)).LastOrDefault() + : target.GetScopeBoundary(); + return scope?.Find(ast => ast.IsBefore(target) && predicate(ast), false); + } + + internal static IEnumerable FindAllBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) + { + Ast? scope = crossScopeBoundaries + ? target.FindParents(typeof(ScriptBlockAst)).LastOrDefault() + : target.GetScopeBoundary(); + return scope?.FindAll(ast => ast.IsBefore(target) && predicate(ast), false) ?? []; + } + + internal static Ast? FindAfter(this Ast target, Func predicate, bool crossScopeBoundaries = false) + => target.Parent.Find(ast => ast.IsAfter(target) && predicate(ast), crossScopeBoundaries); + + internal static IEnumerable FindAllAfter(this Ast target, Func predicate, bool crossScopeBoundaries = false) + => target.Parent.FindAll(ast => ast.IsAfter(target) && predicate(ast), crossScopeBoundaries); + /// /// Finds the most specific Ast at the given script position, or returns null if none found.
/// For example, if the position is on a variable expression within a function definition, - /// the variable will be returned even if the function definition is found first. + /// the variable will be returned even if the function definition is found first, unless variable definitions are not in the list of allowed types ///
- internal static Ast? FindAtPosition(this Ast ast, IScriptPosition position, Type[]? allowedTypes) + internal static Ast? FindClosest(this Ast ast, IScriptPosition position, Type[]? allowedTypes) { // Short circuit quickly if the position is not in the provided range, no need to traverse if not // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened. @@ -70,6 +140,12 @@ public static class AstExtensions return mostSpecificAst; } + public static bool TryFindFunctionDefinition(this Ast ast, CommandAst command, out FunctionDefinitionAst? functionDefinition) + { + functionDefinition = ast.FindFunctionDefinition(command); + return functionDefinition is not null; + } + public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command) { string? name = command.GetCommandName()?.ToLower(); @@ -115,10 +191,72 @@ public static class AstExtensions return candidateFuncDefs.LastOrDefault(); } + public static string GetUnqualifiedName(this VariableExpressionAst ast) + => ast.VariablePath.IsUnqualified + ? ast.VariablePath.ToString() + : ast.VariablePath.ToString().Split(':').Last(); + + /// + /// Finds the closest variable definition to the given reference. + /// + public static VariableExpressionAst? FindVariableDefinition(this Ast ast, Ast reference) + { + string? name = reference switch + { + VariableExpressionAst var => var.GetUnqualifiedName(), + CommandParameterAst param => param.ParameterName, + // StringConstantExpressionAst stringConstant => , + _ => null + }; + if (name is null) { return null; } + + return ast.FindAll(candidate => + { + if (candidate is not VariableExpressionAst candidateVar) { return false; } + if (candidateVar.GetUnqualifiedName() != name) { return false; } + if + ( + // TODO: Replace with a position match + candidateVar.Extent.EndLineNumber > reference.Extent.StartLineNumber + || + ( + candidateVar.Extent.EndLineNumber == reference.Extent.StartLineNumber + && candidateVar.Extent.EndColumnNumber >= reference.Extent.StartColumnNumber + ) + ) + { + return false; + } + + return candidateVar.HasParent(reference.Parent); + }, true).Cast().LastOrDefault(); + } + + public static Ast GetHighestParent(this Ast ast) + => ast.Parent is null ? ast : ast.Parent.GetHighestParent(); + + public static Ast GetHighestParent(this Ast ast, params Type[] type) + => FindParents(ast, type).LastOrDefault() ?? ast; + + /// + /// Gets the closest parent that matches the specified type or null if none found. + /// + public static T? FindParent(this Ast ast) where T : Ast + => ast.FindParent(typeof(T)) as T; + + /// + /// Gets the closest parent that matches the specified type or null if none found. + /// + public static Ast? FindParent(this Ast ast, params Type[] type) + => FindParents(ast, type).FirstOrDefault(); + + /// + /// Returns an array of parents in order from closest to furthest + /// public static Ast[] FindParents(this Ast ast, params Type[] type) { List parents = new(); - Ast parent = ast; + Ast parent = ast.Parent; while (parent is not null) { if (type.Contains(parent.GetType())) @@ -130,23 +268,311 @@ public static Ast[] FindParents(this Ast ast, params Type[] type) return parents.ToArray(); } - public static Ast GetHighestParent(this Ast ast) - => ast.Parent is null ? ast : ast.Parent.GetHighestParent(); + /// + /// Gets the closest scope boundary of the ast. + /// + public static Ast? GetScopeBoundary(this Ast ast) + => ast.FindParent + ( + typeof(ScriptBlockAst), + typeof(FunctionDefinitionAst), + typeof(ForEachStatementAst), + typeof(ForStatementAst) + ); - public static Ast GetHighestParent(this Ast ast, params Type[] type) - => FindParents(ast, type).LastOrDefault() ?? ast; + /// + /// Returns true if the Expression is part of a variable assignment + /// + /// TODO: Potentially check the name matches + public static bool IsVariableAssignment(this VariableExpressionAst var) + => var.Parent is AssignmentStatementAst or ParameterAst; + + public static bool IsOperatorAssignment(this VariableExpressionAst var) + { + if (var.Parent is AssignmentStatementAst assignast) + { + return assignast.Operator != TokenKind.Equals; + } + else + { + return true; + } + } /// - /// Gets the closest parent that matches the specified type or null if none found. + /// Returns true if the Ast is a potential variable reference /// - public static Ast? FindParent(this Ast ast, params Type[] type) - => FindParents(ast, type).FirstOrDefault(); + public static bool IsPotentialVariableReference(this Ast ast) + => ast is VariableExpressionAst or CommandParameterAst or StringConstantExpressionAst; /// - /// Gets the closest parent that matches the specified type or null if none found. + /// Determines if a variable assignment is a scoped variable assignment, meaning that it can be considered the top assignment within the current scope. This does not include Variable assignments within the body of a scope which may or may not be the top only if one of these do not exist above it in the same scope. /// - public static T? FindParent(this Ast ast) where T : Ast - => ast.FindParent(typeof(T)) as T; + // TODO: Naming is hard, I feel like this could have a better name + public static bool IsScopedVariableAssignment(this VariableExpressionAst var) + { + // foreach ($x in $y) { } + if (var.Parent is ForEachStatementAst forEachAst && forEachAst.Variable == var) + { + return true; + } + + // for ($x = 1; $x -lt 10; $x++) { } + if (var.Parent is ForStatementAst forAst && forAst.Initializer is AssignmentStatementAst assignAst && assignAst.Left == var) + { + return true; + } + + // param($x = 1) + if (var.Parent is ParameterAst paramAst && paramAst.Name == var) + { + return true; + } + + return false; + } + + /// + /// For a given string constant, determine if it is a splat, and there is at least one splat reference. If so, return a tuple of the variable assignment and the name of the splat reference. If not, return null. + /// + public static VariableExpressionAst? FindSplatVariableAssignment(this StringConstantExpressionAst stringConstantAst) + { + if (stringConstantAst.Parent is not HashtableAst hashtableAst) { return null; } + if (hashtableAst.Parent is not CommandExpressionAst commandAst) { return null; } + if (commandAst.Parent is not AssignmentStatementAst assignmentAst) { return null; } + if (assignmentAst.Left is not VariableExpressionAst leftAssignVarAst) { return null; } + return assignmentAst.FindAfter(ast => + ast is VariableExpressionAst var + && var.Splatted + && var.GetUnqualifiedName().ToLower() == leftAssignVarAst.GetUnqualifiedName().ToLower() + , true) as VariableExpressionAst; + } + + /// + /// For a given splat reference, find its source splat assignment. If the reference is not a splat, an exception will be thrown. If no assignment is found, null will be returned. + /// TODO: Support incremental splat references e.g. $x = @{}, $x.Method = 'GET' + /// + public static StringConstantExpressionAst? FindSplatAssignmentReference(this VariableExpressionAst varAst) + { + if (!varAst.Splatted) { throw new InvalidOperationException("The provided variable reference is not a splat and cannot be used with FindSplatVariableAssignment"); } + + return varAst.FindBefore(ast => + ast is StringConstantExpressionAst stringAst + && stringAst.Value == varAst.GetUnqualifiedName() + && stringAst.FindSplatVariableAssignment() == varAst, + crossScopeBoundaries: true) as StringConstantExpressionAst; + } + + /// + /// Returns the function a parameter is defined in. Returns null if it is an anonymous function such as a scriptblock + /// + public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionAst? function) + { + if (ast.Parent is FunctionDefinitionAst funcDef) { function = funcDef; return true; } + if (ast.Parent.Parent is FunctionDefinitionAst paramBlockFuncDef) { function = paramBlockFuncDef; return true; } + function = null; + return false; + } + + + /// + /// Finds the highest variable expression within a variable assignment within the current scope of the provided variable reference. Returns the original object if it is the highest assignment or null if no assignment was found. It is assumed the reference is part of a larger Ast. + /// + /// A variable reference that is either a VariableExpression or a StringConstantExpression (splatting reference) + public static Ast? GetTopVariableAssignment(this Ast reference) + { + if (!reference.IsPotentialVariableReference()) + { + throw new NotSupportedException("The provided reference is not a variable reference type."); + } + + // Splats are special, we will treat them as a top variable assignment and search both above for a parameter assignment and below for a splat reference, but we don't require a command definition within the same scope for the splat. + if (reference is StringConstantExpressionAst stringConstant) + { + VariableExpressionAst? splat = stringConstant.FindSplatVariableAssignment(); + if (splat is not null) + { + return reference; + } + } + + // If nothing found, search parent scopes for a variable assignment until we hit the top of the document + string name = reference switch + { + VariableExpressionAst varExpression => varExpression.GetUnqualifiedName(), + CommandParameterAst param => param.ParameterName, + StringConstantExpressionAst stringConstantExpressionAst => stringConstantExpressionAst.Value, + _ => throw new NotSupportedException("The provided reference is not a variable reference type.") + }; + + Ast? scope = reference.GetScopeBoundary(); + + VariableExpressionAst? varAssignment = null; + + while (scope is not null) + { + // Check if the reference is a parameter in the current scope. This saves us from having to do a nested search later on. + // TODO: Can probably be combined with below + IEnumerable? parameters = scope switch + { + // Covers both function test() { param($x) } and function param($x) + FunctionDefinitionAst f => f.Body?.ParamBlock?.Parameters ?? f.Parameters, + ScriptBlockAst s => s.ParamBlock?.Parameters, + _ => null + }; + ParameterAst? matchParam = parameters?.SingleOrDefault( + param => param.Name.GetUnqualifiedName().ToLower() == name.ToLower() + ); + if (matchParam is not null) + { + return matchParam.Name; + } + + // Find any top level function definitions in the currentscope that might match the parameter + // TODO: This could be less complicated + if (reference is CommandParameterAst parameterAst) + { + FunctionDefinitionAst? closestFunctionMatch = scope.FindAll( + ast => ast is FunctionDefinitionAst funcDef + && funcDef.Name.ToLower() == (parameterAst.Parent as CommandAst)?.GetCommandName()?.ToLower() + && (funcDef.Parameters ?? funcDef.Body.ParamBlock.Parameters).SingleOrDefault( + param => param.Name.GetUnqualifiedName().ToLower() == name.ToLower() + ) is not null + , false + ).LastOrDefault() as FunctionDefinitionAst; + + if (closestFunctionMatch is not null) + { + //TODO: This should not ever be null but should probably be sure. + return + (closestFunctionMatch.Parameters ?? closestFunctionMatch.Body.ParamBlock.Parameters) + .SingleOrDefault + ( + param => param.Name.GetUnqualifiedName().ToLower() == name.ToLower() + )?.Name; + }; + }; + + // Will find the outermost assignment that matches the reference. + varAssignment = reference switch + { + VariableExpressionAst var => scope.Find + ( + ast => ast is VariableExpressionAst var + && ast.IsBefore(reference) + && + ( + (var.IsVariableAssignment() && !var.IsOperatorAssignment()) + || var.IsScopedVariableAssignment() + ) + && var.GetUnqualifiedName().ToLower() == name.ToLower() + , searchNestedScriptBlocks: false + ) as VariableExpressionAst, + + CommandParameterAst param => scope.Find + ( + ast => ast is VariableExpressionAst var + && ast.IsBefore(reference) + && var.GetUnqualifiedName().ToLower() == name.ToLower() + && var.Parent is ParameterAst paramAst + && paramAst.TryGetFunction(out FunctionDefinitionAst? foundFunction) + && foundFunction?.Name.ToLower() + == (param.Parent as CommandAst)?.GetCommandName()?.ToLower() + && foundFunction?.Parent?.Parent == scope + , searchNestedScriptBlocks: true //This might hit side scopes... + ) as VariableExpressionAst, + _ => null + }; + + if (varAssignment is not null) + { + return varAssignment; + } + + if (reference is VariableExpressionAst varAst + && + ( + varAst.IsScopedVariableAssignment() + || (varAst.IsVariableAssignment() && !varAst.IsOperatorAssignment()) + ) + ) + { + // The current variable reference is the top level assignment because we didn't find any other assignments above it + return reference; + } + + // Get the next highest scope + scope = scope.GetScopeBoundary(); + } + + // If we make it this far we didn't find any references. + + // An operator assignment can be a definition only as long as there are no assignments above it in all scopes. + if (reference is VariableExpressionAst variableAst + && variableAst.IsVariableAssignment() + && variableAst.IsOperatorAssignment()) + { + return reference; + } + + return null; + } + + public static bool WithinScope(this Ast Target, Ast Child) + { + Ast childParent = Child.Parent; + Ast? TargetScope = Target.GetScopeBoundary(); + while (childParent != null) + { + if (childParent is FunctionDefinitionAst FuncDefAst) + { + if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) + { + + } + else + { + break; + } + } + if (childParent == TargetScope) + { + break; + } + childParent = childParent.Parent; + } + return childParent == TargetScope; + } + + public static bool IsVariableExpressionAssignedInTargetScope(this VariableExpressionAst node, Ast scope) + { + bool r = false; + + List VariableAssignments = node.FindAll(ast => + { + return ast is VariableExpressionAst VarDef && + VarDef.Parent is AssignmentStatementAst or ParameterAst && + VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && + // Look Backwards from the node above + (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || + (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && + VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && + // Must be within the the designated scope + VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; + }, true).Cast().ToList(); + + if (VariableAssignments.Count > 0) + { + r = true; + } + // Node is probably the first Assignment Statement within scope + if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) + { + r = true; + } + + return r; + } public static bool HasParent(this Ast ast, Ast parent) { diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/ParameterUndefinedFunction.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/ParameterUndefinedFunction.ps1 new file mode 100644 index 000000000..a07d73e79 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/ParameterUndefinedFunction.ps1 @@ -0,0 +1 @@ +FunctionThatIsNotDefinedInThisScope -TestParameter 'test' diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs index f0d35214f..e3c02f4e6 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs @@ -18,7 +18,7 @@ public class RefactorVariableTestCases new ("VariableInForloopDuplicateAssignment.ps1", Line: 9, Column: 14), new ("VariableInLoop.ps1", Line: 1, Column: 1), new ("VariableInParam.ps1", Line: 24, Column: 16), - new ("VariableInPipeline.ps1", Line: 2, Column: 23), + new ("VariableInPipeline.ps1", Line: 3, Column: 23), new ("VariableInScriptblockScoped.ps1", Line: 2, Column: 16), new ("VariableNestedFunctionScriptblock.ps1", Line: 4, Column: 20), new ("VariableNestedScopeFunction.ps1", Line: 1, Column: 1), @@ -31,6 +31,7 @@ public class RefactorVariableTestCases new ("VariableusedInWhileLoop.ps1", Line: 2, Column: 5), new ("VariableWithinCommandAstScriptBlock.ps1", Line: 3, Column: 75), new ("VariableWithinForeachObject.ps1", Line: 2, Column: 1), - new ("VariableWithinHastableExpression.ps1", Line: 3, Column: 46), + new ("VariableWithinHastableExpression.ps1", Line: 3, Column: 46), + new ("ParameterUndefinedFunction.ps1", Line: 1, Column: 39, ShouldFail: true), ]; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 index 036a9b108..220a984b7 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipeline.ps1 @@ -1,3 +1,4 @@ +$oldVarName = 5 1..10 | Where-Object { $_ -le $oldVarName } | Write-Output diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 index 34af48896..dea826fbf 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableInPipelineRenamed.ps1 @@ -1,3 +1,4 @@ +$Renamed = 5 1..10 | Where-Object { $_ -le $Renamed } | Write-Output diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 55fab99b6..99b92c75b 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -98,9 +98,9 @@ public async Task FindsVariable(RenameTestTarget s) { result = await testHandler.Handle(testParams, CancellationToken.None); } - catch (HandlerErrorException) + catch (HandlerErrorException err) { - Assert.True(s.ShouldFail); + Assert.True(s.ShouldFail, err.Message); return; } if (s.ShouldFail) diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index d22e35c26..9c00253c7 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -98,9 +98,9 @@ public async void RenamedVariable(RenameTestTarget s) { response = await testHandler.Handle(request, CancellationToken.None); } - catch (HandlerErrorException) + catch (HandlerErrorException err) { - Assert.True(s.ShouldFail); + Assert.True(s.ShouldFail, $"Shouldfail is {s.ShouldFail} and error is {err.Message}"); return; } if (s.ShouldFail) From 81bd16eb2e82409d20b65150257e34b17f82262b Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sun, 29 Sep 2024 09:47:10 -0700 Subject: [PATCH 192/203] Fix up Splat function finding, all tests pass --- .../Services/TextDocument/RenameService.cs | 5 ++ .../Utility/AstExtensions.cs | 80 +++++++++++++------ 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 7efa09665..b92aeac1d 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -481,6 +481,11 @@ private TextEdit GetRenameVariableEdit(Ast ast) NewText = '-' + NewName, Range = new ScriptExtentAdapter(param.Extent) }, + StringConstantExpressionAst stringAst => new TextEdit + { + NewText = NewName, + Range = new ScriptExtentAdapter(stringAst.Extent) + }, _ => throw new InvalidOperationException($"GetRenameVariableEdit was called on an Ast that was not the target. This is a bug and you should file an issue.") }; } diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index d4836b7e9..fdcdde10f 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -280,6 +280,37 @@ public static Ast[] FindParents(this Ast ast, params Type[] type) typeof(ForStatementAst) ); + public static VariableExpressionAst? FindClosestParameterInFunction(this Ast target, string functionName, string parameterName) + { + Ast? scope = target.GetScopeBoundary(); + while (scope is not null) + { + FunctionDefinitionAst? funcDef = scope.FindAll + ( + ast => ast is FunctionDefinitionAst funcDef + && funcDef.StartsBefore(target) + && funcDef.Name.ToLower() == functionName.ToLower() + && (funcDef.Parameters ?? funcDef.Body.ParamBlock.Parameters) + .SingleOrDefault( + param => param.Name.GetUnqualifiedName().ToLower() == parameterName.ToLower() + ) is not null + , false + ).LastOrDefault() as FunctionDefinitionAst; + + if (funcDef is not null) + { + return (funcDef.Parameters ?? funcDef.Body.ParamBlock.Parameters) + .SingleOrDefault + ( + param => param.Name.GetUnqualifiedName().ToLower() == parameterName.ToLower() + )?.Name; //Should not be null at this point + } + + scope = scope.GetScopeBoundary(); + } + return null; + } + /// /// Returns true if the Expression is part of a variable assignment /// @@ -333,9 +364,9 @@ public static bool IsScopedVariableAssignment(this VariableExpressionAst var) } /// - /// For a given string constant, determine if it is a splat, and there is at least one splat reference. If so, return a tuple of the variable assignment and the name of the splat reference. If not, return null. + /// For a given string constant, determine if it is a splat, and there is at least one splat reference. If so, return the location of the splat assignment. /// - public static VariableExpressionAst? FindSplatVariableAssignment(this StringConstantExpressionAst stringConstantAst) + public static VariableExpressionAst? FindSplatParameterReference(this StringConstantExpressionAst stringConstantAst) { if (stringConstantAst.Parent is not HashtableAst hashtableAst) { return null; } if (hashtableAst.Parent is not CommandExpressionAst commandAst) { return null; } @@ -359,7 +390,7 @@ ast is VariableExpressionAst var return varAst.FindBefore(ast => ast is StringConstantExpressionAst stringAst && stringAst.Value == varAst.GetUnqualifiedName() - && stringAst.FindSplatVariableAssignment() == varAst, + && stringAst.FindSplatParameterReference() == varAst, crossScopeBoundaries: true) as StringConstantExpressionAst; } @@ -389,11 +420,18 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA // Splats are special, we will treat them as a top variable assignment and search both above for a parameter assignment and below for a splat reference, but we don't require a command definition within the same scope for the splat. if (reference is StringConstantExpressionAst stringConstant) { - VariableExpressionAst? splat = stringConstant.FindSplatVariableAssignment(); - if (splat is not null) + VariableExpressionAst? splat = stringConstant.FindSplatParameterReference(); + if (splat is null) { return null; } + // Find the function associated with the splat parameter reference + string? commandName = (splat.Parent as CommandAst)?.GetCommandName().ToLower(); + if (commandName is null) { return null; } + VariableExpressionAst? splatParamReference = splat.FindClosestParameterInFunction(commandName, stringConstant.Value); + + if (splatParamReference is not null) { - return reference; + return splatParamReference; } + } // If nothing found, search parent scopes for a variable assignment until we hit the top of the document @@ -432,27 +470,17 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA // TODO: This could be less complicated if (reference is CommandParameterAst parameterAst) { - FunctionDefinitionAst? closestFunctionMatch = scope.FindAll( - ast => ast is FunctionDefinitionAst funcDef - && funcDef.Name.ToLower() == (parameterAst.Parent as CommandAst)?.GetCommandName()?.ToLower() - && (funcDef.Parameters ?? funcDef.Body.ParamBlock.Parameters).SingleOrDefault( - param => param.Name.GetUnqualifiedName().ToLower() == name.ToLower() - ) is not null - , false - ).LastOrDefault() as FunctionDefinitionAst; - - if (closestFunctionMatch is not null) - { - //TODO: This should not ever be null but should probably be sure. - return - (closestFunctionMatch.Parameters ?? closestFunctionMatch.Body.ParamBlock.Parameters) - .SingleOrDefault - ( - param => param.Name.GetUnqualifiedName().ToLower() == name.ToLower() - )?.Name; - }; - }; + string? commandName = (parameterAst.Parent as CommandAst)?.GetCommandName()?.ToLower(); + if (commandName is not null) + { + VariableExpressionAst? paramDefinition = parameterAst.FindClosestParameterInFunction(commandName, parameterAst.ParameterName); + if (paramDefinition is not null) + { + return paramDefinition; + } + } + } // Will find the outermost assignment that matches the reference. varAssignment = reference switch { From 26fe7d99cfa71e7eb5cbf8c39bf196a3bf1757a2 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 30 Sep 2024 12:58:54 -0700 Subject: [PATCH 193/203] Clean out some dead code --- .../Services/TextDocument/RenameService.cs | 7 -- .../Utility/AstExtensions.cs | 93 ------------------- 2 files changed, 100 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index b92aeac1d..c17c98e6e 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -132,13 +132,6 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam throw new HandlerErrorException($"Asked to rename a variable but the target is not a viable variable type: {symbol.GetType()}. This is a bug, file an issue if you see this."); } - // RenameVariableVisitor visitor = new( - // requestParams.NewName, - // symbol.Extent.StartLineNumber, - // symbol.Extent.StartColumnNumber, - // scriptAst, - // createParameterAlias - // ); NewRenameVariableVisitor visitor = new( symbol, requestParams.NewName ); diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index fdcdde10f..8737cb645 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -196,42 +196,6 @@ public static string GetUnqualifiedName(this VariableExpressionAst ast) ? ast.VariablePath.ToString() : ast.VariablePath.ToString().Split(':').Last(); - /// - /// Finds the closest variable definition to the given reference. - /// - public static VariableExpressionAst? FindVariableDefinition(this Ast ast, Ast reference) - { - string? name = reference switch - { - VariableExpressionAst var => var.GetUnqualifiedName(), - CommandParameterAst param => param.ParameterName, - // StringConstantExpressionAst stringConstant => , - _ => null - }; - if (name is null) { return null; } - - return ast.FindAll(candidate => - { - if (candidate is not VariableExpressionAst candidateVar) { return false; } - if (candidateVar.GetUnqualifiedName() != name) { return false; } - if - ( - // TODO: Replace with a position match - candidateVar.Extent.EndLineNumber > reference.Extent.StartLineNumber - || - ( - candidateVar.Extent.EndLineNumber == reference.Extent.StartLineNumber - && candidateVar.Extent.EndColumnNumber >= reference.Extent.StartColumnNumber - ) - ) - { - return false; - } - - return candidateVar.HasParent(reference.Parent); - }, true).Cast().LastOrDefault(); - } - public static Ast GetHighestParent(this Ast ast) => ast.Parent is null ? ast : ast.Parent.GetHighestParent(); @@ -405,7 +369,6 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA return false; } - /// /// Finds the highest variable expression within a variable assignment within the current scope of the provided variable reference. Returns the original object if it is the highest assignment or null if no assignment was found. It is assumed the reference is part of a larger Ast. /// @@ -546,62 +509,6 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA return null; } - public static bool WithinScope(this Ast Target, Ast Child) - { - Ast childParent = Child.Parent; - Ast? TargetScope = Target.GetScopeBoundary(); - while (childParent != null) - { - if (childParent is FunctionDefinitionAst FuncDefAst) - { - if (Child is VariableExpressionAst VarExpAst && !IsVariableExpressionAssignedInTargetScope(VarExpAst, FuncDefAst)) - { - - } - else - { - break; - } - } - if (childParent == TargetScope) - { - break; - } - childParent = childParent.Parent; - } - return childParent == TargetScope; - } - - public static bool IsVariableExpressionAssignedInTargetScope(this VariableExpressionAst node, Ast scope) - { - bool r = false; - - List VariableAssignments = node.FindAll(ast => - { - return ast is VariableExpressionAst VarDef && - VarDef.Parent is AssignmentStatementAst or ParameterAst && - VarDef.VariablePath.UserPath.ToLower() == node.VariablePath.UserPath.ToLower() && - // Look Backwards from the node above - (VarDef.Extent.EndLineNumber < node.Extent.StartLineNumber || - (VarDef.Extent.EndColumnNumber <= node.Extent.StartColumnNumber && - VarDef.Extent.EndLineNumber <= node.Extent.StartLineNumber)) && - // Must be within the the designated scope - VarDef.Extent.StartLineNumber >= scope.Extent.StartLineNumber; - }, true).Cast().ToList(); - - if (VariableAssignments.Count > 0) - { - r = true; - } - // Node is probably the first Assignment Statement within scope - if (node.Parent is AssignmentStatementAst && node.Extent.StartLineNumber >= scope.Extent.StartLineNumber) - { - r = true; - } - - return r; - } - public static bool HasParent(this Ast ast, Ast parent) { Ast? current = ast; From 1042ab84c698151fd210dfa91b312ca946289b11 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 30 Sep 2024 16:56:25 -0700 Subject: [PATCH 194/203] Add name validation and related tests, also rearrange/rename some test fixtures --- .../Services/TextDocument/RenameService.cs | 138 +++++++++++------- .../Utility/AstExtensions.cs | 15 ++ ...FunctionsSingle.ps1 => FunctionSimple.ps1} | 0 ...eRenamed.ps1 => FunctionSimpleRenamed.ps1} | 0 ...Cases.cs => _RefactorFunctionTestCases.cs} | 5 +- .../Refactoring/RenameTestTarget.cs | 11 +- ...nment.ps1 => VariableSimpleAssignment.ps1} | 0 ...s1 => VariableSimpleAssignmentRenamed.ps1} | 0 ...Cases.cs => _RefactorVariableTestCases.cs} | 9 +- .../Refactoring/PrepareRenameHandlerTests.cs | 11 +- .../Refactoring/RefactorUtilities.cs | 57 ++------ .../Refactoring/RenameHandlerTests.cs | 6 +- 12 files changed, 135 insertions(+), 117 deletions(-) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{FunctionsSingle.ps1 => FunctionSimple.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{FunctionsSingleRenamed.ps1 => FunctionSimpleRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/{RefactorFunctionTestCases.cs => _RefactorFunctionTestCases.cs} (78%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{SimpleVariableAssignment.ps1 => VariableSimpleAssignment.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{SimpleVariableAssignmentRenamed.ps1 => VariableSimpleAssignmentRenamed.ps1} (100%) rename test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/{RefactorVariableTestCases.cs => _RefactorVariableTestCases.cs} (83%) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index c17c98e6e..beed37923 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -69,6 +69,7 @@ ILanguageServerConfiguration config // Since LSP 3.16 we can simply basically return a DefaultBehavior true or null to signal to the client that the position is valid for rename and it should use its default selection criteria (which is probably the language semantic highlighting or grammar). For the current scope of the rename provider, this should be fine, but we have the option to supply the specific range in the future for special cases. RangeOrPlaceholderRange renameSupported = new(new RenameDefaultBehavior() { DefaultBehavior = true }); + return (renameResponse?.Changes?[request.TextDocument.Uri].ToArray().Length > 0) ? renameSupported : null; @@ -95,9 +96,8 @@ or CommandAst => RenameFunction(tokenToRename, scriptFile.ScriptAst, request), VariableExpressionAst - or ParameterAst or CommandParameterAst - or AssignmentStatementAst + or StringConstantExpressionAst => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createParameterAlias), _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") @@ -114,24 +114,14 @@ or AssignmentStatementAst // TODO: We can probably merge these two methods with Generic Type constraints since they are factored into overloading - internal static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParams renameParams) + private static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParams renameParams) { - if (target is not (FunctionDefinitionAst or CommandAst)) - { - throw new HandlerErrorException($"Asked to rename a function but the target is not a viable function type: {target.GetType()}. This is a bug, file an issue if you see this."); - } - RenameFunctionVisitor visitor = new(target, renameParams.NewName); return visitor.VisitAndGetEdits(scriptAst); } - internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createParameterAlias) + private static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createParameterAlias) { - if (symbol is not (VariableExpressionAst or ParameterAst or CommandParameterAst or StringConstantExpressionAst)) - { - throw new HandlerErrorException($"Asked to rename a variable but the target is not a viable variable type: {symbol.GetType()}. This is a bug, file an issue if you see this."); - } - NewRenameVariableVisitor visitor = new( symbol, requestParams.NewName ); @@ -144,22 +134,33 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam /// Ast of the token or null if no renamable symbol was found internal static Ast? FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) { - Ast? ast = scriptFile.ScriptAst.FindClosest(position, - [ + List renameableAstTypes = [ // Functions typeof(FunctionDefinitionAst), typeof(CommandAst), // Variables typeof(VariableExpressionAst), - typeof(CommandParameterAst) - // FIXME: Splat parameter in hashtable - ]); + typeof(CommandParameterAst), + typeof(StringConstantExpressionAst) + ]; + Ast? ast = scriptFile.ScriptAst.FindClosest(position, renameableAstTypes.ToArray()); + + if (ast is StringConstantExpressionAst stringAst) + { + // Only splat string parameters should be considered for evaluation. + if (stringAst.FindSplatParameterReference() is not null) { return stringAst; } + // Otherwise redo the search without stringConstant, so the most specific is a command, etc. + renameableAstTypes.Remove(typeof(StringConstantExpressionAst)); + ast = scriptFile.ScriptAst.FindClosest(position, renameableAstTypes.ToArray()); + } + + // Performance optimizations // Only the function name is valid for rename, not other components if (ast is FunctionDefinitionAst funcDefAst) { - if (!GetFunctionNameExtent(funcDefAst).Contains(position)) + if (!funcDefAst.GetFunctionNameExtent().Contains(position)) { return null; } @@ -179,23 +180,9 @@ internal static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParam } } - return ast; - } - /// - /// Return an extent that only contains the position of the name of the function, for Client highlighting purposes. - /// - internal static ScriptExtentAdapter GetFunctionNameExtent(FunctionDefinitionAst ast) - { - string name = ast.Name; - // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present - int funcLength = "function ".Length; - ScriptExtentAdapter funcExtent = new(ast.Extent); - funcExtent.Start = funcExtent.Start.Delta(0, funcLength); - funcExtent.End = funcExtent.Start.Delta(0, name.Length); - - return funcExtent; + return ast; } /// @@ -322,7 +309,7 @@ internal AstVisitAction Visit(Ast ast) { FunctionDefinitionAst f => f, CommandAst command => CurrentDocument.FindFunctionDefinition(command) - ?? throw new HandlerErrorException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within the same scope"), + ?? throw new HandlerErrorException("The command to rename does not have a function definition. Renaming a function is only supported when the function is defined within an accessible scope"), _ => throw new Exception($"Unsupported AST type {target.GetType()} encountered") }; }; @@ -373,7 +360,12 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) throw new InvalidOperationException("GetRenameFunctionEdit was called on an Ast that was not the target. This is a bug and you should file an issue."); } - ScriptExtentAdapter functionNameExtent = RenameService.GetFunctionNameExtent(funcDef); + if (!IsValidFunctionName(newName)) + { + throw new HandlerErrorException($"{newName} is not a valid function name."); + } + + ScriptExtentAdapter functionNameExtent = funcDef.GetFunctionNameExtent(); return new TextEdit() { @@ -399,6 +391,19 @@ private TextEdit GetRenameFunctionEdit(Ast candidate) Range = new ScriptExtentAdapter(funcName.Extent) }; } + + internal static bool IsValidFunctionName(string name) + { + // Allows us to supply function:varname or varname and get a proper result + string candidate = "function " + name.TrimStart('$').TrimStart('-') + " {}"; + Parser.ParseInput(candidate, out Token[] tokens, out _); + return tokens.Length == 5 + && tokens[0].Kind == TokenKind.Function + && tokens[1].Kind == TokenKind.Identifier + && tokens[2].Kind == TokenKind.LCurly + && tokens[3].Kind == TokenKind.RCurly + && tokens[4].Kind == TokenKind.EndOfInput; + } } internal class NewRenameVariableVisitor(Ast target, string newName, bool skipVerify = false) : RenameVisitorBase @@ -430,7 +435,7 @@ internal AstVisitAction Visit(Ast ast) VariableDefinition = target.GetTopVariableAssignment(); if (VariableDefinition is null) { - throw new HandlerErrorException("The element to rename does not have a definition. Renaming an element is only supported when the element is defined within the same scope"); + throw new HandlerErrorException("The variable element to rename does not have a definition. Renaming an element is only supported when the variable element is defined within an accessible scope"); } } else if (CurrentDocument != ast.GetHighestParent()) @@ -464,24 +469,51 @@ private TextEdit GetRenameVariableEdit(Ast ast) { return ast switch { - VariableExpressionAst var => new TextEdit - { - NewText = '$' + NewName, - Range = new ScriptExtentAdapter(var.Extent) - }, - CommandParameterAst param => new TextEdit - { - NewText = '-' + NewName, - Range = new ScriptExtentAdapter(param.Extent) - }, - StringConstantExpressionAst stringAst => new TextEdit - { - NewText = NewName, - Range = new ScriptExtentAdapter(stringAst.Extent) - }, + VariableExpressionAst var => !IsValidVariableName(NewName) + ? throw new HandlerErrorException($"${NewName} is not a valid variable name.") + : new TextEdit + { + NewText = '$' + NewName, + Range = new ScriptExtentAdapter(var.Extent) + }, + StringConstantExpressionAst stringAst => !IsValidVariableName(NewName) + ? throw new Exception($"{NewName} is not a valid variable name.") + : new TextEdit + { + NewText = NewName, + Range = new ScriptExtentAdapter(stringAst.Extent) + }, + CommandParameterAst param => !IsValidCommandParameterName(NewName) + ? throw new Exception($"-{NewName} is not a valid command parameter name.") + : new TextEdit + { + NewText = '-' + NewName, + Range = new ScriptExtentAdapter(param.Extent) + }, _ => throw new InvalidOperationException($"GetRenameVariableEdit was called on an Ast that was not the target. This is a bug and you should file an issue.") }; } + + internal static bool IsValidVariableName(string name) + { + // Allows us to supply $varname or varname and get a proper result + string candidate = '$' + name.TrimStart('$').TrimStart('-'); + Parser.ParseInput(candidate, out Token[] tokens, out _); + return tokens.Length is 2 + && tokens[0].Kind == TokenKind.Variable + && tokens[1].Kind == TokenKind.EndOfInput; + } + + internal static bool IsValidCommandParameterName(string name) + { + // Allows us to supply -varname or varname and get a proper result + string candidate = "Command -" + name.TrimStart('$').TrimStart('-'); + Parser.ParseInput(candidate, out Token[] tokens, out _); + return tokens.Length == 3 + && tokens[0].Kind == TokenKind.Command + && tokens[1].Kind == TokenKind.Parameter + && tokens[2].Kind == TokenKind.EndOfInput; + } } /// diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index 8737cb645..0073b4c08 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -523,4 +523,19 @@ public static bool HasParent(this Ast ast, Ast parent) return false; } + + /// + /// Return an extent that only contains the position of the name of the function, for Client highlighting purposes. + /// + internal static ScriptExtentAdapter GetFunctionNameExtent(this FunctionDefinitionAst ast) + { + string name = ast.Name; + // FIXME: Gather dynamically from the AST and include backticks and whatnot that might be present + int funcLength = "function ".Length; + ScriptExtentAdapter funcExtent = new(ast.Extent); + funcExtent.Start = funcExtent.Start.Delta(0, funcLength); + funcExtent.End = funcExtent.Start.Delta(0, name.Length); + + return funcExtent; + } } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingle.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSimple.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingle.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSimple.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingleRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSimpleRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionsSingleRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/FunctionSimpleRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/_RefactorFunctionTestCases.cs similarity index 78% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/_RefactorFunctionTestCases.cs index 3583c631f..d57a5aede 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/RefactorFunctionTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Functions/_RefactorFunctionTestCases.cs @@ -10,6 +10,10 @@ public class RefactorFunctionTestCases /// public static RenameTestTarget[] TestCases = [ + new("FunctionSimple.ps1", Line: 1, Column: 11 ), + new("FunctionSimple.ps1", Line: 1, Column: 1, NoResult: true ), + new("FunctionSimple.ps1", Line: 2, Column: 4, NoResult: true ), + new("FunctionSimple.ps1", Line: 1, Column: 11, NewName: "Bad Name", ShouldThrow: true ), new("FunctionCallWIthinStringExpression.ps1", Line: 1, Column: 10 ), new("FunctionCmdlet.ps1", Line: 1, Column: 10 ), new("FunctionForeach.ps1", Line: 11, Column: 5 ), @@ -21,7 +25,6 @@ public class RefactorFunctionTestCases new("FunctionOuterHasNestedFunction.ps1", Line: 1, Column: 10 ), new("FunctionSameName.ps1", Line: 3, Column: 14 ), new("FunctionScriptblock.ps1", Line: 5, Column: 5 ), - new("FunctionsSingle.ps1", Line: 1, Column: 11 ), new("FunctionWithInnerFunction.ps1", Line: 5, Column: 5 ), new("FunctionWithInternalCalls.ps1", Line: 3, Column: 6 ), ]; diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs index 5c8c48d5f..25c0e3d7d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/RenameTestTarget.cs @@ -28,21 +28,24 @@ public class RenameTestTarget public string NewName = "Renamed"; public bool ShouldFail; + public bool ShouldThrow; /// The test case file name e.g. testScript.ps1 /// The line where the cursor should be positioned for the rename /// The column/character indent where ther cursor should be positioned for the rename /// What the target symbol represented by the line and column should be renamed to. Defaults to "Renamed" if not specified - /// This test case should not succeed and return either null or a handler error - public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed", bool ShouldFail = false) + /// This test case should return null (cannot be renamed) + /// This test case should throw a HandlerErrorException meaning user needs to be alerted in a custom way + public RenameTestTarget(string FileName, int Line, int Column, string NewName = "Renamed", bool NoResult = false, bool ShouldThrow = false) { this.FileName = FileName; this.Line = Line; this.Column = Column; this.NewName = NewName; - this.ShouldFail = ShouldFail; + this.ShouldFail = NoResult; + this.ShouldThrow = ShouldThrow; } public RenameTestTarget() { } - public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)} {Line}:{Column} N:{NewName} F:{ShouldFail}"; + public override string ToString() => $"{FileName.Substring(0, FileName.Length - 4)} {Line}:{Column} N:{NewName} F:{ShouldFail} T:{ShouldThrow}"; } diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleAssignment.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignment.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleAssignment.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleAssignmentRenamed.ps1 similarity index 100% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/SimpleVariableAssignmentRenamed.ps1 rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableSimpleAssignmentRenamed.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs similarity index 83% rename from test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs rename to test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs index e3c02f4e6..fdfb2c174 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs @@ -5,9 +5,11 @@ public class RefactorVariableTestCases { public static RenameTestTarget[] TestCases = [ - new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1, NewName: "$Renamed"), - new ("SimpleVariableAssignment.ps1", Line: 1, Column: 1), - new ("SimpleVariableAssignment.ps1", Line: 2, Column: 1, NewName: "Wrong", ShouldFail: true), + new ("VariableSimpleAssignment.ps1", Line: 1, Column: 1), + new ("VariableSimpleAssignment.ps1", Line: 1, Column: 1, NewName: "$Renamed"), + new ("VariableSimpleAssignment.ps1", Line: 1, Column: 1, NewName: "$Bad Name", ShouldThrow: true), + new ("VariableSimpleAssignment.ps1", Line: 1, Column: 1, NewName: "Bad Name", ShouldThrow: true), + new ("VariableSimpleAssignment.ps1", Line: 1, Column: 6, NoResult: true), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), new ("VariableCommandParameter.ps1", Line: 10, Column: 10), new ("VariableCommandParameterSplatted.ps1", Line: 3, Column: 19 ), @@ -32,6 +34,5 @@ public class RefactorVariableTestCases new ("VariableWithinCommandAstScriptBlock.ps1", Line: 3, Column: 75), new ("VariableWithinForeachObject.ps1", Line: 2, Column: 1), new ("VariableWithinHastableExpression.ps1", Line: 3, Column: 46), - new ("ParameterUndefinedFunction.ps1", Line: 1, Column: 39, ShouldFail: true), ]; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs index 99b92c75b..4986212b9 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/PrepareRenameHandlerTests.cs @@ -72,9 +72,9 @@ public async Task FindsFunction(RenameTestTarget s) { result = await testHandler.Handle(testParams, CancellationToken.None); } - catch (HandlerErrorException) + catch (HandlerErrorException err) { - Assert.True(s.ShouldFail); + Assert.True(s.ShouldThrow, $"Unexpected HandlerErrorException: {err.Message}"); return; } if (s.ShouldFail) @@ -100,7 +100,7 @@ public async Task FindsVariable(RenameTestTarget s) } catch (HandlerErrorException err) { - Assert.True(s.ShouldFail, err.Message); + Assert.True(s.ShouldThrow, $"Unexpected HandlerErrorException: {err.Message}"); return; } if (s.ShouldFail) @@ -213,6 +213,7 @@ public void Serialize(IXunitSerializationInfo info) info.AddValue(nameof(Column), Column); info.AddValue(nameof(NewName), NewName); info.AddValue(nameof(ShouldFail), ShouldFail); + info.AddValue(nameof(ShouldThrow), ShouldThrow); } public void Deserialize(IXunitSerializationInfo info) @@ -222,6 +223,7 @@ public void Deserialize(IXunitSerializationInfo info) Column = info.GetValue(nameof(Column)); NewName = info.GetValue(nameof(NewName)); ShouldFail = info.GetValue(nameof(ShouldFail)); + ShouldThrow = info.GetValue(nameof(ShouldThrow)); } public static RenameTestTargetSerializable FromRenameTestTarget(RenameTestTarget t) @@ -231,6 +233,7 @@ public static RenameTestTargetSerializable FromRenameTestTarget(RenameTestTarget Column = t.Column, Line = t.Line, NewName = t.NewName, - ShouldFail = t.ShouldFail + ShouldFail = t.ShouldFail, + ShouldThrow = t.ShouldThrow }; } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs index 6338d5fcf..288b7b83b 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RefactorUtilities.cs @@ -9,16 +9,6 @@ namespace PowerShellEditorServices.Test.Refactoring { - internal class TextEditComparer : IComparer - { - public int Compare(TextEdit a, TextEdit b) - { - return a.Range.Start.Line == b.Range.Start.Line - ? b.Range.End.Character - a.Range.End.Character - : b.Range.Start.Line - a.Range.Start.Line; - } - } - public class RefactorUtilities { /// @@ -50,44 +40,15 @@ internal static string GetModifiedScript(string OriginalScript, TextEdit[] Modif return string.Join(Environment.NewLine, Lines); } + } - // public class RenameSymbolParamsSerialized : IRequest, IXunitSerializable - // { - // public string FileName { get; set; } - // public int Line { get; set; } - // public int Column { get; set; } - // public string RenameTo { get; set; } - - // // Default constructor needed for deserialization - // public RenameSymbolParamsSerialized() { } - - // // Parameterized constructor for convenience - // public RenameSymbolParamsSerialized(RenameSymbolParams RenameSymbolParams) - // { - // FileName = RenameSymbolParams.FileName; - // Line = RenameSymbolParams.Line; - // Column = RenameSymbolParams.Column; - // RenameTo = RenameSymbolParams.RenameTo; - // } - - // public void Deserialize(IXunitSerializationInfo info) - // { - // FileName = info.GetValue("FileName"); - // Line = info.GetValue("Line"); - // Column = info.GetValue("Column"); - // RenameTo = info.GetValue("RenameTo"); - // } - - // public void Serialize(IXunitSerializationInfo info) - // { - // info.AddValue("FileName", FileName); - // info.AddValue("Line", Line); - // info.AddValue("Column", Column); - // info.AddValue("RenameTo", RenameTo); - // } - - // public override string ToString() => $"{FileName}"; - // } - + internal class TextEditComparer : IComparer + { + public int Compare(TextEdit a, TextEdit b) + { + return a.Range.Start.Line == b.Range.Start.Line + ? b.Range.End.Character - a.Range.End.Character + : b.Range.Start.Line - a.Range.Start.Line; + } } } diff --git a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs index 9c00253c7..e115e5fcb 100644 --- a/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Refactoring/RenameHandlerTests.cs @@ -61,9 +61,9 @@ public async void RenamedFunction(RenameTestTarget s) { response = await testHandler.Handle(request, CancellationToken.None); } - catch (HandlerErrorException) + catch (HandlerErrorException err) { - Assert.True(s.ShouldFail); + Assert.True(s.ShouldThrow, $"Unexpected HandlerErrorException: {err.Message}"); return; } if (s.ShouldFail) @@ -100,7 +100,7 @@ public async void RenamedVariable(RenameTestTarget s) } catch (HandlerErrorException err) { - Assert.True(s.ShouldFail, $"Shouldfail is {s.ShouldFail} and error is {err.Message}"); + Assert.True(s.ShouldThrow, $"Unexpected HandlerErrorException: {err.Message}"); return; } if (s.ShouldFail) From d06bb51087159fa555d6a32fcfd99d8798aaf6c6 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 30 Sep 2024 17:02:40 -0700 Subject: [PATCH 195/203] Update readme with current unsupported scenarios --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9e890545f..aa7d5d294 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ functionality needed to enable a consistent and robust PowerShell development experience in almost any editor or integrated development environment (IDE). -## [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) clients using PowerShell Editor Services: +## [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) clients using PowerShell Editor Services - [PowerShell for Visual Studio Code](https://github.com/PowerShell/vscode-powershell) > [!NOTE] @@ -150,14 +150,15 @@ PowerShell is not a statically typed language. As such, the renaming of function There are several edge case scenarios which may exist where rename is difficult or impossible, or unable to be determined due to the dynamic scoping nature of PowerShell. The focus of the rename support is on quick updates to variables or functions within a self-contained script file. It is not intended for module developers to find and rename a symbol across multiple files, which is very difficult to do as the relationships are primarily only computed at runtime and not possible to be statically analyzed. +👍👍 [Implemented and Tested Rename Scenarios](https://github.com/PowerShell/PowerShellEditorServices/blob/main/test/PowerShellEditorServices.Test.Shared/Refactoring) 🤚🤚 Unsupported Scenarios -❌ Renaming can only be done within a single file. Renaming symbols across multiple files is not supported. -❌ Files containing dotsourcing are currently not supported. -❌ Functions or variables must have a corresponding definition within their scope to be renamed. If we cannot find the original definition of a variable or function, the rename will not be supported. - -👍👍 [Implemented and Tested Rename Scenarios](https://github.com/PowerShell/PowerShellEditorServices/blob/main/test/PowerShellEditorServices.Test.Shared/Refactoring) +❌ Renaming can only be done within a single file. Renaming symbols across multiple files is not supported, even if those are dotsourced from the source file. +❌ Functions or variables must have a corresponding definition within their scope or above to be renamed. If we cannot find the original definition of a variable or function, the rename will not be supported. +❌ Dynamic Parameters are not supported +❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`) +❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported 📄📄 Filing a Rename Issue From 627365c74442b5a10a6f95b1bbe8b3503ba033f1 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 30 Sep 2024 17:14:58 -0700 Subject: [PATCH 196/203] Rework function name check to be ast based --- .../Services/TextDocument/RenameService.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index beed37923..0b39b442c 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -396,13 +396,14 @@ internal static bool IsValidFunctionName(string name) { // Allows us to supply function:varname or varname and get a proper result string candidate = "function " + name.TrimStart('$').TrimStart('-') + " {}"; - Parser.ParseInput(candidate, out Token[] tokens, out _); - return tokens.Length == 5 - && tokens[0].Kind == TokenKind.Function - && tokens[1].Kind == TokenKind.Identifier - && tokens[2].Kind == TokenKind.LCurly - && tokens[3].Kind == TokenKind.RCurly - && tokens[4].Kind == TokenKind.EndOfInput; + Ast ast = Parser.ParseInput(candidate, out _, out ParseError[] errors); + if (errors.Length > 0) + { + return false; + } + + return (ast.Find(a => a is FunctionDefinitionAst, false) as FunctionDefinitionAst)? + .Name is not null; } } From 107622953ce6e3c36acc8e5ef8243457c06ed613 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 1 Oct 2024 13:08:22 -0700 Subject: [PATCH 197/203] Add Limitation about scriptblocks --- README.md | 1 + .../Refactoring/Variables/_RefactorVariableTestCases.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index aa7d5d294..36eb0ec26 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ The focus of the rename support is on quick updates to variables or functions wi ❌ Dynamic Parameters are not supported ❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`) ❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported +❌ Renaming a variable inside of a scriptblock that is used in unscoped operations like `Foreach-Parallel` or `Start-Job` and the variable is not defined within the scriptblock is not supported and can have unexpected results. 📄📄 Filing a Rename Issue diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs index fdfb2c174..52a343a19 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs @@ -11,6 +11,7 @@ public class RefactorVariableTestCases new ("VariableSimpleAssignment.ps1", Line: 1, Column: 1, NewName: "Bad Name", ShouldThrow: true), new ("VariableSimpleAssignment.ps1", Line: 1, Column: 6, NoResult: true), new ("VariableCommandParameter.ps1", Line: 3, Column: 17), + new ("VariableCommandParameter.ps1", Line: 3, Column: 17, NewName: "-Renamed"), new ("VariableCommandParameter.ps1", Line: 10, Column: 10), new ("VariableCommandParameterSplatted.ps1", Line: 3, Column: 19 ), new ("VariableCommandParameterSplatted.ps1", Line: 21, Column: 12), From a0eb1230c5d7e390ef1226175fb45db9f685607a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 3 Oct 2024 14:11:28 -0700 Subject: [PATCH 198/203] Cleanup and refactor a lot of logic out to the extension functions, reduce some unnecessary ast searches --- .../Services/TextDocument/RenameService.cs | 24 +- .../Utility/AstExtensions.cs | 277 ++++++++++-------- 2 files changed, 158 insertions(+), 143 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 0b39b442c..cab72ad52 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -132,7 +132,7 @@ private static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams /// Finds the most specific renamable symbol at the given position /// /// Ast of the token or null if no renamable symbol was found - internal static Ast? FindRenamableSymbol(ScriptFile scriptFile, ScriptPositionAdapter position) + internal static Ast? FindRenamableSymbol(ScriptFile scriptFile, IScriptPosition position) { List renameableAstTypes = [ // Functions @@ -604,26 +604,4 @@ internal record ScriptExtentAdapter(IScriptExtent extent) : IScriptExtent public int StartColumnNumber => extent.StartColumnNumber; public int StartLineNumber => extent.StartLineNumber; public string Text => extent.Text; - - public bool Contains(IScriptPosition position) => Contains(new ScriptPositionAdapter(position)); - - public bool Contains(ScriptPositionAdapter position) - { - if (position.Line < Start.Line || position.Line > End.Line) - { - return false; - } - - if (position.Line == Start.Line && position.Character < Start.Character) - { - return false; - } - - if (position.Line == End.Line && position.Character > End.Character) - { - return false; - } - - return true; - } } diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index 0073b4c08..76930d15a 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -10,77 +10,161 @@ namespace Microsoft.PowerShell.EditorServices.Language; +// NOTE: A lot of this is reimplementation of https://github.com/PowerShell/PowerShell/blob/2d5d702273060b416aea9601e939ff63bb5679c9/src/System.Management.Automation/engine/parser/Position.cs which is internal and sealed. + public static class AstExtensions { - - internal static bool Contains(this Ast ast, Ast other) => ast.Find(ast => ast == other, true) != null; - internal static bool Contains(this Ast ast, IScriptPosition position) => new ScriptExtentAdapter(ast.Extent).Contains(position); - - internal static bool IsAfter(this Ast ast, Ast other) + private const int IS_BEFORE = -1; + private const int IS_AFTER = 1; + private const int IS_EQUAL = 0; + internal static int CompareTo(this IScriptPosition position, IScriptPosition other) { - return - ast.Extent.StartLineNumber > other.Extent.EndLineNumber - || - ( - ast.Extent.StartLineNumber == other.Extent.EndLineNumber - && ast.Extent.StartColumnNumber > other.Extent.EndColumnNumber - ); + if (position.LineNumber < other.LineNumber) + { + return IS_BEFORE; + } + else if (position.LineNumber > other.LineNumber) + { + return IS_AFTER; + } + else //Lines are equal + { + if (position.ColumnNumber < other.ColumnNumber) + { + return IS_BEFORE; + } + else if (position.ColumnNumber > other.ColumnNumber) + { + return IS_AFTER; + } + else //Columns are equal + { + return IS_EQUAL; + } + } } + internal static bool IsEqual(this IScriptPosition position, IScriptPosition other) + => position.CompareTo(other) == IS_EQUAL; + + internal static bool IsBefore(this IScriptPosition position, IScriptPosition other) + => position.CompareTo(other) == IS_BEFORE; + + internal static bool IsAfter(this IScriptPosition position, IScriptPosition other) + => position.CompareTo(other) == IS_AFTER; + + internal static bool Contains(this IScriptExtent extent, IScriptPosition position) + => extent.StartScriptPosition.IsEqual(position) + || extent.EndScriptPosition.IsEqual(position) + || (extent.StartScriptPosition.IsBefore(position) && extent.EndScriptPosition.IsAfter(position)); + + internal static bool Contains(this IScriptExtent extent, IScriptExtent other) + => extent.Contains(other.StartScriptPosition) && extent.Contains(other.EndScriptPosition); + + internal static bool StartsBefore(this IScriptExtent extent, IScriptExtent other) + => extent.StartScriptPosition.IsBefore(other.StartScriptPosition); + + internal static bool StartsBefore(this IScriptExtent extent, IScriptPosition other) + => extent.StartScriptPosition.IsBefore(other); + + internal static bool StartsAfter(this IScriptExtent extent, IScriptExtent other) + => extent.StartScriptPosition.IsAfter(other.StartScriptPosition); + + internal static bool StartsAfter(this IScriptExtent extent, IScriptPosition other) + => extent.StartScriptPosition.IsAfter(other); + + internal static bool IsBefore(this IScriptExtent extent, IScriptExtent other) + => !other.Contains(extent) + && !extent.Contains(other) + && extent.StartScriptPosition.IsBefore(other.StartScriptPosition); + + internal static bool IsAfter(this IScriptExtent extent, IScriptExtent other) + => !other.Contains(extent) + && !extent.Contains(other) + && extent.StartScriptPosition.IsAfter(other.StartScriptPosition); + + internal static bool Contains(this Ast ast, Ast other) + => ast.Extent.Contains(other.Extent); + + internal static bool Contains(this Ast ast, IScriptPosition position) + => ast.Extent.Contains(position); + + internal static bool Contains(this Ast ast, IScriptExtent position) + => ast.Extent.Contains(position); + internal static bool IsBefore(this Ast ast, Ast other) - { - return - ast.Extent.EndLineNumber < other.Extent.StartLineNumber - || - ( - ast.Extent.EndLineNumber == other.Extent.StartLineNumber - && ast.Extent.EndColumnNumber < other.Extent.StartColumnNumber - ); - } + => ast.Extent.IsBefore(other.Extent); + + internal static bool IsAfter(this Ast ast, Ast other) + => ast.Extent.IsAfter(other.Extent); internal static bool StartsBefore(this Ast ast, Ast other) - { - return - ast.Extent.StartLineNumber < other.Extent.StartLineNumber - || - ( - ast.Extent.StartLineNumber == other.Extent.StartLineNumber - && ast.Extent.StartColumnNumber < other.Extent.StartColumnNumber - ); - } + => ast.Extent.StartsBefore(other.Extent); + + internal static bool StartsBefore(this Ast ast, IScriptExtent other) + => ast.Extent.StartsBefore(other); + + internal static bool StartsBefore(this Ast ast, IScriptPosition other) + => ast.Extent.StartsBefore(other); internal static bool StartsAfter(this Ast ast, Ast other) - { - return - ast.Extent.StartLineNumber < other.Extent.StartLineNumber - || - ( - ast.Extent.StartLineNumber == other.Extent.StartLineNumber - && ast.Extent.StartColumnNumber < other.Extent.StartColumnNumber - ); - } + => ast.Extent.StartsAfter(other.Extent); + + internal static bool StartsAfter(this Ast ast, IScriptExtent other) + => ast.Extent.StartsAfter(other); + + internal static bool StartsAfter(this Ast ast, IScriptPosition other) + => ast.Extent.StartsAfter(other); - internal static Ast? FindBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) + /// + /// Finds the outermost Ast that starts before the target and matches the predicate within the scope. Returns null if none found. Useful for finding definitions of variable/function references + /// + /// The target Ast to search from + /// The predicate to match the Ast against + /// If true, the search will continue until the topmost scope boundary is reached + internal static Ast? FindStartsBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) { - Ast? scope = crossScopeBoundaries - ? target.FindParents(typeof(ScriptBlockAst)).LastOrDefault() - : target.GetScopeBoundary(); - return scope?.Find(ast => ast.IsBefore(target) && predicate(ast), false); + Ast? scope = target.GetScopeBoundary(); + do + { + Ast? result = scope?.Find(ast => ast.StartsBefore(target) && predicate(ast) + , searchNestedScriptBlocks: false); + + if (result is not null) + { + return result; + } + + scope = scope?.GetScopeBoundary(); + } while (crossScopeBoundaries && scope is not null); + + return null; } - internal static IEnumerable FindAllBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) + /// + /// Finds all AST items that start before the target and match the predicate within the scope. Items are returned in order from closest to furthest. Returns an empty list if none found. Useful for finding definitions of variable/function references + /// + internal static IEnumerable FindAllStartsBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) { - Ast? scope = crossScopeBoundaries - ? target.FindParents(typeof(ScriptBlockAst)).LastOrDefault() - : target.GetScopeBoundary(); - return scope?.FindAll(ast => ast.IsBefore(target) && predicate(ast), false) ?? []; + Ast? scope = target.GetScopeBoundary(); + do + { + IEnumerable results = scope?.FindAll(ast => ast.StartsBefore(target) && predicate(ast) + , searchNestedScriptBlocks: false) ?? []; + + foreach (Ast result in results.Reverse()) + { + yield return result; + } + scope = scope?.GetScopeBoundary(); + } while (crossScopeBoundaries && scope is not null); } - internal static Ast? FindAfter(this Ast target, Func predicate, bool crossScopeBoundaries = false) - => target.Parent.Find(ast => ast.IsAfter(target) && predicate(ast), crossScopeBoundaries); + internal static Ast? FindStartsAfter(this Ast target, Func predicate, bool searchNestedScriptBlocks = false) + => target.Parent.Find(ast => ast.StartsAfter(target) && predicate(ast), searchNestedScriptBlocks); - internal static IEnumerable FindAllAfter(this Ast target, Func predicate, bool crossScopeBoundaries = false) - => target.Parent.FindAll(ast => ast.IsAfter(target) && predicate(ast), crossScopeBoundaries); + internal static IEnumerable FindAllStartsAfter(this Ast target, Func predicate, bool searchNestedScriptBlocks = false) + => target.Parent.FindAllStartsAfter(ast => ast.StartsAfter(target) && predicate(ast), searchNestedScriptBlocks); /// /// Finds the most specific Ast at the given script position, or returns null if none found.
@@ -89,52 +173,28 @@ internal static IEnumerable FindAllAfter(this Ast target, Func p ///
internal static Ast? FindClosest(this Ast ast, IScriptPosition position, Type[]? allowedTypes) { - // Short circuit quickly if the position is not in the provided range, no need to traverse if not - // TODO: Maybe this should be an exception instead? I mean technically its not found but if you gave a position outside the file something very wrong probably happened. - if (!new ScriptExtentAdapter(ast.Extent).Contains(position)) { return null; } + // Short circuit quickly if the position is not in the provided ast, no need to traverse if not + if (!ast.Contains(position)) { return null; } - // This will be updated with each loop, and re-Find to dig deeper Ast? mostSpecificAst = null; Ast? currentAst = ast; - do { currentAst = currentAst.Find(thisAst => { + // Always starts with the current item, we can skip it if (thisAst == mostSpecificAst) { return false; } - int line = position.LineNumber; - int column = position.ColumnNumber; - - // Performance optimization, skip statements that don't contain the position - if ( - thisAst.Extent.EndLineNumber < line - || thisAst.Extent.StartLineNumber > line - || (thisAst.Extent.EndLineNumber == line && thisAst.Extent.EndColumnNumber < column) - || (thisAst.Extent.StartLineNumber == line && thisAst.Extent.StartColumnNumber > column) - ) - { - return false; - } + if (allowedTypes is not null && !allowedTypes.Contains(thisAst.GetType())) { return false; } - if (allowedTypes is not null && !allowedTypes.Contains(thisAst.GetType())) - { - return false; - } - - if (new ScriptExtentAdapter(thisAst.Extent).Contains(position)) + if (thisAst.Contains(position)) { mostSpecificAst = thisAst; - return true; //Stops this particular find and looks more specifically + return true; //Restart the search within the more specific AST } return false; }, true); - - if (currentAst is not null) - { - mostSpecificAst = currentAst; - } } while (currentAst is not null); return mostSpecificAst; @@ -148,47 +208,24 @@ public static bool TryFindFunctionDefinition(this Ast ast, CommandAst command, o public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command) { + if (!ast.Contains(command)) { return null; } // Short circuit if the command is not in the ast + string? name = command.GetCommandName()?.ToLower(); if (name is null) { return null; } - FunctionDefinitionAst[] candidateFuncDefs = ast.FindAll(ast => + // NOTE: There should only be one match most of the time, the only other cases is when a function is defined multiple times (bad practice). If there are multiple definitions, the candidate "closest" to the command, which would be the last one found, is the appropriate one + return command.FindAllStartsBefore(ast => { - if (ast is not FunctionDefinitionAst funcDef) - { - return false; - } + if (ast is not FunctionDefinitionAst funcDef) { return false; } - if (funcDef.Name.ToLower() != name) - { - return false; - } + if (funcDef.Name.ToLower() != name) { return false; } // If the function is recursive (calls itself), its parent is a match unless a more specific in-scope function definition comes next (this is a "bad practice" edge case) // TODO: Consider a simple "contains" match - if (command.HasParent(funcDef)) - { - return true; - } - - if - ( - // TODO: Replace with a position match - funcDef.Extent.EndLineNumber > command.Extent.StartLineNumber - || - ( - funcDef.Extent.EndLineNumber == command.Extent.StartLineNumber - && funcDef.Extent.EndColumnNumber >= command.Extent.StartColumnNumber - ) - ) - { - return false; - } + if (command.HasParent(funcDef)) { return true; } return command.HasParent(funcDef.Parent); // The command is in the same scope as the function definition - }, true).Cast().ToArray(); - - // There should only be one match most of the time, the only other cases is when a function is defined multiple times (bad practice). If there are multiple definitions, the candidate "closest" to the command, which would be the last one found, is the appropriate one - return candidateFuncDefs.LastOrDefault(); + }, true).FirstOrDefault() as FunctionDefinitionAst; } public static string GetUnqualifiedName(this VariableExpressionAst ast) @@ -211,19 +248,19 @@ public static Ast GetHighestParent(this Ast ast, params Type[] type) /// /// Gets the closest parent that matches the specified type or null if none found. /// - public static Ast? FindParent(this Ast ast, params Type[] type) - => FindParents(ast, type).FirstOrDefault(); + public static Ast? FindParent(this Ast ast, params Type[] types) + => FindParents(ast, types).FirstOrDefault(); /// /// Returns an array of parents in order from closest to furthest /// - public static Ast[] FindParents(this Ast ast, params Type[] type) + public static Ast[] FindParents(this Ast ast, params Type[] types) { List parents = new(); Ast parent = ast.Parent; while (parent is not null) { - if (type.Contains(parent.GetType())) + if (types.Contains(parent.GetType())) { parents.Add(parent); } @@ -336,7 +373,7 @@ public static bool IsScopedVariableAssignment(this VariableExpressionAst var) if (hashtableAst.Parent is not CommandExpressionAst commandAst) { return null; } if (commandAst.Parent is not AssignmentStatementAst assignmentAst) { return null; } if (assignmentAst.Left is not VariableExpressionAst leftAssignVarAst) { return null; } - return assignmentAst.FindAfter(ast => + return assignmentAst.FindStartsAfter(ast => ast is VariableExpressionAst var && var.Splatted && var.GetUnqualifiedName().ToLower() == leftAssignVarAst.GetUnqualifiedName().ToLower() @@ -351,7 +388,7 @@ ast is VariableExpressionAst var { if (!varAst.Splatted) { throw new InvalidOperationException("The provided variable reference is not a splat and cannot be used with FindSplatVariableAssignment"); } - return varAst.FindBefore(ast => + return varAst.FindStartsBefore(ast => ast is StringConstantExpressionAst stringAst && stringAst.Value == varAst.GetUnqualifiedName() && stringAst.FindSplatParameterReference() == varAst, From 5973349cf10f78f4ead82657300b77b3ecd2e19a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 4 Oct 2024 09:15:44 -0700 Subject: [PATCH 199/203] Simplify GetTopVariableAssignment using new extension methods --- README.md | 1 + .../Services/TextDocument/RenameService.cs | 2 + .../Utility/AstExtensions.cs | 76 +++++++++---------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 36eb0ec26..92335bd6f 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ The focus of the rename support is on quick updates to variables or functions wi ❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`) ❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported ❌ Renaming a variable inside of a scriptblock that is used in unscoped operations like `Foreach-Parallel` or `Start-Job` and the variable is not defined within the scriptblock is not supported and can have unexpected results. +❌ Scriptblocks part of an assignment are considered isolated scopes. For example `$a = 5; $x = {$a}; & $x` does not consider the two $a to be related, even though in execution this reference matches. 📄📄 Filing a Rename Issue diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index cab72ad52..5d890f6af 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -460,7 +460,9 @@ private bool ShouldRename(Ast candidate) } if (candidate == VariableDefinition) { return true; } + // Performance optimization if (VariableDefinition.IsAfter(candidate)) { return false; } + if (candidate.GetTopVariableAssignment() == VariableDefinition) { return true; } return false; diff --git a/src/PowerShellEditorServices/Utility/AstExtensions.cs b/src/PowerShellEditorServices/Utility/AstExtensions.cs index 76930d15a..787ed426f 100644 --- a/src/PowerShellEditorServices/Utility/AstExtensions.cs +++ b/src/PowerShellEditorServices/Utility/AstExtensions.cs @@ -121,14 +121,17 @@ internal static bool StartsAfter(this Ast ast, IScriptPosition other) ///
/// The target Ast to search from /// The predicate to match the Ast against - /// If true, the search will continue until the topmost scope boundary is reached - internal static Ast? FindStartsBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false) + /// If true, the search will continue until the topmost scope boundary is + /// Searches scriptblocks within the parent at each level. This can be helpful to find "side" scopes but affects performance + internal static Ast? FindStartsBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false, bool searchNestedScriptBlocks = false) { Ast? scope = target.GetScopeBoundary(); do { - Ast? result = scope?.Find(ast => ast.StartsBefore(target) && predicate(ast) - , searchNestedScriptBlocks: false); + Ast? result = scope?.Find(ast => + ast.StartsBefore(target) + && predicate(ast) + , searchNestedScriptBlocks); if (result is not null) { @@ -141,6 +144,12 @@ internal static bool StartsAfter(this Ast ast, IScriptPosition other) return null; } + internal static T? FindStartsBefore(this Ast target, Func predicate, bool crossScopeBoundaries = false, bool searchNestedScriptBlocks = false) where T : Ast + => target.FindStartsBefore + ( + ast => ast is T type && predicate(type), crossScopeBoundaries, searchNestedScriptBlocks + ) as T; + /// /// Finds all AST items that start before the target and match the predicate within the scope. Items are returned in order from closest to furthest. Returns an empty list if none found. Useful for finding definitions of variable/function references /// @@ -252,21 +261,19 @@ public static Ast GetHighestParent(this Ast ast, params Type[] type) => FindParents(ast, types).FirstOrDefault(); /// - /// Returns an array of parents in order from closest to furthest + /// Returns an enumerable of parents, in order of closest to furthest, that match the specified types. /// - public static Ast[] FindParents(this Ast ast, params Type[] types) + public static IEnumerable FindParents(this Ast ast, params Type[] types) { - List parents = new(); Ast parent = ast.Parent; while (parent is not null) { if (types.Contains(parent.GetType())) { - parents.Add(parent); + yield return parent; } parent = parent.Parent; } - return parents.ToArray(); } /// @@ -442,10 +449,8 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA StringConstantExpressionAst stringConstantExpressionAst => stringConstantExpressionAst.Value, _ => throw new NotSupportedException("The provided reference is not a variable reference type.") }; - - Ast? scope = reference.GetScopeBoundary(); - VariableExpressionAst? varAssignment = null; + Ast? scope = reference; while (scope is not null) { @@ -481,34 +486,29 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA } } } - // Will find the outermost assignment that matches the reference. + + // Will find the outermost assignment within the scope that matches the reference. varAssignment = reference switch { - VariableExpressionAst var => scope.Find - ( - ast => ast is VariableExpressionAst var - && ast.IsBefore(reference) - && - ( - (var.IsVariableAssignment() && !var.IsOperatorAssignment()) - || var.IsScopedVariableAssignment() - ) - && var.GetUnqualifiedName().ToLower() == name.ToLower() - , searchNestedScriptBlocks: false - ) as VariableExpressionAst, - - CommandParameterAst param => scope.Find - ( - ast => ast is VariableExpressionAst var - && ast.IsBefore(reference) - && var.GetUnqualifiedName().ToLower() == name.ToLower() - && var.Parent is ParameterAst paramAst - && paramAst.TryGetFunction(out FunctionDefinitionAst? foundFunction) - && foundFunction?.Name.ToLower() - == (param.Parent as CommandAst)?.GetCommandName()?.ToLower() - && foundFunction?.Parent?.Parent == scope - , searchNestedScriptBlocks: true //This might hit side scopes... - ) as VariableExpressionAst, + VariableExpressionAst => scope.FindStartsBefore(var => + var.GetUnqualifiedName().ToLower() == name.ToLower() + && ( + (var.IsVariableAssignment() && !var.IsOperatorAssignment()) + || var.IsScopedVariableAssignment() + ) + , crossScopeBoundaries: false, searchNestedScriptBlocks: false + ), + + CommandParameterAst param => scope.FindStartsBefore(var => + var.GetUnqualifiedName().ToLower() == name.ToLower() + && var.Parent is ParameterAst paramAst + && paramAst.TryGetFunction(out FunctionDefinitionAst? foundFunction) + && foundFunction?.Name.ToLower() + == (param.Parent as CommandAst)?.GetCommandName()?.ToLower() + && foundFunction?.Parent?.Parent == scope + ), + + _ => null }; From 5d6a2f3c97a54eba5e65b7fcb2412379ead5c6d8 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 4 Oct 2024 09:48:33 -0700 Subject: [PATCH 200/203] Add Test case and more info --- README.md | 3 +++ .../Variables/VariableDefinedInParamBlock.ps1 | 12 ++++++++++++ .../Variables/VariableDefinedInParamBlockRenamed.ps1 | 12 ++++++++++++ .../Variables/_RefactorVariableTestCases.cs | 1 + 4 files changed, 28 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlock.ps1 create mode 100644 test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlockRenamed.ps1 diff --git a/README.md b/README.md index 92335bd6f..991f10993 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,9 @@ The focus of the rename support is on quick updates to variables or functions wi ❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported ❌ Renaming a variable inside of a scriptblock that is used in unscoped operations like `Foreach-Parallel` or `Start-Job` and the variable is not defined within the scriptblock is not supported and can have unexpected results. ❌ Scriptblocks part of an assignment are considered isolated scopes. For example `$a = 5; $x = {$a}; & $x` does not consider the two $a to be related, even though in execution this reference matches. +❌ Scriptblocks that are part of a parameter are assumed to not be executing in a different runspace. For example, the renaming behavior will treat `ForEach-Object -Parallel {$x}` the same as `Foreach-Object {$x}` for purposes of finding scope definitions. To avoid unexpected renaming, define/redefine all your variables in the scriptblock using a param block. +❌ A lot of the logic relies on the position of items, so for example, defining a variable in a `begin` block and placing it after a `process` block, while technically correct in PowerShell, will not rename as expected. +❌ Similarly, defining a function, and having the function rely on a variable that is assigned outside the function and after the function definition, will not find the outer variable reference. 📄📄 Filing a Rename Issue diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlock.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlock.ps1 new file mode 100644 index 000000000..737974e68 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlock.ps1 @@ -0,0 +1,12 @@ +$x = 1 +function test { + begin { + $x = 5 + } + process { + $x + } + end { + $x + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlockRenamed.ps1 b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlockRenamed.ps1 new file mode 100644 index 000000000..abc8c54c8 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/VariableDefinedInParamBlockRenamed.ps1 @@ -0,0 +1,12 @@ +$x = 1 +function test { + begin { + $Renamed = 5 + } + process { + $Renamed + } + end { + $Renamed + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs index 52a343a19..3497c41eb 100644 --- a/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs +++ b/test/PowerShellEditorServices.Test.Shared/Refactoring/Variables/_RefactorVariableTestCases.cs @@ -15,6 +15,7 @@ public class RefactorVariableTestCases new ("VariableCommandParameter.ps1", Line: 10, Column: 10), new ("VariableCommandParameterSplatted.ps1", Line: 3, Column: 19 ), new ("VariableCommandParameterSplatted.ps1", Line: 21, Column: 12), + new ("VariableDefinedInParamBlock.ps1", Line: 10, Column: 9), new ("VariableDotNotationFromInnerFunction.ps1", Line: 1, Column: 1), new ("VariableDotNotationFromInnerFunction.ps1", Line: 11, Column: 26), new ("VariableInForeachDuplicateAssignment.ps1", Line: 6, Column: 18), From 2609d3c1bc522f5d9857ac4e7efff495013c8844 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 4 Oct 2024 22:27:24 -0700 Subject: [PATCH 201/203] Add Rename Parameter Alias Support --- .../Services/TextDocument/RenameService.cs | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs index 5d890f6af..41051daaf 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/RenameService.cs @@ -54,7 +54,7 @@ ILanguageServerConfiguration config internal bool DisclaimerAcceptedForSession; //This is exposed to allow testing non-interactively private bool DisclaimerDeclinedForSession; private const string ConfigSection = "powershell.rename"; - + private RenameServiceOptions? options; public async Task PrepareRenameSymbol(PrepareRenameParams request, CancellationToken cancellationToken) { RenameParams renameRequest = new() @@ -78,7 +78,7 @@ ILanguageServerConfiguration config public async Task RenameSymbol(RenameParams request, CancellationToken cancellationToken) { // We want scoped settings because a workspace setting might be relevant here. - RenameServiceOptions options = await GetScopedSettings(request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); + options = await GetScopedSettings(request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); if (!await AcceptRenameDisclaimer(options.acceptDisclaimer, cancellationToken).ConfigureAwait(false)) { return null; } @@ -98,7 +98,7 @@ or CommandAst VariableExpressionAst or CommandParameterAst or StringConstantExpressionAst - => RenameVariable(tokenToRename, scriptFile.ScriptAst, request, options.createParameterAlias), + => RenameVariable(tokenToRename, scriptFile.ScriptAst, request), _ => throw new InvalidOperationException("This should not happen as PrepareRename should have already checked for viability. File an issue if you see this.") }; @@ -120,10 +120,10 @@ private static TextEdit[] RenameFunction(Ast target, Ast scriptAst, RenameParams return visitor.VisitAndGetEdits(scriptAst); } - private static TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams, bool createParameterAlias) + private TextEdit[] RenameVariable(Ast symbol, Ast scriptAst, RenameParams requestParams) { - NewRenameVariableVisitor visitor = new( - symbol, requestParams.NewName + RenameVariableVisitor visitor = new( + symbol, requestParams.NewName, createParameterAlias: options?.createParameterAlias ?? false ); return visitor.VisitAndGetEdits(scriptAst); } @@ -407,7 +407,7 @@ internal static bool IsValidFunctionName(string name) } } -internal class NewRenameVariableVisitor(Ast target, string newName, bool skipVerify = false) : RenameVisitorBase +internal class RenameVariableVisitor(Ast target, string newName, bool skipVerify = false, bool createParameterAlias = false) : RenameVisitorBase { // Used to store the original definition of the variable to use as a reference. internal Ast? VariableDefinition; @@ -446,6 +446,24 @@ internal AstVisitAction Visit(Ast ast) if (ShouldRename(ast)) { + if ( + createParameterAlias + && ast == VariableDefinition + && VariableDefinition is not null and VariableExpressionAst varDefAst + && varDefAst.Parent is ParameterAst paramAst + ) + { + Edits.Add(new TextEdit + { + NewText = $"[Alias('{varDefAst.VariablePath.UserPath}')]", + Range = new Range() + { + Start = new ScriptPositionAdapter(paramAst.Extent.StartScriptPosition), + End = new ScriptPositionAdapter(paramAst.Extent.StartScriptPosition) + } + }); + } + Edits.Add(GetRenameVariableEdit(ast)); } From 7051803c8f552c0e5e186b4b6a6ec4114e5ddbeb Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 7 Oct 2024 16:25:04 -0700 Subject: [PATCH 202/203] Add note about Get/Set variable limitation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 991f10993..fd2cf97c8 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ The focus of the rename support is on quick updates to variables or functions wi ❌ Scriptblocks that are part of a parameter are assumed to not be executing in a different runspace. For example, the renaming behavior will treat `ForEach-Object -Parallel {$x}` the same as `Foreach-Object {$x}` for purposes of finding scope definitions. To avoid unexpected renaming, define/redefine all your variables in the scriptblock using a param block. ❌ A lot of the logic relies on the position of items, so for example, defining a variable in a `begin` block and placing it after a `process` block, while technically correct in PowerShell, will not rename as expected. ❌ Similarly, defining a function, and having the function rely on a variable that is assigned outside the function and after the function definition, will not find the outer variable reference. +❌ `Get-Variable` and `Set-Variable` are not considered and not currently searched for renames 📄📄 Filing a Rename Issue From 63bfac1010afa5d7f295f16d4067e997ea3b8543 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Mon, 7 Oct 2024 16:58:07 -0700 Subject: [PATCH 203/203] Fix readme markdown formatting --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fd2cf97c8..4f6c10c75 100644 --- a/README.md +++ b/README.md @@ -150,23 +150,23 @@ PowerShell is not a statically typed language. As such, the renaming of function There are several edge case scenarios which may exist where rename is difficult or impossible, or unable to be determined due to the dynamic scoping nature of PowerShell. The focus of the rename support is on quick updates to variables or functions within a self-contained script file. It is not intended for module developers to find and rename a symbol across multiple files, which is very difficult to do as the relationships are primarily only computed at runtime and not possible to be statically analyzed. -👍👍 [Implemented and Tested Rename Scenarios](https://github.com/PowerShell/PowerShellEditorServices/blob/main/test/PowerShellEditorServices.Test.Shared/Refactoring) - -🤚🤚 Unsupported Scenarios - -❌ Renaming can only be done within a single file. Renaming symbols across multiple files is not supported, even if those are dotsourced from the source file. -❌ Functions or variables must have a corresponding definition within their scope or above to be renamed. If we cannot find the original definition of a variable or function, the rename will not be supported. -❌ Dynamic Parameters are not supported -❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`) -❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported -❌ Renaming a variable inside of a scriptblock that is used in unscoped operations like `Foreach-Parallel` or `Start-Job` and the variable is not defined within the scriptblock is not supported and can have unexpected results. -❌ Scriptblocks part of an assignment are considered isolated scopes. For example `$a = 5; $x = {$a}; & $x` does not consider the two $a to be related, even though in execution this reference matches. -❌ Scriptblocks that are part of a parameter are assumed to not be executing in a different runspace. For example, the renaming behavior will treat `ForEach-Object -Parallel {$x}` the same as `Foreach-Object {$x}` for purposes of finding scope definitions. To avoid unexpected renaming, define/redefine all your variables in the scriptblock using a param block. -❌ A lot of the logic relies on the position of items, so for example, defining a variable in a `begin` block and placing it after a `process` block, while technically correct in PowerShell, will not rename as expected. -❌ Similarly, defining a function, and having the function rely on a variable that is assigned outside the function and after the function definition, will not find the outer variable reference. -❌ `Get-Variable` and `Set-Variable` are not considered and not currently searched for renames - -📄📄 Filing a Rename Issue +#### 👍 [Implemented and Tested Rename Scenarios](https://github.com/PowerShell/PowerShellEditorServices/blob/main/test/PowerShellEditorServices.Test.Shared/Refactoring) + +#### 🤚 Unsupported Scenarios + +- ❌ Renaming can only be done within a single file. Renaming symbols across multiple files is not supported, even if those are dotsourced from the source file. +- ❌ Functions or variables must have a corresponding definition within their scope or above to be renamed. If we cannot find the original definition of a variable or function, the rename will not be supported. +- ❌ Dynamic Parameters are not supported +- ❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`) +- ❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported +- ❌ Renaming a variable inside of a scriptblock that is used in unscoped operations like `Foreach-Parallel` or `Start-Job` and the variable is not defined within the scriptblock is not supported and can have unexpected results. +- ❌ Scriptblocks part of an assignment are considered isolated scopes. For example `$a = 5; $x = {$a}; & $x` does not consider the two $a to be related, even though in execution this reference matches. +- ❌ Scriptblocks that are part of a parameter are assumed to not be executing in a different runspace. For example, the renaming behavior will treat `ForEach-Object -Parallel {$x}` the same as `Foreach-Object {$x}` for purposes of finding scope definitions. To avoid unexpected renaming, define/redefine all your variables in the scriptblock using a param block. +- ❌ A lot of the logic relies on the position of items, so for example, defining a variable in a `begin` block and placing it after a `process` block, while technically correct in PowerShell, will not rename as expected. +- ❌ Similarly, defining a function, and having the function rely on a variable that is assigned outside the function and after the function definition, will not find the outer variable reference. +- ❌ `Get-Variable` and `Set-Variable` are not considered and not currently searched for renames + +#### 📄 Filing a Rename Issue If there is a rename scenario you feel can be reasonably supported in PowerShell, please file a bug report in the PowerShellEditorServices repository with the "Expected" and "Actual" being the before and after rename. We will evaluate it and accept or reject it and give reasons why. Items that fall under the Unsupported Scenarios above will be summarily rejected, however that does not mean that they may not be supported in the future if we come up with a reasonably safe way to implement a scenario.