Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RenameProvider for variable/function renaming #2152

Open
wants to merge 203 commits into
base: main
Choose a base branch
from

Conversation

Razmo99
Copy link

@Razmo99 Razmo99 commented Mar 25, 2024

Issue: Smart Variable Rename

PR Summary

This pull request implements the LSP textDocument/rename and textDocument/prepareRename handlers for PowerShell.

#2152 exposes the related service settings in vscode-powershell.

Reviewer's Guide

PowerShell is not a statically typed language and as such, some variable definitions and relations are determined at runtime, therefore true static analysis of a full PowerShell project is not possible. However, by presenting a disclaimer of the limitations, we feel we can provide fairly stable rename support within a single document for several scenarios without turning this into a bug/issue farm.

All work is driven through a RenameService which controls the messaging and registers the handlers. The general flow is to:

  1. Identify if a symbol at least meets the basic criteria for a rename, as a performance optimization
  2. Find the original definition/assignment where the target symbol is referenced
  3. Use that as an anchor for all renames in the document.

The RenameHandlerTests and PrepareRenameHandlerTests follow the rename behavior, and the Handler is considered our public API for these purposes and what we test against, even though there is a lot of implementation detail inside.

Taking this approach minimizes state maintenance in the Service at the expense of probably more-than-necessary AST walks, but current testing performance finds this to be sufficient even for large documents. There are several places caching of some AST walks could be performed to optimize performance but they are out of scope for this initial PR.

Reviewer Asks

  • Identify areas where there is a public PowerShell API where logic is being duplicated that could be replaced with the API instead
  • Find edge cases/scenarios that we should be able to reasonably cover, if not, we will add them to the exclusions.

TODO List

  • Replaced Custom LSP calls with OmniSharp IPrepareRenameHandler and IRenameHandler
  • Use custom ScriptExtentAdapter and ScriptPositionAdapter types to translate between script and lsp positions to avoid "off-by-one" errors since lsp is zero-based and script is one-based
  • Tests for PrepareRenameProvider and RenameProvider
  • Deduplicate PrepareRenameProvider and RenameProvider logic
  • Add Configuration Support via Omnisharp ILanguageServerConfiguration
  • Create Settings for Function and Variable Aliases and pass thru the info to LSP
  • Wire up Settings to actually work
  • Add Message Support via ILanguageServerFacade
  • Present Risk Warning and acceptance via LSP ShowMessageRequest (cannot set the setting directly ATM as it's not an LSP standard scenario)
  • Rewrite IterativeFunctionVisitor to an AstVisitor
  • Rewrite IterativeVariableVisitor to an AstVisitor
  • Finish fixing tests
  • Add more "sad path" tests for things that should not get triggered for rename
  • Add E2E tests for testing the actual rename process
  • Validate FunctionName and VariableName to be valid before passing through
  • Support scope modifiers (e.g. $GLOBAL, $SCRIPT, etc.)

Potential Additional Features

  • Simple conditional splat parameter rename.
function Do-Thing($test) {} #test should rename
$x = @{test = 2} #test here should rename
$x.test = 5 #Test here should rename
Do-Thing @x
  • Renaming $_ or $PSItem can place a $_ = or $PSItem = at the top of the scope. (I would use this a lot and seems pretty reasonable to do, but we would need an implicit definition of $PSItem in the current structure since we require a definition)

More Tests

  • Space in variable or function rename request should fail "$va riable" along with any other invalid characters for a rename
  • Parameter, Function, and Variable renames where they are not defined in scope should fail
  • Renaming a parameter and removing the '-' should still work

@Razmo99 Razmo99 changed the title WIP: Implemtation of VSCode RenameProvider for variable/function renaming Implemtation of VSCode RenameProvider for variable/function renaming Mar 25, 2024
@Razmo99 Razmo99 marked this pull request as ready for review March 25, 2024 04:06
@Razmo99 Razmo99 requested a review from a team March 25, 2024 04:06
@andyleejordan andyleejordan force-pushed the main branch 2 times, most recently from caa9e27 to 677f42c Compare May 3, 2024 23:07
@JustinGrote
Copy link
Collaborator

Looks like there's some tests that need to be fixed after pulling in latest main

@Razmo99
Copy link
Author

Razmo99 commented Jun 2, 2024

Hey @JustinGrote I've updated the tests to use Async, they should at least run / compile now

@JustinGrote
Copy link
Collaborator

JustinGrote commented Jun 3, 2024

Latest MacOS/Ubuntu test failures look like a path combine issue.
System.IO.DirectoryNotFoundException : Could not find a part of the path '/Users/runner/work/PowerShellEditorServices/PowerShellEditorServices/test/PowerShellEditorServices.Test.Shared/Refactoring\Utilities/TestDetection.ps1'.

Note the backslash after Refactoring, may want to make sure you are using Path.Combine or Join-Path

EDIT: Found the backslash hardcoded, refactored to use the three-argument path.combine, this should still work with net462

@JustinGrote JustinGrote changed the title Implemtation of VSCode RenameProvider for variable/function renaming RenameProvider for variable/function renaming Jun 3, 2024
@JustinGrote
Copy link
Collaborator

OK, all tests pass in CI now :). I'll do my best to get a first-pass review done sometime this week, and provide devcontainer/codespaces instructions to allow people to test for edge cases we need to document.

