Skip to content

Commit 598d4be

Browse files
committed
Use propsed snippet and additonal tests
1 parent 729a76d commit 598d4be

File tree

2 files changed

+141
-100
lines changed

2 files changed

+141
-100
lines changed

Source/Private/GetCommandAlias.ps1

Lines changed: 109 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,126 @@
1-
function GetCommandAlias {
2-
<#
3-
.SYNOPSIS
4-
Parses one or more files for aliases and returns a list of alias names.
5-
#>
6-
[CmdletBinding()]
7-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification='There are issues reported for this rule in PSScriptAnalyzer repository when setting variables in a ForEach-Object. This suppression should be removed once that issue in PSScriptAnalyzer is resolved.')]
8-
param(
9-
# Path to the PSM1 file to amend
10-
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)]
11-
[System.Management.Automation.Language.Ast]$AST
12-
)
13-
begin {
14-
$RemovedAliases = @()
15-
$Result = [Ordered]@{}
1+
# This is used only to parse the parameters to New|Set|Remove-Alias
2+
class AliasParameterVisitor : System.Management.Automation.Language.AstVisitor {
3+
[string]$Parameter = $null
4+
[string]$Command = $null
5+
[string]$Name = $null
6+
[string]$Value = $null
7+
[string]$Scope = $null
8+
9+
# Parameter Names
10+
[System.Management.Automation.Language.AstVisitAction] VisitCommandParameter([System.Management.Automation.Language.CommandParameterAst]$ast) {
11+
$this.Parameter = $ast.ParameterName
12+
return [System.Management.Automation.Language.AstVisitAction]::Continue
1613
}
17-
process {
18-
foreach($function in $AST.FindAll(
19-
{ $Args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] },
20-
$false )
21-
) {
22-
$Result[$function.Name] = $function.Body.ParamBlock.Attributes.Where{
23-
$_.TypeName.Name -eq "Alias" }.PositionalArguments.Value
24-
}
2514

26-
<#
27-
Search for New-Alias, Set-Alias, and Remove-Alias.
15+
# Parameter Values
16+
[System.Management.Automation.Language.AstVisitAction] VisitStringConstantExpression([System.Management.Automation.Language.StringConstantExpressionAst]$ast) {
17+
# The FIRST command element is always the command name
18+
if (!$this.Command) {
19+
$this.Command = $ast.Value
20+
return [System.Management.Automation.Language.AstVisitAction]::Continue
21+
} else {
22+
switch ($this.Parameter) {
23+
"Scope" {
24+
$this.Scope = $ast.Value
25+
}
26+
"Name" {
27+
$this.Name = $ast.Value
28+
}
29+
"Value" {
30+
$this.Value = $ast.Value
31+
}
32+
"Force" {
33+
if ($ast.Value) {
34+
# Force parameter was passed as named parameter with a positional parameter after it which is alias name
35+
$this.Name = $ast.Value
36+
}
37+
}
38+
default {
39+
if (!$this.Parameter) {
40+
# For bare arguments, the order is Name, Value:
41+
if (!$this.Name) {
42+
$this.Name = $ast.Value
43+
} else {
44+
$this.Value = $ast.Value
45+
}
46+
}
47+
}
48+
}
2849

29-
The parents for a script level command is:
30-
- PipelineAst
31-
- NamedBlockAst
32-
- ScriptBlockAst
33-
- $null
50+
$this.Parameter = $null
3451

35-
While a command in a function has more parents:
36-
- PipelineAst
37-
- NamedBlockAst
38-
- ScriptBlockAst
39-
- FunctionDefinitionAst
40-
- ...
41-
#>
42-
$astFilter = {
43-
$args[0] -is [System.Management.Automation.Language.CommandAst] `
44-
-and $args[0].CommandElements.StringConstantType -eq 'BareWord' `
45-
-and $args[0].CommandElements.Value -match '(New|Set|Remove)-Alias' `
46-
-and $null -eq $args[0].Parent.Parent.Parent.Parent # Make sure it exist at script level
52+
# If we have enough information, stop the visit
53+
# For -Scope global or Remove-Alias, we don't want to export these
54+
if ($this.Name -and $this.Command -eq "Remove-Alias") {
55+
$this.Command = "Remove-Alias"
56+
return [System.Management.Automation.Language.AstVisitAction]::StopVisit
57+
} elseif ($this.Name -and $this.Scope -eq "Global") {
58+
return [System.Management.Automation.Language.AstVisitAction]::StopVisit
59+
}
60+
return [System.Management.Automation.Language.AstVisitAction]::Continue
4761
}
62+
}
4863

49-
$commandAsts = $AST.FindAll($astFilter, $true)
50-
51-
foreach ($aliasCommandAst in $commandAsts) {
52-
<#
53-
Named parameter 'Name' has position parameter 0 in all three commands
54-
(New-Alias, Set-Alias, and Remove-Alias). That means that the alias
55-
name is in either item 1 if positional or in any other element in the
56-
array if named.
57-
58-
Scope must always be a named parameter.
59-
#>
60-
61-
$isGlobal = $false
62-
63-
# Evaluate if the command uses named parameter Scope set to Global. Always start at second element.
64-
for ($i=1; $i -lt $aliasCommandAst.CommandElements.Count - 1; $i++) {
65-
if ($aliasCommandAst.CommandElements[$i] -is [System.Management.Automation.Language.CommandParameterAst] `
66-
-and $aliasCommandAst.CommandElements[$i].ParameterName -eq 'Scope'
67-
) {
68-
# Value (the scope) is in the next item in the array.
69-
if ($aliasCommandAst.CommandElements[$i + 1].Value -ieq 'Global') {
70-
$isGlobal = $true
71-
}
72-
}
73-
}
64+
[AliasParameterVisitor] Clear() {
65+
$this.Command = $null
66+
$this.Parameter = $null
67+
$this.Name = $null
68+
$this.Value = $null
69+
$this.Scope = $null
70+
return $this
71+
}
72+
}
7473

75-
if (-not $isGlobal) {
76-
$aliasName = $null
74+
# This visits everything at the top level of the script
75+
class AliasVisitor : System.Management.Automation.Language.AstVisitor {
76+
[System.Collections.Hashtable]$Aliases = @{}
77+
[AliasParameterVisitor]$Parameters = @{}
7778

78-
# Evaluate if the command uses positional parameter 1.
79-
if ($aliasCommandAst.CommandElements[1] -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
80-
# Value is in second item in the array.
81-
$aliasName = $aliasCommandAst.CommandElements[1].Value
82-
} else {
83-
# Evaluate if the command uses named parameter Name. Always start at second element.
84-
for ($i=1; $i -lt $aliasCommandAst.CommandElements.Count - 1; $i++) {
85-
if ($aliasCommandAst.CommandElements[$i] -is [System.Management.Automation.Language.CommandParameterAst] `
86-
-and $aliasCommandAst.CommandElements[$i].ParameterName -eq 'Name'
87-
) {
88-
# Value (the alias name) is in the next item in the array.
89-
$aliasName = $aliasCommandAst.CommandElements[$i + 1].Value
90-
}
91-
}
92-
}
79+
# The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function
80+
[System.Management.Automation.Language.AstVisitAction] VisitFunctionDefinition([System.Management.Automation.Language.FunctionDefinitionAst]$ast) {
81+
$this.Aliases[$ast.Name] = @($ast.Body.ParamBlock.Attributes.Where{ $_.TypeName.Name -eq "Alias" }.PositionalArguments.Value)
82+
return [System.Management.Automation.Language.AstVisitAction]::SkipChildren
83+
}
9384

94-
if ($aliasCommandAst.CommandElements[0].Value -eq 'Remove-Alias') {
95-
Write-Warning -Message "Found an alias '$aliasName' that is removed using Remove-Alias, assuming the alias should not be exported."
85+
# Top-level commands matter, but only if they're alias commands
86+
[System.Management.Automation.Language.AstVisitAction] VisitCommand([System.Management.Automation.Language.CommandAst]$ast) {
87+
if ($ast.CommandElements[0].Value -imatch "(New|Set|Remove)-Alias") {
88+
$ast.Visit($this.Parameters.Clear())
89+
if ($this.Parameters.Command -ieq "Remove-Alias") {
90+
Write-Warning -Message "Found an alias '$($this.Parameters.Name)' that is removed using $($this.Parameters.Command), assuming the alias should not be exported."
9691

97-
<#
98-
Save the alias name to the end so that it can be removed from
99-
the resulting list of aliases.
100-
#>
101-
$RemovedAliases += $aliasName
102-
} else {
103-
$Result[$aliasName] = $aliasName
92+
$this.Aliases.Remove($this.Parameters.Name)
93+
} elseif ($this.Parameters.Scope -ine 'Global') {
94+
if ($this.Parameters.Name -notin $this.Aliases.Keys)
95+
{
96+
$this.Aliases[$this.Parameters.Name] = $this.Parameters.Name
10497
}
10598
}
10699
}
100+
return [System.Management.Automation.Language.AstVisitAction]::SkipChildren
107101
}
108-
end {
109-
# Return the aliases after filtering out those that was removed by `Remove-Alias`.
110-
$RemovedAliases | ForEach-Object -Process {
111-
$Result.Remove($_)
112-
}
102+
}
113103

114-
$Result
104+
function GetCommandAlias {
105+
<#
106+
.SYNOPSIS
107+
Parses one or more files for aliases and returns a list of alias names.
108+
#>
109+
[CmdletBinding()]
110+
[OutputType([System.Collections.Hashtable])]
111+
param(
112+
# The AST to find aliases in
113+
[Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)]
114+
[System.Management.Automation.Language.Ast]$Ast
115+
)
116+
begin {
117+
$Visitor = [AliasVisitor]::new()
118+
}
119+
process {
120+
$Ast.Visit($Visitor)
121+
}
122+
end {
123+
$Visitor.Aliases
115124
}
116125
}
126+

Tests/Private/GetCommandAlias.Tests.ps1

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ Describe "GetCommandAlias" {
4949
}.Ast
5050
}
5151

52-
$Result.Keys | Should -Be "Test-Alias", "TestAlias"
52+
$Result.Keys | Should -Contain "Test-Alias"
53+
$Result.Keys | Should -Contain "TestAlias"
5354
$Result["Test-Alias"] | Should -Be "TA","TAlias"
5455
$Result["TestAlias"] | Should -Be "T"
5556
}
@@ -198,4 +199,34 @@ Describe "GetCommandAlias" {
198199
Assert-MockCalled -CommandName Write-Warning -Exactly -Times 1 -Scope It -ModuleName 'ModuleBuilder'
199200
}
200201
}
202+
203+
Context "Parsing Code For unusual aliases" {
204+
BeforeAll {
205+
# Must write a mock module script file and parse it to replicate real conditions
206+
"
207+
New-Alias -Value Remove-Alias rma
208+
209+
Set-Alias -Force rmal Remove-Alias
210+
211+
# Should not be returned since it is Global
212+
Set-Alias -Scope Global rma Remove-Alias
213+
214+
# Duplicate with first Set-Alias above, should not be part of the result
215+
New-Alias -Name rmal Remove-Alias
216+
" | Out-File -FilePath "$TestDrive/MockBuiltModule.psm1" -Encoding ascii -Force
217+
}
218+
219+
It "returns a hashtable with correct aliases" {
220+
$Result = InModuleScope ModuleBuilder {
221+
$ParseErrors, $Tokens = $null
222+
$mockAST = [System.Management.Automation.Language.Parser]::ParseFile("$TestDrive/MockBuiltModule.psm1", [ref]$Tokens, [ref]$ParseErrors)
223+
224+
GetCommandAlias -Ast $mockAST
225+
}
226+
227+
$Result.Count | Should -Be 2
228+
$Result['rma'] | Should -Be 'rma'
229+
$Result['rmal'] | Should -Be 'rmal'
230+
}
231+
}
201232
}

0 commit comments

Comments
 (0)