Skip to content

Commit 06337f4

Browse files
Refactor Expand-MemberExpression Function (#21)
* Move sub-function ToPascalCase to utility class Move the ToPascalCase outside of the ConvertTo-FunctionDefinition function and into a shared utility class for reuse. * Refactor Expand-MemberExpression This change refactors Expand-MemberExpression significantly. - Breaking Change - Remove TemplateName and NoParameterNameComments parameters - Remove the overly complicated logic for targeting a specific overload and replace it with a QuickOpen choice prompt - Change expression generation to use StringBuilder instead of PSStringTemplate. This is the first step in removing the dependancy. - Improve logic for determining the Type.Get* overload required to resolve a non-public member - Fix a variety of scenarios where invalid expressions would be generated - Add ShowOnThrow parameter to internal function GetAncestorOrThrow - Fix an issue where detection of shortest resolvable type name would use the TypeResolutionScope, resulting in invalid type expressions * Update help to reflect changes
1 parent af8558d commit 06337f4

10 files changed

+604
-265
lines changed

docs/en-US/EditorServicesCommandSuite.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The Expand-Expression function replaces text at a specified range with it's outp
5252

5353
### [Expand-MemberExpression](Expand-MemberExpression.md)
5454

55-
The Expand-MemberExpression function creates an expression for the closest MemberExpressionAst to the cursor in the current editor context. This is mainly to assist with creating expressions to access private members of .NET classes through reflection.
55+
The Expand-MemberExpression function expands member expressions to a more explicit statement.
5656

5757
### [Expand-TypeImplementation](Expand-TypeImplementation.md)
5858

docs/en-US/Expand-MemberExpression.md

Lines changed: 98 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ schema: 2.0.0
88

99
## SYNOPSIS
1010

11-
Builds an expression for accessing or invoking a member through reflection.
11+
Expands a member expression into a more explicit form.
1212

1313
## SYNTAX
1414

@@ -18,44 +18,115 @@ Expand-MemberExpression [[-Ast] <Ast>] [-TemplateName <String>] [-NoParameterNam
1818

1919
## DESCRIPTION
2020

21-
The Expand-MemberExpression function creates an expression for the closest MemberExpressionAst to the cursor in the current editor context. This is mainly to assist with creating expressions to access private members of .NET classes through reflection.
21+
The Expand-MemberExpression function expands member expressions to a more explicit statement. This
22+
function has two main purposes.
2223

23-
The expression is created using string templates. There are templates for several ways of accessing members including InvokeMember, GetProperty/GetValue, and a more verbose GetMethod/Invoke. If using the GetMethod/Invoke template it will automatically build type expressions for the "types" argument including nonpublic and generic types. If a template is not specified, this function will attempt to determine the most fitting template. If you have issues invoking a method with the default, try the VerboseInvokeMethod template. This function currently works on member expressions attached to the following:
24+
* Add parameter name comments (e.g. <# parameterName: #>) to method invocation arguments
2425

25-
1. Type literal expressions (including invalid expressions with non public types)
26+
* Invokable expressions that target non-public class members using reflection
2627

27-
2. Variable expressions where the variable exists within a currently existing scope.
28+
As an editor command, this function will expand the AST closest to the current cursor location
29+
if applicable.
2830

29-
3. Any other scenario where standard completion works.
31+
## EXAMPLES
3032

31-
4. Any number of nested member expressions where one of the above is true at some point in the chain.
33+
### -------------------------- EXAMPLE 1 --------------------------
3234

35+
```powershell
36+
'[ConsoleKeyInfo]::new' | Out-File .\example1.ps1
37+
psedit .\example1.ps1
38+
$psEditor.GetEditorContext().SetSelection(1, 20, 1, 20)
39+
Expand-MemberExpression
40+
$psEditor.GetEditorContext().CurrentFile.GetText()
3341
34-
Additionally chains may break if a member returns a type that is too generic like System.Object or a vague interface.
42+
# [System.ConsoleKeyInfo]::new(
43+
# <# keyChar: #> $keyChar,
44+
# <# key: #> $key,
45+
# <# shift: #> $shift,
46+
# <# alt: #> $alt,
47+
# <# control: #> $control)
3548
36-
## EXAMPLES
49+
```
3750

38-
### -------------------------- EXAMPLE 1 --------------------------
51+
* Creates a new file with an unfinished member expression
52+
* Opens it in the editor
53+
* Sets the cursor within the member expression
54+
* Invokes Expand-MemberExpression
55+
* Returns the new expression
56+
57+
The new expression is expanded to include arguments and parameter name comments for every parameter.
58+
59+
### -------------------------- EXAMPLE 2 --------------------------
3960

4061
```powershell
62+
'[sessionstatescope]::createfunction' | Out-File .\example2.ps1
63+
psedit .\example2.ps1
64+
$psEditor.GetEditorContext().SetSelection(1, 30, 1, 30)
4165
Expand-MemberExpression
66+
$psEditor.GetEditorContext().CurrentFile.GetText()
67+
68+
# $createFunction = [ref].Assembly.GetType('System.Management.Automation.SessionStateScope').
69+
# GetMethod('CreateFunction', [System.Reflection.BindingFlags]'Static, NonPublic').
70+
# Invoke($null, @(
71+
# <# name: #> $name,
72+
# <# function: #> $function,
73+
# <# originalFunction: #> $originalFunction,
74+
# <# options: #> $options,
75+
# <# context: #> $context,
76+
# <# helpFile: #> $helpFile))
77+
4278
```
4379

44-
Expands the member expression closest to the cursor in the current editor context using an automatically determined template.
80+
* Creates a new file with an unfinished member expression
81+
* Opens it in the editor
82+
* Sets the cursor within the member expression
83+
* Invokes Expand-MemberExpression
84+
* Returns the new expression
4585

46-
### -------------------------- EXAMPLE 2 --------------------------
86+
The new expression generated will resolve the non-public type and invoke the non-public method.
87+
88+
### -------------------------- EXAMPLE 3 --------------------------
4789

4890
```powershell
49-
Expand-MemberExpression -Template VerboseInvokeMethod
91+
'$ExecutionContext.SessionState.Internal.RemoveVariableAtScope' | Out-File .\example3.ps1
92+
psedit .\example3.ps1
93+
$psEditor.GetEditorContext().SetSelection(1, 60, 1, 60)
94+
Expand-MemberExpression
95+
# Manually select the last overload in the menu opened in the editor.
96+
$psEditor.GetEditorContext().CurrentFile.GetText()
97+
98+
# $internal = $ExecutionContext.SessionState.GetType().
99+
# GetProperty('Internal', [System.Reflection.BindingFlags]'Instance, NonPublic').
100+
# GetValue($ExecutionContext.SessionState)
101+
#
102+
# $removeVariableAtScope = $internal.GetType().
103+
# GetMethod(
104+
# <# name: #> 'RemoveVariableAtScope',
105+
# <# bindingAttr: #> [System.Reflection.BindingFlags]'Instance, NonPublic',
106+
# <# binder: #> $null,
107+
# <# types: #> @([string], [string], [bool]),
108+
# <# modifiers: #> 3).
109+
# Invoke($internal, @(
110+
# <# name: #> $name,
111+
# <# scopeID: #> $scopeID,
112+
# <# force: #> $force))
50113
```
51114

52-
Expands the member expression closest to the cursor in the current editor context using the VerboseInvokeMethod template.
115+
* Creates a new file with an unfinished member expression
116+
* Opens it in the editor
117+
* Sets the cursor within the member expression
118+
* Invokes Expand-MemberExpression
119+
* Returns the new expression
120+
121+
This example shows that an expression will be generated for each non-public member in the chain. It
122+
also demonstrates the ability to select an overload from a menu in the editor and the more alternate
123+
syntax generated for harder to resolve methods.
53124

54125
## PARAMETERS
55126

56127
### -Ast
57128

58-
Specifies the member expression ast (or child of) to expand.
129+
Specifies the AST of the member expression to expand.
59130

60131
```yaml
61132
Type: Ast
@@ -69,38 +140,6 @@ Accept pipeline input: True (ByPropertyName, ByValue)
69140
Accept wildcard characters: False
70141
```
71142
72-
### -TemplateName
73-
74-
A template is automatically chosen based on member type and visibility. You can use this parameter to force the use of a specific template.
75-
76-
```yaml
77-
Type: String
78-
Parameter Sets: (All)
79-
Aliases:
80-
81-
Required: False
82-
Position: Named
83-
Default value: None
84-
Accept pipeline input: False
85-
Accept wildcard characters: False
86-
```
87-
88-
### -NoParameterNameComments
89-
90-
By default expanded methods will have a comment with the parameter name on each line. (e.g. `<# paramName: #> $paramName,`) If you specify this parameter it will be omitted.
91-
92-
```yaml
93-
Type: SwitchParameter
94-
Parameter Sets: (All)
95-
Aliases:
96-
97-
Required: False
98-
Position: Named
99-
Default value: False
100-
Accept pipeline input: False
101-
Accept wildcard characters: False
102-
```
103-
104143
### CommonParameters
105144
106145
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216).
@@ -115,5 +154,16 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
115154
116155
## NOTES
117156
118-
## RELATED LINKS
157+
* When this function is building reflection statements, it will automatically choose the simpliest form
158+
of the Type.Get* methods that will resolve the target member.
159+
160+
* Member resolution is currently only possible in the following scenarios:
161+
* Type literal expressions, including invalid expressions with non public types like [localpipeline]
162+
* Variable expressions where the variable exists within a currently existing scope
163+
* Any other scenario where standard completion works
164+
* Any number of nested member expressions where one of the above is true at some point in the chain
119165
166+
* Member resolution may break in member chains if a member returns a type that is too generic like
167+
System.Object or IEnumerable
168+
169+
## RELATED LINKS

module/Classes/Expressions.ps1

Lines changed: 17 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
using namespace System.Reflection
2-
using namespace System.Collections.ObjectModel
3-
using namespace System.Management.Automation.Language
1+
using namespace System.Text
2+
using namespace Microsoft.PowerShell
43

54
class TypeExpressionHelper {
65
[type] $Type;
@@ -33,11 +32,16 @@ class TypeExpressionHelper {
3332
}
3433
}
3534
hidden [string] CreateProxy () {
36-
$builder = [System.Text.StringBuilder]::new('[')
35+
$builder = [StringBuilder]::new('[')
3736
$assembly = $this.Type.Assembly
3837

3938
# First check if there are any type accelerators in the same assembly.
40-
$choices = $this.GetAccelerators().GetEnumerator().Where{ $PSItem.Value.Assembly -eq $assembly }.Key
39+
$choices = [ref].
40+
Assembly.
41+
GetType('System.Management.Automation.TypeAccelerators')::
42+
Get.
43+
GetEnumerator().
44+
Where{ $PSItem.Value.Assembly -eq $assembly }.Key
4145

4246
if (-not $choices) {
4347
# Then as a last resort pull every type from the assembly. This takes a extra second or
@@ -64,7 +68,7 @@ class TypeExpressionHelper {
6468
}
6569
}
6670
hidden [string] CreateLiteral () {
67-
$builder = [System.Text.StringBuilder]::new()
71+
$builder = [StringBuilder]::new()
6872
# If we are building the type name as a generic type argument in a type literal we don't want
6973
# to enclose it with brackets.
7074
if ($this.encloseWithBrackets) { $builder.Append('[') }
@@ -77,13 +81,14 @@ class TypeExpressionHelper {
7781
Append(']')
7882
}
7983
else {
80-
$name = $this.GetAccelerators().
81-
GetEnumerator().
82-
Where{ $PSItem.Value -eq $this.Type }.
83-
Key |
84-
Sort-Object Length
84+
$name = [string]::Empty
85+
# Try to resolve the type name with a code method that won't take this script's
86+
# using statements into account. This also handles type accelerators for us.
87+
$resolvableName = [ToStringCodeMethods]::Type($this.Type)
88+
if ($resolvableName -as [type]) {
89+
$name = $resolvableName
90+
}
8591

86-
if (-not $name) { $name = ($this.Type.Name -as [type]).Name }
8792
if (-not $name) { $name = $this.Type.ToString() }
8893

8994
if ($name.Count -gt 1) { $name = $name[0] }
@@ -105,59 +110,4 @@ class TypeExpressionHelper {
105110
[TypeExpressionHelper]::Create($PSItem, $enclose)
106111
} -join ', '
107112
}
108-
hidden [System.Collections.Generic.Dictionary[string, type]] GetAccelerators () {
109-
return [ref].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get
110-
}
111-
}
112-
113-
class ExtendedMemberExpressionAst : MemberExpressionAst {
114-
[type] $InferredType;
115-
[MemberInfo] $InferredMember;
116-
[BindingFlags] $BindingFlags;
117-
[ReadOnlyCollection[ExpressionAst]] $Arguments;
118-
119-
ExtendedMemberExpressionAst ([IScriptExtent] $extent,
120-
[ExpressionAst] $expression,
121-
[CommandElementAst] $member,
122-
[bool] $static,
123-
[ReadOnlyCollection[ExpressionAst]] $arguments) :
124-
base($extent, $expression, $member, $static) {
125-
126-
try {
127-
$this.Arguments = $arguments
128-
$this.InferredMember = GetInferredMember -Ast $this
129-
$this.InferredType = ($this.InferredMember.ReturnType,
130-
$this.InferredMember.PropertyType,
131-
$this.InferredMember.FieldType).
132-
Where({ $PSItem }, 'First')[0]
133-
134-
$this.BindingFlags = $this.InferredMember.GetType().
135-
GetProperty('BindingFlags', [BindingFlags]'Instance, NonPublic').
136-
GetValue($this.InferredMember)
137-
} catch {
138-
$this.InferredType = [object]
139-
}
140-
}
141-
static [ExtendedMemberExpressionAst] op_Implicit ([MemberExpressionAst] $ast) {
142-
143-
$expression = $ast.Expression.Copy()
144-
if ($expression -is [MemberExpressionAst]) {
145-
$expression = [ExtendedMemberExpressionAst]$expression
146-
}
147-
$newAst = [ExtendedMemberExpressionAst]::new(
148-
$ast.Extent,
149-
$expression,
150-
$ast.Member.Copy(),
151-
$ast.Static,
152-
$ast.Arguments
153-
)
154-
155-
if ($ast.Parent) {
156-
$ast.Parent.GetType().
157-
GetMethod('SetParent', [BindingFlags]'Instance, NonPublic').
158-
Invoke($ast.Parent, $newAst)
159-
}
160-
161-
return $newAst
162-
}
163113
}

0 commit comments

Comments
 (0)