1+ function Format-PSMDParameter
2+ {
3+ <#
4+ . SYNOPSIS
5+ Formats the parameter block on commands.
6+
7+ . DESCRIPTION
8+ Formats the parameter block on commands.
9+ This function will convert legacy functions that have their parameters straight behind their command name.
10+ It also fixes missing CmdletBinding attributes.
11+
12+ Nested commands will also be affected.
13+
14+ . PARAMETER FullName
15+ The file to process
16+
17+ . PARAMETER DisableCache
18+ By default, this command caches the results of its execution in the PSFramework result cache.
19+ This information can then be retrieved for the last command to do so by running Get-PSFResultCache.
20+ Setting this switch disables the caching of data in the cache.
21+
22+ . PARAMETER Confirm
23+ If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
24+
25+ . PARAMETER WhatIf
26+ If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
27+
28+ . EXAMPLE
29+ PS C:\> Get-ChildItem .\functions\*\*.ps1 | Set-PSMDCmdletBinding
30+
31+ Updates all commands in the module to have a cmdletbinding attribute.
32+ #>
33+ [CmdletBinding (SupportsShouldProcess = $true )]
34+ param (
35+ [Parameter (Mandatory = $true , ValueFromPipeline = $true , ValueFromPipelineByPropertyName = $true )]
36+ [string []]
37+ $FullName ,
38+
39+ [switch ]
40+ $DisableCache
41+ )
42+
43+ begin
44+ {
45+ # region Utility functions
46+ function Invoke-AstWalk
47+ {
48+ [CmdletBinding ()]
49+ param (
50+ $Ast ,
51+
52+ [string []]
53+ $Command ,
54+
55+ [string []]
56+ $Name ,
57+
58+ [string ]
59+ $NewName ,
60+
61+ [bool ]
62+ $IsCommand ,
63+
64+ [bool ]
65+ $NoAlias
66+ )
67+
68+ # Write-PSFMessage -Level Host -Message "Processing $($Ast.Extent.StartLineNumber) | $($Ast.Extent.File) | $($Ast.GetType().FullName)"
69+ $typeName = $Ast.GetType ().FullName
70+
71+ switch ($typeName )
72+ {
73+ " System.Management.Automation.Language.FunctionDefinitionAst"
74+ {
75+ # region Has no param block
76+ if ($null -eq $Ast.Body.ParamBlock )
77+ {
78+ $baseIndent = $Ast.Extent.Text.Split (" `n " )[0 ] -replace " ^(\s{0,}).*" , ' $1'
79+ $indent = $baseIndent + " `t "
80+
81+ # Kill explicit parameter section behind name
82+ $startIndex = " function " .Length + $Ast.Name.Length
83+ $endIndex = $Ast.Extent.Text.IndexOf (" {" )
84+ Add-FileReplacement - Path $ast.Extent.File - Start ($Ast.Extent.StartOffset + $startIndex ) - Length ($endIndex - $startIndex ) - NewContent " `n "
85+
86+ $baseParam = @"
87+ $ ( $indent ) [CmdletBinding()]
88+ $ ( $indent ) param (
89+ {0}
90+ $ ( $indent ) )
91+ "@
92+ $parameters = @ ()
93+ $paramIndent = $indent + " `t "
94+ foreach ($parameter in $Ast.Parameters )
95+ {
96+ $defaultValue = " "
97+ if ($parameter.DefaultValue ) { $defaultValue = " = $ ( $parameter.DefaultValue.Extent.Text ) " }
98+ $values = @ ()
99+ foreach ($attribute in $parameter.Attributes )
100+ {
101+ $values += " $ ( $paramIndent ) $ ( $attribute.Extent.Text ) "
102+ }
103+ $values += " $ ( $paramIndent ) $ ( $parameter.Name.Extent.Text ) $ ( $defaultValue ) "
104+ $parameters += $values -join " `n "
105+ }
106+ $baseParam = $baseParam -f ($parameters -join " ,`n`n " )
107+
108+ Add-FileReplacement - Path $ast.Extent.File - Start $Ast.Body.Extent.StartOffset - Length 1 - NewContent " {`n $ ( $baseParam ) "
109+ }
110+ # endregion Has no param block
111+
112+ # region Has a param block, but no cmdletbinding
113+ if (($null -ne $Ast.Body.ParamBlock ) -and (-not ($Ast.Body.ParamBlock.Attributes | Where-Object TypeName -Like " CmdletBinding" )))
114+ {
115+ $text = [System.IO.File ]::ReadAllText($Ast.Extent.File )
116+
117+ $index = $Ast.Body.ParamBlock.Extent.StartOffset
118+ while (($index -gt 0 ) -and ($text.Substring ($index , 1 ) -ne " `n " )) { $index = $index - 1 }
119+
120+ $indentIndex = $index + 1
121+ $indent = $text.Substring ($indentIndex , ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex ))
122+ Add-FileReplacement - Path $Ast.Body.ParamBlock.Extent.File - Start $indentIndex - Length ($Ast.Body.ParamBlock.Extent.StartOffset - $indentIndex ) - NewContent " $ ( $indent ) [CmdletBinding()]`n $ ( $indent ) "
123+ }
124+ # endregion Has a param block, but no cmdletbinding
125+
126+ Invoke-AstWalk - Ast $Ast.Body - Command $Command - Name $Name - NewName $NewName - IsCommand $false
127+ }
128+ default
129+ {
130+ foreach ($property in $Ast.PSObject.Properties )
131+ {
132+ if ($property.Name -eq " Parent" ) { continue }
133+ if ($null -eq $property.Value ) { continue }
134+
135+ if (Get-Member - InputObject $property.Value - Name GetEnumerator - MemberType Method)
136+ {
137+ foreach ($item in $property.Value )
138+ {
139+ if ($item.PSObject.TypeNames -contains " System.Management.Automation.Language.Ast" )
140+ {
141+ Invoke-AstWalk - Ast $item - Command $Command - Name $Name - NewName $NewName - IsCommand $IsCommand
142+ }
143+ }
144+ continue
145+ }
146+
147+ if ($property.Value.PSObject.TypeNames -contains " System.Management.Automation.Language.Ast" )
148+ {
149+ Invoke-AstWalk - Ast $property.Value - Command $Command - Name $Name - NewName $NewName - IsCommand $IsCommand
150+ }
151+ }
152+ }
153+ }
154+ }
155+
156+ function Add-FileReplacement
157+ {
158+ [CmdletBinding ()]
159+ param (
160+ [string ]
161+ $Path ,
162+
163+ [int ]
164+ $Start ,
165+
166+ [int ]
167+ $Length ,
168+
169+ [string ]
170+ $NewContent
171+ )
172+ Write-PSFMessage - Level Verbose - Message " Change Submitted: $Path | $Start | $Length | $NewContent " - Tag ' update' , ' change' , ' file'
173+
174+ if (-not $globalFunctionHash.ContainsKey ($Path ))
175+ {
176+ $globalFunctionHash [$Path ] = @ ()
177+ }
178+
179+ $globalFunctionHash [$Path ] += New-Object PSObject - Property @ {
180+ Content = $NewContent
181+ Start = $Start
182+ Length = $Length
183+ }
184+ }
185+
186+ function Apply-FileReplacement
187+ {
188+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute (" PSUseApprovedVerbs" , " " )]
189+ [CmdletBinding ()]
190+ param (
191+
192+ )
193+
194+ foreach ($key in $globalFunctionHash.Keys )
195+ {
196+ $value = $globalFunctionHash [$key ] | Sort-Object Start
197+ $content = [System.IO.File ]::ReadAllText($key )
198+
199+ $newString = " "
200+ $currentIndex = 0
201+
202+ foreach ($item in $value )
203+ {
204+ $newString += $content.SubString ($currentIndex , ($item.Start - $currentIndex ))
205+ $newString += $item.Content
206+ $currentIndex = $item.Start + $item.Length
207+ }
208+
209+ $newString += $content.SubString ($currentIndex )
210+
211+ [System.IO.File ]::WriteAllText($key , $newString )
212+ # $newString
213+ }
214+ }
215+
216+ function Write-Issue
217+ {
218+ [CmdletBinding ()]
219+ param (
220+ $Extent ,
221+
222+ $Data ,
223+
224+ [string ]
225+ $Type
226+ )
227+
228+ New-Object PSObject - Property @ {
229+ Type = $Type
230+ Data = $Data
231+ File = $Extent.File
232+ StartLine = $Extent.StartLineNumber
233+ Text = $Extent.Text
234+ }
235+ }
236+ # endregion Utility functions
237+ }
238+ process
239+ {
240+ foreach ($path in $FullName )
241+ {
242+ $globalFunctionHash = @ { }
243+
244+ $tokens = $null
245+ $parsingError = $null
246+ $ast = [System.Management.Automation.Language.Parser ]::ParseFile($path , [ref ]$tokens , [ref ]$parsingError )
247+
248+ Write-PSFMessage - Level VeryVerbose - Message " Ensuring Cmdletbinding for all functions in $path " - Tag ' start' - Target $Name
249+ $issues += Invoke-AstWalk - Ast $ast - Command $Command - Name $Name - NewName $NewName - IsCommand $false
250+
251+ Set-PSFResultCache - InputObject $issues - DisableCache $DisableCache
252+ if ($PSCmdlet.ShouldProcess ($path , " Set CmdletBinding attribute" ))
253+ {
254+ Apply- FileReplacement
255+ }
256+ $issues
257+ }
258+ }
259+ }
0 commit comments