Skip to content

Commit fb82b77

Browse files
committed
Simplify GetTopVariableAssignment using new extension methods
1 parent bfc4ce1 commit fb82b77

File tree

3 files changed

+41
-38
lines changed

3 files changed

+41
-38
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ The focus of the rename support is on quick updates to variables or functions wi
151151
❌ Dynamically constructed splat parameters will not be renamed/updated (e.g. `$splat = @{};$splat.a = 5;Do-Thing @a`)
152152
❌ Scoped variables (e.g. $SCRIPT:test) are not currently supported
153153
❌ 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.
154+
❌ 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.
154155

155156
📄📄 Filing a Rename Issue
156157

src/PowerShellEditorServices/Services/TextDocument/RenameService.cs

+2
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,9 @@ private bool ShouldRename(Ast candidate)
460460
}
461461

462462
if (candidate == VariableDefinition) { return true; }
463+
// Performance optimization
463464
if (VariableDefinition.IsAfter(candidate)) { return false; }
465+
464466
if (candidate.GetTopVariableAssignment() == VariableDefinition) { return true; }
465467

466468
return false;

src/PowerShellEditorServices/Utility/AstExtensions.cs

+38-38
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,17 @@ internal static bool StartsAfter(this Ast ast, IScriptPosition other)
121121
/// </summary>
122122
/// <param name="target">The target Ast to search from</param>
123123
/// <param name="predicate">The predicate to match the Ast against</param>
124-
/// <param name="crossScopeBoundaries">If true, the search will continue until the topmost scope boundary is reached</param>
125-
internal static Ast? FindStartsBefore(this Ast target, Func<Ast, bool> predicate, bool crossScopeBoundaries = false)
124+
/// <param name="crossScopeBoundaries">If true, the search will continue until the topmost scope boundary is
125+
/// <param name="searchNestedScriptBlocks">Searches scriptblocks within the parent at each level. This can be helpful to find "side" scopes but affects performance</param>
126+
internal static Ast? FindStartsBefore(this Ast target, Func<Ast, bool> predicate, bool crossScopeBoundaries = false, bool searchNestedScriptBlocks = false)
126127
{
127128
Ast? scope = target.GetScopeBoundary();
128129
do
129130
{
130-
Ast? result = scope?.Find(ast => ast.StartsBefore(target) && predicate(ast)
131-
, searchNestedScriptBlocks: false);
131+
Ast? result = scope?.Find(ast =>
132+
ast.StartsBefore(target)
133+
&& predicate(ast)
134+
, searchNestedScriptBlocks);
132135

133136
if (result is not null)
134137
{
@@ -141,6 +144,12 @@ internal static bool StartsAfter(this Ast ast, IScriptPosition other)
141144
return null;
142145
}
143146

147+
internal static T? FindStartsBefore<T>(this Ast target, Func<T, bool> predicate, bool crossScopeBoundaries = false, bool searchNestedScriptBlocks = false) where T : Ast
148+
=> target.FindStartsBefore
149+
(
150+
ast => ast is T type && predicate(type), crossScopeBoundaries, searchNestedScriptBlocks
151+
) as T;
152+
144153
/// <summary>
145154
/// 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
146155
/// </summary>
@@ -252,21 +261,19 @@ public static Ast GetHighestParent(this Ast ast, params Type[] type)
252261
=> FindParents(ast, types).FirstOrDefault();
253262

254263
/// <summary>
255-
/// Returns an array of parents in order from closest to furthest
264+
/// Returns an enumerable of parents, in order of closest to furthest, that match the specified types.
256265
/// </summary>
257-
public static Ast[] FindParents(this Ast ast, params Type[] types)
266+
public static IEnumerable<Ast> FindParents(this Ast ast, params Type[] types)
258267
{
259-
List<Ast> parents = new();
260268
Ast parent = ast.Parent;
261269
while (parent is not null)
262270
{
263271
if (types.Contains(parent.GetType()))
264272
{
265-
parents.Add(parent);
273+
yield return parent;
266274
}
267275
parent = parent.Parent;
268276
}
269-
return parents.ToArray();
270277
}
271278

272279
/// <summary>
@@ -442,10 +449,8 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA
442449
StringConstantExpressionAst stringConstantExpressionAst => stringConstantExpressionAst.Value,
443450
_ => throw new NotSupportedException("The provided reference is not a variable reference type.")
444451
};
445-
446-
Ast? scope = reference.GetScopeBoundary();
447-
448452
VariableExpressionAst? varAssignment = null;
453+
Ast? scope = reference;
449454

450455
while (scope is not null)
451456
{
@@ -481,34 +486,29 @@ public static bool TryGetFunction(this ParameterAst ast, out FunctionDefinitionA
481486
}
482487
}
483488
}
484-
// Will find the outermost assignment that matches the reference.
489+
490+
// Will find the outermost assignment within the scope that matches the reference.
485491
varAssignment = reference switch
486492
{
487-
VariableExpressionAst var => scope.Find
488-
(
489-
ast => ast is VariableExpressionAst var
490-
&& ast.IsBefore(reference)
491-
&&
492-
(
493-
(var.IsVariableAssignment() && !var.IsOperatorAssignment())
494-
|| var.IsScopedVariableAssignment()
495-
)
496-
&& var.GetUnqualifiedName().ToLower() == name.ToLower()
497-
, searchNestedScriptBlocks: false
498-
) as VariableExpressionAst,
499-
500-
CommandParameterAst param => scope.Find
501-
(
502-
ast => ast is VariableExpressionAst var
503-
&& ast.IsBefore(reference)
504-
&& var.GetUnqualifiedName().ToLower() == name.ToLower()
505-
&& var.Parent is ParameterAst paramAst
506-
&& paramAst.TryGetFunction(out FunctionDefinitionAst? foundFunction)
507-
&& foundFunction?.Name.ToLower()
508-
== (param.Parent as CommandAst)?.GetCommandName()?.ToLower()
509-
&& foundFunction?.Parent?.Parent == scope
510-
, searchNestedScriptBlocks: true //This might hit side scopes...
511-
) as VariableExpressionAst,
493+
VariableExpressionAst => scope.FindStartsBefore<VariableExpressionAst>(var =>
494+
var.GetUnqualifiedName().ToLower() == name.ToLower()
495+
&& (
496+
(var.IsVariableAssignment() && !var.IsOperatorAssignment())
497+
|| var.IsScopedVariableAssignment()
498+
)
499+
, crossScopeBoundaries: false, searchNestedScriptBlocks: false
500+
),
501+
502+
CommandParameterAst param => scope.FindStartsBefore<VariableExpressionAst>(var =>
503+
var.GetUnqualifiedName().ToLower() == name.ToLower()
504+
&& var.Parent is ParameterAst paramAst
505+
&& paramAst.TryGetFunction(out FunctionDefinitionAst? foundFunction)
506+
&& foundFunction?.Name.ToLower()
507+
== (param.Parent as CommandAst)?.GetCommandName()?.ToLower()
508+
&& foundFunction?.Parent?.Parent == scope
509+
),
510+
511+
512512
_ => null
513513
};
514514

0 commit comments

Comments
 (0)