Skip to content

Commit 69d88f7

Browse files
authored
Add support for detecting and updating splatted parameters (#68)
* Splatted parameter support (part 1) * Added unit test support for splatted parameter detection * Fixed failing unit test from missing property * Added splatted parameter example scripts * Removed warnings for splatted parameter detection * Added matched az upgraded sample scripts * Fixed off-by-one bug and updated unit tests * Added support for splatting with ordered hashtable
1 parent f0f367f commit 69d88f7

21 files changed

+630
-176
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params
15+
$virtualMachineParams = @{
16+
Location = $location
17+
Image = "Win2016Datacenter"
18+
VirtualNetworkName = "myVnet"
19+
SubnetName = "mySubnet"
20+
SecurityGroupName = "myNetworkSecurityGroup"
21+
PublicIpAddressName = "myPublicIp"
22+
Credential = $cred
23+
OpenPorts = 3389
24+
}
25+
New-AzVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params (keys are wrapped with quotes)
15+
$virtualMachineParams = @{
16+
"Location" = $location
17+
"Image" = "Win2016Datacenter"
18+
"VirtualNetworkName" = "myVnet"
19+
"SubnetName" = "mySubnet"
20+
"SecurityGroupName" = "myNetworkSecurityGroup"
21+
"PublicIpAddressName" = "myPublicIp"
22+
"Credential" = $cred
23+
"OpenPorts" = 3389
24+
}
25+
New-AzVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params
15+
$virtualMachineParams = [ordered]@{
16+
Location = $location
17+
Image = "Win2016Datacenter"
18+
VirtualNetworkName = "myVnet"
19+
SubnetName = "mySubnet"
20+
SecurityGroupName = "myNetworkSecurityGroup"
21+
PublicIpAddressName = "myPublicIp"
22+
Credential = $cred
23+
OpenPorts = 3389
24+
}
25+
New-AzVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzureRmResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params
15+
$virtualMachineParams = @{
16+
Location = $location
17+
ImageName = "Win2016Datacenter"
18+
VirtualNetworkName = "myVnet"
19+
SubnetName = "mySubnet"
20+
SecurityGroupName = "myNetworkSecurityGroup"
21+
PublicIpAddressName = "myPublicIp"
22+
Credential = $cred
23+
OpenPorts = 3389
24+
}
25+
New-AzureRmVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzureRmResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params (keys are wrapped with quotes)
15+
$virtualMachineParams = @{
16+
"Location" = $location
17+
"ImageName" = "Win2016Datacenter"
18+
"VirtualNetworkName" = "myVnet"
19+
"SubnetName" = "mySubnet"
20+
"SecurityGroupName" = "myNetworkSecurityGroup"
21+
"PublicIpAddressName" = "myPublicIp"
22+
"Credential" = $cred
23+
"OpenPorts" = 3389
24+
}
25+
New-AzureRmVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Original source code: https://github.com/Azure/azure-docs-powershell-samples/blob/a513b6fceae51aaea1daaa8edd4d6fc66590d172/virtual-machine/create-vm-detailed/create-windows-vm-quick.ps1
2+
# Variables for common values
3+
$resourceGroup = "myResourceGroup"
4+
$location = "westeurope"
5+
$vmName = "myVM"
6+
7+
# Create user object
8+
$cred = Get-Credential -Message "Enter a username and password for the virtual machine."
9+
10+
# Create a resource group
11+
New-AzureRmResourceGroup -Name $resourceGroup -Location $location
12+
13+
# Create a virtual machine
14+
# use splatted params
15+
$virtualMachineParams = [ordered]@{
16+
Location = $location
17+
ImageName = "Win2016Datacenter"
18+
VirtualNetworkName = "myVnet"
19+
SubnetName = "mySubnet"
20+
SecurityGroupName = "myNetworkSecurityGroup"
21+
PublicIpAddressName = "myPublicIp"
22+
Credential = $cred
23+
OpenPorts = 3389
24+
}
25+
New-AzureRmVM @virtualMachineParams -ResourceGroupName $resourceGroup -Name $vmName

powershell-module/Az.Tools.Migration/Classes/Classes.ps1

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class CommandReferenceParameter
4545
[System.String] $FileName
4646
[System.String] $FullPath
4747
[System.String] $Name
48-
[System.String] $Value
4948
[System.Int32] $StartLine
5049
[System.Int32] $StartColumn
5150
[System.Int32] $EndLine
@@ -91,7 +90,7 @@ Enum UpgradeStepType
9190
Enum PlanResultReasonCode
9291
{
9392
ReadyToUpgrade = 0
94-
WarningSplattedParameters = 1
93+
WarningSplattedParameters = 1 # deprecated
9594
ErrorNoUpgradeAlias = 2
9695
ErrorNoModuleSpecMatch = 3
9796
ErrorParameterNotFound = 4

powershell-module/Az.Tools.Migration/Functions/Private/Find-CmdletsInFile.ps1

Lines changed: 105 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ function Find-CmdletsInFile
2626
)
2727
Process
2828
{
29+
# constants
2930
$matchPattern = "(\b[a-zA-z]+-[a-zA-z]+\b)"
30-
$cmdletRegex = New-Object System.Text.RegularExpressions.Regex($matchPattern)
31+
$doubleQuoteCharacter = '"'
32+
$singleQuoteCharacter = ''''
33+
$orderedTypeName = 'ordered'
3134

3235
# ref output vars
3336
$parserErrors = $null
@@ -47,10 +50,60 @@ function Find-CmdletsInFile
4750
}
4851
}
4952

50-
$predicate = { param($astObject) $astObject -is [System.Management.Automation.Language.CommandAst] }
53+
# search for variable assignment statements
54+
# the goal here is to build a table with the hastable variable sets (if any are present), to support splatted parameter names.
5155
$recurse = $true
56+
$assignmentPredicate = { param($astObject) $astObject -is [System.Management.Automation.Language.AssignmentStatementAst] }
57+
$assignmentAstNodes = $rootAstNode.FindAll($assignmentPredicate, $recurse)
58+
$hashtableVariables = New-Object -TypeName 'System.Collections.Generic.Dictionary[System.String, System.Collections.Generic.List[System.Management.Automation.Language.StringConstantExpressionAst]]'
59+
60+
for ([int]$i = 0; $i -lt $assignmentAstNodes.Count; $i++)
61+
{
62+
$currentVarAstNode = $assignmentAstNodes[$i]
63+
64+
# is the right hand side of the expression statement a hashtable node?
65+
if ($currentVarAstNode.Right.Expression -is [System.Management.Automation.Language.HashtableAst])
66+
{
67+
# capture the hashtable variable name
68+
$htVariableName = $currentVarAstNode.Left.VariablePath.UserPath
69+
$hashtableVariables[$htVariableName] = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.Language.StringConstantExpressionAst]'
70+
71+
# capture the hashtable key name extents.
72+
# -- the tuple's .Item1 contains the key name AST (which may represent a splatted parameter name).
73+
# -- the tuple's .Item2 contains the key value AST (we dont need to capture this)
74+
# -- also make sure to only grab hashtable key names that come from ConstantExpressionAst (to avoid unsupported subexpression keyname scenarios).
75+
foreach ($expressionAst in $currentVarAstNode.Right.Expression.KeyValuePairs)
76+
{
77+
if ($expressionAst.Item1 -is [System.Management.Automation.Language.StringConstantExpressionAst])
78+
{
79+
$hashtableVariables[$htVariableName].Add($expressionAst.Item1)
80+
}
81+
}
82+
}
83+
elseif ($currentVarAstNode.Right.Expression -is [System.Management.Automation.Language.ConvertExpressionAst] `
84+
-and $currentVarAstNode.Right.Expression.Type.TypeName.FullName -eq $orderedTypeName `
85+
-and $currentVarAstNode.Right.Expression.Child -is [System.Management.Automation.Language.HashtableAst])
86+
{
87+
# same as the above 'if' condition case, but special handling for [ordered] hashtable objects.
88+
# we have to check the .Child [HashtableAst] of the ConvertExpressionAst.
89+
90+
$htVariableName = $currentVarAstNode.Left.VariablePath.UserPath
91+
$hashtableVariables[$htVariableName] = New-Object -TypeName 'System.Collections.Generic.List[System.Management.Automation.Language.StringConstantExpressionAst]'
92+
93+
foreach ($expressionAst in $currentVarAstNode.Right.Expression.Child.KeyValuePairs)
94+
{
95+
if ($expressionAst.Item1 -is [System.Management.Automation.Language.StringConstantExpressionAst])
96+
{
97+
$hashtableVariables[$htVariableName].Add($expressionAst.Item1)
98+
}
99+
}
100+
}
101+
}
52102

53-
$commandAstNodes = $rootAstNode.FindAll($predicate, $recurse)
103+
# search for command statements
104+
$commandPredicate = { param($astObject) $astObject -is [System.Management.Automation.Language.CommandAst] }
105+
$commandAstNodes = $rootAstNode.FindAll($commandPredicate, $recurse)
106+
$cmdletRegex = New-Object System.Text.RegularExpressions.Regex($matchPattern)
54107

55108
for ([int]$i = 0; $i -lt $commandAstNodes.Count; $i++)
56109
{
@@ -86,32 +139,17 @@ function Find-CmdletsInFile
86139
{
87140
$paramRef = New-Object -TypeName CommandReferenceParameter
88141

89-
# substring to cut off the dash (-) character we dont need
90-
$paramRef.Name = $currentAstNodeCmdElement.Extent.Text.Substring(1)
91-
92-
# check for the parameter value.
93-
# if this is the last element in the list, or the next item is also a
94-
# parameter, then this parameter has no value (switch parameter)
95-
96-
if ($j -eq ($currentAstNode.CommandElements.Count -1) `
97-
-or $currentAstNode.CommandElements[($j + 1)] -is [System.Management.Automation.Language.CommandParameterAst])
98-
{
99-
# switch param (no value)
100-
$paramRef.Value = $null
101-
}
102-
else
103-
{
104-
# regular param (has value)
105-
$paramRef.Value = $currentAstNode.CommandElements[($j + 1)].Extent.Text
106-
}
107-
142+
# grab the parameter name with no dash value
143+
# the extent offsets here include the dash, so add +1 to the starting values
144+
# construct the parameter object with location details
145+
$paramRef.Name = $currentAstNodeCmdElement.ParameterName
108146
$paramRef.FullPath = $cmdletRef.FullPath
109147
$paramRef.FileName = $cmdletRef.FileName
110148
$paramRef.StartLine = $currentAstNodeCmdElement.Extent.StartLineNumber
111-
$paramRef.StartColumn = $currentAstNodeCmdElement.Extent.StartColumnNumber
149+
$paramRef.StartColumn = ($currentAstNodeCmdElement.Extent.StartColumnNumber + 1)
112150
$paramRef.EndLine = $currentAstNodeCmdElement.Extent.EndLineNumber
113151
$paramRef.EndPosition = $currentAstNodeCmdElement.Extent.EndColumnNumber
114-
$paramRef.StartOffset = $currentAstNodeCmdElement.Extent.StartOffset
152+
$paramRef.StartOffset = ($currentAstNodeCmdElement.Extent.StartOffset + 1)
115153
$paramRef.EndOffset = $currentAstNodeCmdElement.Extent.EndOffset
116154
$paramRef.Location = "{0}:{1}:{2}" -f $paramRef.FileName, $paramRef.StartLine, $paramRef.StartColumn
117155

@@ -121,6 +159,49 @@ function Find-CmdletsInFile
121159
-and $currentAstNodeCmdElement.Splatted -eq $true)
122160
{
123161
$cmdletRef.HasSplattedArguments = $true
162+
163+
# grab the splatted parameter name without the '@' character prefix.
164+
# we can then look this up in our known hashtable variables table.
165+
$hashtableVariableName = $currentAstNodeCmdElement.VariablePath.UserPath
166+
167+
if ($hashtableVariables.ContainsKey($hashtableVariableName))
168+
{
169+
foreach ($splattedParameter in $hashtableVariables[$hashtableVariableName])
170+
{
171+
$paramRef = New-Object -TypeName CommandReferenceParameter
172+
173+
# add new parameter, similar to above, however a hashtable key name is the parameter name.
174+
$paramRef.Name = $splattedParameter.Value
175+
$paramRef.FullPath = $cmdletRef.FullPath
176+
$paramRef.FileName = $cmdletRef.FileName
177+
178+
if ($splattedParameter.Extent.Text[0] -ne $doubleQuoteCharacter -and $splattedParameter.Extent.Text[0] -ne $singleQuoteCharacter)
179+
{
180+
# normal hash table key (not wrapped in quote characters)
181+
$paramRef.StartLine = $splattedParameter.Extent.StartLineNumber
182+
$paramRef.StartColumn = $splattedParameter.Extent.StartColumnNumber
183+
$paramRef.EndLine = $splattedParameter.Extent.EndLineNumber
184+
$paramRef.EndPosition = $splattedParameter.Extent.EndColumnNumber
185+
$paramRef.StartOffset = $splattedParameter.Extent.StartOffset
186+
$paramRef.EndOffset = $splattedParameter.Extent.EndOffset
187+
}
188+
else
189+
{
190+
# hash table key wrapped in quotes
191+
# use special offset handling to account for quote wrapper characters.
192+
$paramRef.StartLine = $splattedParameter.Extent.StartLineNumber
193+
$paramRef.StartColumn = ($splattedParameter.Extent.StartColumnNumber + 1)
194+
$paramRef.EndLine = $splattedParameter.Extent.EndLineNumber
195+
$paramRef.EndPosition = ($splattedParameter.Extent.EndColumnNumber - 1)
196+
$paramRef.StartOffset = ($splattedParameter.Extent.StartOffset + 1)
197+
$paramRef.EndOffset = ($splattedParameter.Extent.EndOffset - 1)
198+
}
199+
200+
$paramRef.Location = "{0}:{1}:{2}" -f $paramRef.FileName, $paramRef.StartLine, $paramRef.StartColumn
201+
202+
$cmdletRef.Parameters.Add($paramRef)
203+
}
204+
}
124205
}
125206
}
126207
}

powershell-module/Az.Tools.Migration/Functions/Private/Invoke-ModuleUpgradeStep.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ function Invoke-ModuleUpgradeStep
6060

6161
# safety check
6262
# ensure that the file offsets are an exact match.
63-
Confirm-StringBuilderSubstring -FileContent $FileContent -Substring ("-{0}" -f $Step.Original) `
63+
Confirm-StringBuilderSubstring -FileContent $FileContent -Substring $Step.Original `
6464
-StartOffset $Step.SourceCommandParameter.StartOffset -EndOffset $Step.SourceCommandParameter.EndOffset
6565

6666
# replacement code
6767
$null = $FileContent.Remove($Step.SourceCommandParameter.StartOffset, ($Step.SourceCommandParameter.EndOffset - $Step.SourceCommandParameter.StartOffset));
68-
$null = $FileContent.Insert($Step.SourceCommandParameter.StartOffset, ("-{0}" -f $Step.Replacement));
68+
$null = $FileContent.Insert($Step.SourceCommandParameter.StartOffset, $Step.Replacement);
6969
}
7070
default
7171
{

powershell-module/Az.Tools.Migration/Functions/Public/New-AzUpgradeModulePlan.ps1

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,10 @@ function New-AzUpgradeModulePlan
244244
$cmdletUpgrade.StartOffset = $rmCmdlet.StartOffset
245245
$cmdletUpgrade.Location = $rmCmdlet.Location
246246

247-
if ($rmCmdlet.HasSplattedArguments -eq $false)
248-
{
249-
$cmdletUpgrade.PlanResultReason = "Command can be automatically upgraded."
250-
$cmdletUpgrade.PlanResult = [PlanResultReasonCode]::ReadyToUpgrade
251-
$cmdletUpgrade.PlanSeverity = [DiagnosticSeverity]::Information
252-
$planSteps.Add($cmdletUpgrade)
253-
}
254-
else
255-
{
256-
$cmdletUpgrade.PlanResultReason = "Cmdlet invocation uses splatted parameters. Consider unrolling to allow automated parameter upgrade checks."
257-
$cmdletUpgrade.PlanResult = [PlanResultReasonCode]::WarningSplattedParameters
258-
$cmdletUpgrade.PlanSeverity = [DiagnosticSeverity]::Warning
259-
$planWarningSteps.Add($cmdletUpgrade)
260-
}
247+
$cmdletUpgrade.PlanResultReason = "Command can be automatically upgraded."
248+
$cmdletUpgrade.PlanResult = [PlanResultReasonCode]::ReadyToUpgrade
249+
$cmdletUpgrade.PlanSeverity = [DiagnosticSeverity]::Information
250+
$planSteps.Add($cmdletUpgrade)
261251

262252
# check if parameters need to be updated
263253

0 commit comments

Comments
 (0)