@JustinGrote
Copy link
Collaborator

JustinGrote commented Jun 4, 2024

Looking pretty good so far in terms of design, some cleanup will be needed.

Several foreach rename issues I've already come across, I'll try to define tests for these when I get a chance.

#Doesnt rename testvar
$a = 1..5
$b = 6..10
function test {
    process {
        foreach ($testvar in $a) {
            $testvar
        }
        
        foreach ($testvar in $b) {
            $testvar
        }
    }
}
#Renames both foreach local variables ($aPath), should only rename the one in scope (agreed this is bad practice though)
function Import-FileNoWildcard {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        # Specifies a path to one or more locations.
        [Parameter(Mandatory=$true,
                   Position=0,
                   ParameterSetName="Path",
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   HelpMessage="Path to one or more locations.")]
        [Alias("PSPath", "Path")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Path2
    )

    begin {
    }

    process {
        # Modify [CmdletBinding()] to [CmdletBinding(SupportsShouldProcess=$true)]
        $paths = @()
        foreach ($aPath in $Path2) {
            if (!(Test-Path -LiteralPath $aPath)) {
                $ex = New-Object System.Management.Automation.ItemNotFoundException "Cannot find path '$aPath' because it does not exist."
                $category = [System.Management.Automation.ErrorCategory]::ObjectNotFound
                $errRecord = New-Object System.Management.Automation.ErrorRecord $ex,'PathNotFound',$category,$aPath
                $psCmdlet.WriteError($errRecord)
                continue
            }

            # Resolve any relative paths
            $paths += $psCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($aPath)
        }

        foreach ($aPath in $paths) {
            if ($pscmdlet.ShouldProcess($aPath, 'Operation')) {
                # Process each path
                $aPath
            }
        }
    }

    end {
    }
}

@JustinGrote
Copy link
Collaborator

We will also need to make sure we account for the scoping issues as mentioned by Rob that are very difficult to handle, even if it means we warn about this as best effort, or straight up refuse to do it.
PowerShell/vscode-powershell#261 (comment)

@Razmo99
Copy link
Author

Razmo99 commented Jun 5, 2024

I made some test cases for renaming from within a for / each loop and limiting the rename to the scope if the var is defined within the creation of the statement. My only concern is a case like below.

If you run the code powershell treats the $i = 10 as the highest scope assignment which is then redefined in the loop but kept in function scope for when its printed. As it stands if you renamed $i = 10 it would rename the loop using the same variable. however if you renamed from within the loop it would not touch the $i=10

$a = 1..5
$b = 6..10
function test {
    process {
        $i = 10
        
        for ($Renamed = 0; $Renamed -lt $b.Count; $Renamed++) {
            $null = $Renamed
        }
        
        for ($i = 0; $i -lt $a.Count; $i++) {
            write-output $i
        }
        
        write-output "I will be 5 : $i not 10"
    }
}
test

foreach ($number in $x) {
testing_files $number

function testing_files {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename on function applies to the keyword. I like this behavior if it isn't immediately intuitive, should we do the same for like foreach as well for the inner var?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this behavior aswell, its a happy byproduct of how the FunctionDefinitionAST works, I don't think it would be too hard to get it working for for / foreach statments. In the screenshot below you can see that the VariableExpressionAst is nicely nested just need some logic to target it when the parent is selected.

image

@JustinGrote JustinGrote requested a review from a team as a code owner September 12, 2024 03:57
@JustinGrote
Copy link
Collaborator

JustinGrote commented Sep 12, 2024

@Razmo99 uh I don't know what I did but a bunch of non-matching main commits showed up, it's not letting me force push a reset, can you please revert to ff9c25b?

EDIT: Nevermind fixed it!

@JustinGrote JustinGrote force-pushed the rename-function-customvisitor branch 2 times, most recently from ff9c25b to 386b707 Compare September 12, 2024 04:21
…e and fix when dialog is closed rather than just button
@JustinGrote JustinGrote force-pushed the rename-function-customvisitor branch from 5c10a00 to 63bfac1 Compare December 20, 2024 19:36
@JustinGrote JustinGrote self-assigned this Dec 28, 2024
/// </summary>
/// <param name="target">The target Ast to search from</param>
/// <param name="predicate">The predicate to match the Ast against</param>
/// <param name="crossScopeBoundaries">If true, the search will continue until the topmost scope boundary is

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

End of sentence and closing param is missing.

}
public int CompareTo(Position other) => CompareTo((ScriptPositionAdapter)other);
public int CompareTo(ScriptPosition other) => CompareTo((ScriptPositionAdapter)other);
public string GetFullScript() => throw new NotImplementedException();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you thinking of returning with this? The whole file? Just the current script block? Where could this be used?


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.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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("Command element should always have a string expression as its first item. This is a bug and you should report it.");

Comment on lines +561 to +562
public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new
(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new
(
public static implicit operator Position(ScriptPositionAdapter scriptPosition) => new(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants