Skip to content

Commit 66d8782

Browse files
committed
Breakout and simplify matching logic, fixes more test cases
1 parent f330f5b commit 66d8782

File tree

2 files changed

+48
-78
lines changed

2 files changed

+48
-78
lines changed

src/PowerShellEditorServices/Language/AstExtensions.cs

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -72,54 +72,48 @@ public static class AstExtensions
7272

7373
public static FunctionDefinitionAst? FindFunctionDefinition(this Ast ast, CommandAst command)
7474
{
75-
string? name = command.GetCommandName();
75+
string? name = command.GetCommandName().ToLower();
7676
if (name is null) { return null; }
7777

78-
List<FunctionDefinitionAst> FunctionDefinitions = ast.FindAll(ast =>
78+
FunctionDefinitionAst[] candidateFuncDefs = ast.FindAll(ast =>
7979
{
80-
return ast is FunctionDefinitionAst funcDef &&
81-
funcDef.Name.ToLower() == name &&
82-
(funcDef.Extent.EndLineNumber < command.Extent.StartLineNumber ||
83-
(funcDef.Extent.EndColumnNumber <= command.Extent.StartColumnNumber &&
84-
funcDef.Extent.EndLineNumber <= command.Extent.StartLineNumber));
85-
}, true).Cast<FunctionDefinitionAst>().ToList();
86-
87-
// return the function def if we only have one match
88-
if (FunctionDefinitions.Count == 1)
89-
{
90-
return FunctionDefinitions[0];
91-
}
92-
// Determine which function definition is the right one
93-
FunctionDefinitionAst? CorrectDefinition = null;
94-
for (int i = FunctionDefinitions.Count - 1; i >= 0; i--)
95-
{
96-
FunctionDefinitionAst element = FunctionDefinitions[i];
80+
if (ast is not FunctionDefinitionAst funcDef)
81+
{
82+
return false;
83+
}
9784

98-
Ast parent = element.Parent;
99-
// walk backwards till we hit a functiondefinition if any
100-
while (null != parent)
85+
if (funcDef.Name.ToLower() != name)
10186
{
102-
if (parent is FunctionDefinitionAst)
103-
{
104-
break;
105-
}
106-
parent = parent.Parent;
87+
return false;
10788
}
108-
// we have hit the global scope of the script file
109-
if (null == parent)
89+
90+
// 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)
91+
// TODO: Consider a simple "contains" match
92+
if (command.HasParent(funcDef))
11093
{
111-
CorrectDefinition = element;
112-
break;
94+
return true;
11395
}
11496

115-
if (command?.Parent == parent)
97+
if
98+
(
99+
// TODO: Replace with a position match
100+
funcDef.Extent.EndLineNumber > command.Extent.StartLineNumber
101+
||
102+
(
103+
funcDef.Extent.EndLineNumber == command.Extent.StartLineNumber
104+
&& funcDef.Extent.EndColumnNumber >= command.Extent.StartColumnNumber
105+
)
106+
)
116107
{
117-
CorrectDefinition = (FunctionDefinitionAst)parent;
108+
return false;
118109
}
119-
}
120-
return CorrectDefinition;
121-
}
122110

111+
return command.HasParent(funcDef.Parent); // The command is in the same scope as the function definition
112+
}, true).Cast<FunctionDefinitionAst>().ToArray();
113+
114+
// 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
115+
return candidateFuncDefs.LastOrDefault();
116+
}
123117

124118
public static Ast[] FindParents(this Ast ast, params Type[] type)
125119
{

src/PowerShellEditorServices/Services/TextDocument/RenameService.cs

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public class RenameFunctionVisitor(Ast target, string newName, bool skipVerify =
206206
{
207207
public List<TextEdit> Edits { get; } = new();
208208
private Ast? CurrentDocument;
209-
private string OldName = string.Empty;
209+
private FunctionDefinitionAst? FunctionToRename;
210210

211211
// Wire up our visitor to the relevant AST types we are potentially renaming
212212
public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast) => Visit(ast);
@@ -223,15 +223,13 @@ public AstVisitAction Visit(Ast ast)
223223
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");
224224
}
225225

226-
FunctionDefinitionAst functionDef = target switch
226+
FunctionToRename = target switch
227227
{
228228
FunctionDefinitionAst f => f,
229229
CommandAst command => CurrentDocument.FindFunctionDefinition(command)
230230
?? 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"),
231231
_ => throw new Exception($"Unsupported AST type {target.GetType()} encountered")
232232
};
233-
234-
OldName = functionDef.Name;
235233
};
236234

237235
if (CurrentDocument != ast.GetHighestParent())
@@ -241,7 +239,7 @@ public AstVisitAction Visit(Ast ast)
241239

242240
if (ShouldRename(ast))
243241
{
244-
Edits.Add(GetRenameFunctionEdits(ast));
242+
Edits.Add(GetRenameFunctionEdit(ast));
245243
return AstVisitAction.Continue;
246244
}
247245
else
@@ -254,10 +252,10 @@ public AstVisitAction Visit(Ast ast)
254252

255253
private bool ShouldRename(Ast candidate)
256254
{
257-
// There should be only one function definition and if it is not our target, it may be a duplicately named function
255+
// Rename our original function definition. There may be duplicate definitions of the same name
258256
if (candidate is FunctionDefinitionAst funcDef)
259257
{
260-
return funcDef == target;
258+
return funcDef == FunctionToRename;
261259
}
262260

263261
// Should only be CommandAst (function calls) from this point forward in the visit.
@@ -266,36 +264,20 @@ private bool ShouldRename(Ast candidate)
266264
throw new InvalidOperationException($"ShouldRename for a function had an Unexpected Ast Type {candidate.GetType()}. This is a bug and you should file an issue.");
267265
}
268266

269-
if (command.GetCommandName().ToLower() != OldName.ToLower())
270-
{
271-
return false;
272-
}
273-
274-
// TODO: Use position comparisons here
275-
// Command calls must always come after the function definitions
276-
if (
277-
target.Extent.StartLineNumber > command.Extent.StartLineNumber
278-
|| (
279-
target.Extent.StartLineNumber == command.Extent.StartLineNumber
280-
&& target.Extent.StartColumnNumber >= command.Extent.StartColumnNumber
281-
)
282-
)
267+
if (CurrentDocument is null)
283268
{
284-
return false;
269+
throw new InvalidOperationException("CurrentDoc should always be set by now from first Visit. This is a bug and you should file an issue.");
285270
}
286271

287-
// If the command is defined in the same parent scope as the function
288-
return command.HasParent(target.Parent);
289-
290-
// If we get this far, we hit an edge case
291-
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.");
272+
// Match up the command to its function definition
273+
return CurrentDocument.FindFunctionDefinition(command) == FunctionToRename;
292274
}
293275

294-
private TextEdit GetRenameFunctionEdits(Ast candidate)
276+
private TextEdit GetRenameFunctionEdit(Ast candidate)
295277
{
296278
if (candidate is FunctionDefinitionAst funcDef)
297279
{
298-
if (funcDef != target)
280+
if (funcDef != FunctionToRename)
299281
{
300282
throw new InvalidOperationException("GetRenameFunctionEdit was called on an Ast that was not the target. This is a bug and you should file an issue.");
301283
}
@@ -315,22 +297,16 @@ private TextEdit GetRenameFunctionEdits(Ast candidate)
315297
throw new InvalidOperationException($"Expected a command but got {candidate.GetType()}");
316298
}
317299

318-
if (command.GetCommandName()?.ToLower() == OldName.ToLower() &&
319-
target.Extent.StartLineNumber <= command.Extent.StartLineNumber)
300+
if (command.CommandElements[0] is not StringConstantExpressionAst funcName)
320301
{
321-
if (command.CommandElements[0] is not StringConstantExpressionAst funcName)
322-
{
323-
throw new InvalidOperationException("Command element should always have a string expresssion as its first item. This is a bug and you should report it.");
324-
}
325-
326-
return new TextEdit()
327-
{
328-
NewText = newName,
329-
Range = new ScriptExtentAdapter(funcName.Extent)
330-
};
302+
throw new InvalidOperationException("Command element should always have a string expresssion as its first item. This is a bug and you should report it.");
331303
}
332304

333-
throw new InvalidOperationException("GetRenameFunctionEdit was not provided a FuncitonDefinition or a CommandAst");
305+
return new TextEdit()
306+
{
307+
NewText = newName,
308+
Range = new ScriptExtentAdapter(funcName.Extent)
309+
};
334310
}
335311

336312
public TextEdit[] VisitAndGetEdits(Ast ast)

0 commit comments

Comments
 (0)