From a289e7fa6f15d4a33b7c385a4384e7d026a4b464 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 25 Apr 2023 18:40:28 +0100 Subject: [PATCH 001/130] CI: Use new Ubuntu 22.04 image and remove deprecated Ubuntu 18.04 (#1847) * Use new Ubuntu and mac images * only use latest image for macos --- .azure-pipelines-ci/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines-ci/ci.yaml b/.azure-pipelines-ci/ci.yaml index 01e893f0c..956139389 100644 --- a/.azure-pipelines-ci/ci.yaml +++ b/.azure-pipelines-ci/ci.yaml @@ -22,10 +22,10 @@ stages: - job: strategy: matrix: - Ubuntu_18_04: - vmImage: ubuntu-18.04 Ubuntu_20_04: vmImage: ubuntu-20.04 + Ubuntu_22_04: + vmImage: ubuntu-22.04 mac_Latest: vmImage: macOS-latest Windows_Server2019_PowerShell_Core: From 76e1bbf27434fabffacc1cec5d5028ffda75544f Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Sun, 7 May 2023 09:20:31 -0700 Subject: [PATCH 002/130] Change double quotes to single in docs where possible (#1911) --- docs/Rules/AlignAssignmentStatement.md | 8 ++++---- docs/Rules/AvoidGlobalAliases.md | 2 +- docs/Rules/AvoidInvokingEmptyMembers.md | 4 ++-- docs/Rules/AvoidOverwritingBuiltInCmdlets.md | 6 +++--- docs/Rules/AvoidShouldContinueWithoutForce.md | 4 ++-- docs/Rules/AvoidUsingComputerNameHardcoded.md | 4 ++-- ...oidUsingConvertToSecureStringWithPlainText.md | 4 ++-- docs/Rules/AvoidUsingEmptyCatchBlock.md | 4 ++-- docs/Rules/AvoidUsingInvokeExpression.md | 2 +- docs/Rules/AvoidUsingWMICmdlet.md | 4 ++-- docs/Rules/AvoidUsingWriteHost.md | 8 ++++---- docs/Rules/DSCUseVerboseMessageInDSCResource.md | 2 +- docs/Rules/PlaceCloseBrace.md | 2 +- docs/Rules/PlaceOpenBrace.md | 2 +- docs/Rules/ProvideCommentHelp.md | 2 +- docs/Rules/ShouldProcess.md | 4 ++-- docs/Rules/UseCompatibleCmdlets.md | 2 +- docs/Rules/UseCompatibleCommands.md | 8 ++++---- docs/Rules/UseCompatibleSyntax.md | 6 +++--- docs/Rules/UseCompatibleTypes.md | 14 +++++++------- docs/Rules/UseDeclaredVarsMoreThanAssignments.md | 8 ++++---- docs/Rules/UseOutputTypeCorrectly.md | 2 +- .../Rules/UseUsingScopeModifierInNewRunspaces.md | 16 ++++++++-------- 23 files changed, 59 insertions(+), 59 deletions(-) diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index 9265e5389..1a3b7426f 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -19,8 +19,8 @@ are aligned or not. Consider the following example in which the key value pairs ```powershell $hashtable = @{ - property1 = "value" - anotherProperty = "another value" + property1 = 'value' + anotherProperty = 'another value' } ``` @@ -28,8 +28,8 @@ Alignment in this case would look like the following. ```powershell $hashtable = @{ - property1 = "value" - anotherProperty = "another value" + property1 = 'value' + anotherProperty = 'another value' } ``` diff --git a/docs/Rules/AvoidGlobalAliases.md b/docs/Rules/AvoidGlobalAliases.md index 84158157c..8ceeeaf45 100644 --- a/docs/Rules/AvoidGlobalAliases.md +++ b/docs/Rules/AvoidGlobalAliases.md @@ -28,7 +28,7 @@ Use other scope modifiers for new aliases. ### Wrong ```powershell -New-Alias -Name Name -Value Value -Scope "Global" +New-Alias -Name Name -Value Value -Scope Global ``` ### Correct diff --git a/docs/Rules/AvoidInvokingEmptyMembers.md b/docs/Rules/AvoidInvokingEmptyMembers.md index 45ad72942..1d5324c0e 100644 --- a/docs/Rules/AvoidInvokingEmptyMembers.md +++ b/docs/Rules/AvoidInvokingEmptyMembers.md @@ -23,13 +23,13 @@ Provide the requested members for a given type or class. ### Wrong ```powershell -$MyString = "abc" +$MyString = 'abc' $MyString.('len'+'gth') ``` ### Correct ```powershell -$MyString = "abc" +$MyString = 'abc' $MyString.('length') ``` diff --git a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md index e25b3b579..e579694b9 100644 --- a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md +++ b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md @@ -26,7 +26,7 @@ following your settings file. @{ 'Rules' = @{ 'PSAvoidOverwritingBuiltInCmdlets' = @{ - 'PowerShellVersion' = @("core-6.1.0-windows") + 'PowerShellVersion' = @('core-6.1.0-windows') } } } @@ -38,8 +38,8 @@ following your settings file. The parameter `PowerShellVersion` is a list of allowlists that ship with PSScriptAnalyzer. -**Note**: The default value for `PowerShellVersion` is `"core-6.1.0-windows"` if PowerShell 6 or -later is installed, and `"desktop-5.1.14393.206-windows"` if it is not. +**Note**: The default value for `PowerShellVersion` is `core-6.1.0-windows` if PowerShell 6 or +later is installed, and `desktop-5.1.14393.206-windows` if it is not. Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major and minor versions of PowerShell are supplied. One can also create a custom settings file as well diff --git a/docs/Rules/AvoidShouldContinueWithoutForce.md b/docs/Rules/AvoidShouldContinueWithoutForce.md index e19b370de..36f5d6061 100644 --- a/docs/Rules/AvoidShouldContinueWithoutForce.md +++ b/docs/Rules/AvoidShouldContinueWithoutForce.md @@ -33,7 +33,7 @@ Function Test-ShouldContinue $MyString = 'blah' ) - if ($PsCmdlet.ShouldContinue("ShouldContinue Query", "ShouldContinue Caption")) + if ($PsCmdlet.ShouldContinue('ShouldContinue Query', 'ShouldContinue Caption')) { ... } @@ -52,7 +52,7 @@ Function Test-ShouldContinue [Switch]$Force ) - if ($Force -or $PsCmdlet.ShouldContinue("ShouldContinue Query", "ShouldContinue Caption")) + if ($Force -or $PsCmdlet.ShouldContinue('ShouldContinue Query', 'ShouldContinue Caption')) { ... } diff --git a/docs/Rules/AvoidUsingComputerNameHardcoded.md b/docs/Rules/AvoidUsingComputerNameHardcoded.md index 7cf3c6d8c..7e4120ceb 100644 --- a/docs/Rules/AvoidUsingComputerNameHardcoded.md +++ b/docs/Rules/AvoidUsingComputerNameHardcoded.md @@ -25,7 +25,7 @@ Remove hard coded computer names. ```powershell Function Invoke-MyRemoteCommand () { - Invoke-Command -Port 343 -ComputerName "hardcoderemotehostname" + Invoke-Command -Port 343 -ComputerName hardcoderemotehostname } ``` @@ -45,7 +45,7 @@ Function Invoke-MyCommand ($ComputerName) ```powershell Function Invoke-MyLocalCommand () { - Invoke-Command -Port 343 -ComputerName "hardcodelocalhostname" + Invoke-Command -Port 343 -ComputerName 'hardcodelocalhostname' } ``` diff --git a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md index 02e2033de..7e4088238 100644 --- a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md +++ b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md @@ -30,14 +30,14 @@ module from the PowerShell Gallery. ### Wrong ```powershell -$UserInput = Read-Host "Please enter your secure code" +$UserInput = Read-Host 'Please enter your secure code' $EncryptedInput = ConvertTo-SecureString -String $UserInput -AsPlainText -Force ``` ### Correct ```powershell -$SecureUserInput = Read-Host "Please enter your secure code" -AsSecureString +$SecureUserInput = Read-Host 'Please enter your secure code' -AsSecureString $EncryptedInput = ConvertFrom-SecureString -String $SecureUserInput $SecureString = ConvertTo-SecureString -String $EncryptedInput ``` diff --git a/docs/Rules/AvoidUsingEmptyCatchBlock.md b/docs/Rules/AvoidUsingEmptyCatchBlock.md index 9d675e7ef..16e5ea5d0 100644 --- a/docs/Rules/AvoidUsingEmptyCatchBlock.md +++ b/docs/Rules/AvoidUsingEmptyCatchBlock.md @@ -41,7 +41,7 @@ try } catch [DivideByZeroException] { - Write-Error "DivideByZeroException" + Write-Error 'DivideByZeroException' } try @@ -50,6 +50,6 @@ try } catch [DivideByZeroException] { - throw "DivideByZeroException" + throw 'DivideByZeroException' } ``` diff --git a/docs/Rules/AvoidUsingInvokeExpression.md b/docs/Rules/AvoidUsingInvokeExpression.md index ba37de8c7..eb51e2ee6 100644 --- a/docs/Rules/AvoidUsingInvokeExpression.md +++ b/docs/Rules/AvoidUsingInvokeExpression.md @@ -26,7 +26,7 @@ Remove the use of `Invoke-Expression`. ### Wrong ```powershell -Invoke-Expression "Get-Process" +Invoke-Expression 'Get-Process' ``` ### Correct diff --git a/docs/Rules/AvoidUsingWMICmdlet.md b/docs/Rules/AvoidUsingWMICmdlet.md index 3adb521d4..3e81b6444 100644 --- a/docs/Rules/AvoidUsingWMICmdlet.md +++ b/docs/Rules/AvoidUsingWMICmdlet.md @@ -48,12 +48,12 @@ Change to the equivalent CIM based cmdlet. ```powershell Get-WmiObject -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-WmiObject -Invoke-WmiMethod -Class Win32_Process -Name "Create" -ArgumentList @{ CommandLine = "notepad.exe" } +Invoke-WmiMethod -Class Win32_Process -Name 'Create' -ArgumentList @{ CommandLine = 'notepad.exe' } ``` ### Correct ```powershell Get-CimInstance -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-CIMInstance -Invoke-CimMethod -ClassName Win32_Process -MethodName "Create" -Arguments @{ CommandLine = "notepad.exe" } +Invoke-CimMethod -ClassName Win32_Process -MethodName 'Create' -Arguments @{ CommandLine = 'notepad.exe' } ``` diff --git a/docs/Rules/AvoidUsingWriteHost.md b/docs/Rules/AvoidUsingWriteHost.md index 44dc220f7..e04d4f9d0 100644 --- a/docs/Rules/AvoidUsingWriteHost.md +++ b/docs/Rules/AvoidUsingWriteHost.md @@ -12,7 +12,7 @@ title: AvoidUsingWriteHost ## Description The use of `Write-Host` is greatly discouraged unless in the use of commands with the `Show` verb. -The `Show` verb explicitly means "show on the screen, with no other possibilities". +The `Show` verb explicitly means 'show on the screen, with no other possibilities'. Commands with the `Show` verb do not have this check applied. @@ -29,7 +29,7 @@ logging or returning one or more objects. function Get-MeaningOfLife { ... - Write-Host "Computing the answer to the ultimate question of life, the universe and everything" + Write-Host 'Computing the answer to the ultimate question of life, the universe and everything' ... Write-Host 42 } @@ -42,13 +42,13 @@ function Get-MeaningOfLife { [CmdletBinding()]Param() # to make it possible to set the VerbosePreference when calling the function ... - Write-Verbose "Computing the answer to the ultimate question of life, the universe and everything" + Write-Verbose 'Computing the answer to the ultimate question of life, the universe and everything' ... Write-Output 42 } function Show-Something { - Write-Host "show something on screen"; + Write-Host 'show something on screen' } ``` diff --git a/docs/Rules/DSCUseVerboseMessageInDSCResource.md b/docs/Rules/DSCUseVerboseMessageInDSCResource.md index cf58a30b0..e8e4c725b 100644 --- a/docs/Rules/DSCUseVerboseMessageInDSCResource.md +++ b/docs/Rules/DSCUseVerboseMessageInDSCResource.md @@ -38,7 +38,7 @@ Function Test-Function { [CmdletBinding()] Param() - Write-Verbose "Verbose output" + Write-Verbose 'Verbose output' ... } ``` diff --git a/docs/Rules/PlaceCloseBrace.md b/docs/Rules/PlaceCloseBrace.md index 4a55e3ad2..8c3ef3fcd 100644 --- a/docs/Rules/PlaceCloseBrace.md +++ b/docs/Rules/PlaceCloseBrace.md @@ -42,7 +42,7 @@ Create violation if there is an empty line before a close brace. #### IgnoreOneLineBlock: bool (Default value is `$true`) Indicates if closed brace pairs in a one line block should be ignored or not. For example, -`$x = if ($true) { "blah" } else { "blah blah" }`, if the property is set to true then the rule +`$x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule doesn't fire a violation. #### NewLineAfter: bool (Default value is `$true`) diff --git a/docs/Rules/PlaceOpenBrace.md b/docs/Rules/PlaceOpenBrace.md index 0c873e6a5..876c25131 100644 --- a/docs/Rules/PlaceOpenBrace.md +++ b/docs/Rules/PlaceOpenBrace.md @@ -46,5 +46,5 @@ Enforce a new line character after an open brace. The default value is true. #### IgnoreOneLineBlock: bool (Default value is `$true`) Indicates if open braces in a one line block should be ignored or not. For example, -` $x = if ($true) { "blah" } else { "blah blah" }`, if the property is set to true then the rule +` $x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule doesn't fire a violation. diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index b9305da14..5c1f225b4 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -30,7 +30,7 @@ Rules = @{ ExportedOnly = $false BlockComment = $true VSCodeSnippetCorrection = $false - Placement = "before" + Placement = 'before' } } ``` diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index 2e50e2764..5fb2a3adb 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -42,7 +42,7 @@ function Set-File [Parameter(Mandatory=$true)] $Path ) - "String" | Out-File -FilePath $Path + 'String' | Out-File -FilePath $Path } ``` @@ -62,7 +62,7 @@ function Set-File [string]$Content ) - if ($PSCmdlet.ShouldProcess($Path, ("Setting content to '{0}'" -f $Content))) + if ($PSCmdlet.ShouldProcess($Path, ('Setting content to '{0}'' -f $Content))) { $Content | Out-File -FilePath $Path } diff --git a/docs/Rules/UseCompatibleCmdlets.md b/docs/Rules/UseCompatibleCmdlets.md index 6a9d62d95..0dcfb22e3 100644 --- a/docs/Rules/UseCompatibleCmdlets.md +++ b/docs/Rules/UseCompatibleCmdlets.md @@ -23,7 +23,7 @@ the following your settings file: @{ 'Rules' = @{ 'PSUseCompatibleCmdlets' = @{ - 'compatibility' = @("core-6.1.0-windows") + 'compatibility' = @('core-6.1.0-windows') } } } diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index 3eb650322..779956391 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -82,7 +82,7 @@ The default profile directory is under the PSScriptAnalzyer module at `$PSScriptRoot/compatibility_profiles` (where `$PSScriptRoot` here refers to the directory containing `PSScriptAnalyzer.psd1`). -The compatibility analysis compares a command used to both a target profile and a "union" profile +The compatibility analysis compares a command used to both a target profile and a 'union' profile (containing all commands available in *any* profile in the profile dir). If a command is not present in the union profile, it is assumed to be locally created and ignored. Otherwise, if a command is present in the union profile but not present in a target, it is deemed to be incompatible with that @@ -126,17 +126,17 @@ Command compatibility diagnostics can be suppressed with an attribute on the `pa scriptblock as with other rules. ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCommands", "")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', '')] ``` The rule can also be suppressed only for particular commands: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCommands", "Start-Service")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'Start-Service')] ``` And also suppressed only for parameters: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCommands", "Import-Module/FullyQualifiedName")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'Import-Module/FullyQualifiedName')] ``` diff --git a/docs/Rules/UseCompatibleSyntax.md b/docs/Rules/UseCompatibleSyntax.md index 5a98ea3ec..ce93d2623 100644 --- a/docs/Rules/UseCompatibleSyntax.md +++ b/docs/Rules/UseCompatibleSyntax.md @@ -22,9 +22,9 @@ PowerShell versions because they aren't able to parse the incompatible syntaxes. PSUseCompatibleSyntax = @{ Enable = $true TargetVersions = @( - "6.0", - "5.1", - "4.0" + '6.0', + '5.1', + '4.0' ) } } diff --git a/docs/Rules/UseCompatibleTypes.md b/docs/Rules/UseCompatibleTypes.md index cd9da007d..836607979 100644 --- a/docs/Rules/UseCompatibleTypes.md +++ b/docs/Rules/UseCompatibleTypes.md @@ -83,8 +83,8 @@ The default profile directory is under the PSScriptAnalzyer module at `$PSScriptRoot/PSCompatibilityCollector/profiles` (where `$PSScriptRoot` here refers to the directory containing `PSScriptAnalyzer.psd1`). -The compatibility analysis compares a type used to both a target profile and a "union" profile -(containing all types available in *any* profile in the profile dir). If a type is not present in +The compatibility analysis compares a type used to both a target profile and a 'union' profile +(containing all types available in _any_ profile in the profile dir). If a type is not present in the union profile, it is assumed to be locally created and ignored. Otherwise, if a type is present in the union profile but not present in a target, it is deemed to be incompatible with that target. @@ -127,11 +127,11 @@ PS> $settings = @{ Rules = @{ PSUseCompatibleTypes = @{ Enable = $true - TargetProfiles = @("win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework") + TargetProfiles = @('win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework') } } } -PS> Invoke-ScriptAnalyzer -Settings $settings -ScriptDefinition '[System.Management.Automation.SemanticVersion]"1.18.0-rc1"' +PS> Invoke-ScriptAnalyzer -Settings $settings -ScriptDefinition '[System.Management.Automation.SemanticVersion]'1.18.0-rc1'' RuleName Severity ScriptName Line Message -------- -------- ---------- ---- ------- @@ -146,17 +146,17 @@ Command compatibility diagnostics can be suppressed with an attribute on the `pa scriptblock as with other rules. ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleTypes", "")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes', '')] ``` The rule can also be suppressed only for particular types: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleTypes", "System.Management.Automation.Security.SystemPolicy")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes', 'System.Management.Automation.Security.SystemPolicy')] ``` And also suppressed only for type members: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCompatibleCommands", "System.Management.Automation.LanguagePrimitives/ConvertTypeNameToPSTypeName")] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'System.Management.Automation.LanguagePrimitives/ConvertTypeNameToPSTypeName')] ``` diff --git a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md index 3f66d7b03..2c37e4efc 100644 --- a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md +++ b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md @@ -15,7 +15,7 @@ Variables that are assigned but not used are not needed. > [!NOTE] > For this rule, the variable must be used within the same scriptblock that it was declared or it -> won't be considered to be "used". +> won't be considered to be 'used'. ## How @@ -28,8 +28,8 @@ Remove the variables that are declared but not used. ```powershell function Test { - $declaredVar = "Declared just for fun" - $declaredVar2 = "Not used" + $declaredVar = 'Declared just for fun' + $declaredVar2 = 'Not used' Write-Output $declaredVar } ``` @@ -39,7 +39,7 @@ function Test ```powershell function Test { - $declaredVar = "Declared just for fun" + $declaredVar = 'Declared just for fun' Write-Output $declaredVar } ``` diff --git a/docs/Rules/UseOutputTypeCorrectly.md b/docs/Rules/UseOutputTypeCorrectly.md index ee531d1b7..64482db93 100644 --- a/docs/Rules/UseOutputTypeCorrectly.md +++ b/docs/Rules/UseOutputTypeCorrectly.md @@ -45,6 +45,6 @@ function Get-Foo Param( ) - return "four" + return 'four' } ``` diff --git a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md index b17a471b5..dfab8d1a2 100644 --- a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md +++ b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md @@ -32,40 +32,40 @@ Within the ScriptBlock, instead of just using a variable from the parent scope, ### Wrong ```powershell -$var = "foo" +$var = 'foo' 1..2 | ForEach-Object -Parallel { $var } ``` ### Correct ```powershell -$var = "foo" +$var = 'foo' 1..2 | ForEach-Object -Parallel { $using:var } ``` ## More correct examples ```powershell -$bar = "bar" -Invoke-Command -ComputerName "foo" -ScriptBlock { $using:bar } +$bar = 'bar' +Invoke-Command -ComputerName 'foo' -ScriptBlock { $using:bar } ``` ```powershell -$bar = "bar" -$s = New-PSSession -ComputerName "foo" +$bar = 'bar' +$s = New-PSSession -ComputerName 'foo' Invoke-Command -Session $s -ScriptBlock { $using:bar } ``` ```powershell # Remark: Workflow is supported on Windows PowerShell only Workflow { - $foo = "foo" + $foo = 'foo' InlineScript { $using:foo } } ``` ```powershell -$foo = "foo" +$foo = 'foo' Start-ThreadJob -ScriptBlock { $using:foo } Start-Job -ScriptBlock {$using:foo } ``` From 60c27ac458a8a5a9887ff897c1ef4235a59dd978 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Sun, 7 May 2023 09:22:54 -0700 Subject: [PATCH 003/130] Copy change from docs PR#143 (#1910) --- docs/Rules/PossibleIncorrectComparisonWithNull.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/PossibleIncorrectComparisonWithNull.md b/docs/Rules/PossibleIncorrectComparisonWithNull.md index 671f4914f..fbeed7318 100644 --- a/docs/Rules/PossibleIncorrectComparisonWithNull.md +++ b/docs/Rules/PossibleIncorrectComparisonWithNull.md @@ -66,7 +66,7 @@ This is how the comparison operator works by-design. But, as demonstrated, this to non-intuitive behavior, especially when the intent is simple test for null. The following example demonstrates the designed behavior of the comparison operator when the -left-hand side is a collection. Each element in the collection is compared the right-hand side +left-hand side is a collection. Each element in the collection is compared to the right-hand side value. When true, that element of the collection is returned. ```powershell From 9f456d05b95421090fa63e1bb7378231291b36b6 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Wed, 24 May 2023 15:01:06 -0500 Subject: [PATCH 004/130] Fix typos in rules (#1913) --- docs/Rules/AvoidUsingComputerNameHardcoded.md | 2 +- docs/Rules/ShouldProcess.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Rules/AvoidUsingComputerNameHardcoded.md b/docs/Rules/AvoidUsingComputerNameHardcoded.md index 7e4120ceb..f6374a305 100644 --- a/docs/Rules/AvoidUsingComputerNameHardcoded.md +++ b/docs/Rules/AvoidUsingComputerNameHardcoded.md @@ -45,7 +45,7 @@ Function Invoke-MyCommand ($ComputerName) ```powershell Function Invoke-MyLocalCommand () { - Invoke-Command -Port 343 -ComputerName 'hardcodelocalhostname' + Invoke-Command -Port 343 -ComputerName hardcodelocalhostname } ``` diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index 5fb2a3adb..86c894553 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -62,7 +62,7 @@ function Set-File [string]$Content ) - if ($PSCmdlet.ShouldProcess($Path, ('Setting content to '{0}'' -f $Content))) + if ($PSCmdlet.ShouldProcess($Path, ("Setting content to '{0}'" -f $Content))) { $Content | Out-File -FilePath $Path } From 5ba330a65af5fa8ce810158f9271e1524ae2edf8 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Wed, 31 May 2023 14:54:41 -0700 Subject: [PATCH 005/130] add demand for compliance job (#1920) --- .ci/releaseBuild.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/releaseBuild.yml b/.ci/releaseBuild.yml index 9878498d9..57042b77c 100644 --- a/.ci/releaseBuild.yml +++ b/.ci/releaseBuild.yml @@ -171,6 +171,9 @@ stages: - job: Compliance_Job pool: name: PowerShell1ES # was Package ES CodeHub Lab E + demands: + - ImageOverride -equals PSMMS2019-Secure + steps: - checkout: self - checkout: ComplianceRepo From fadde3d83c2bac8ade3575fe2eea74a37b69349f Mon Sep 17 00:00:00 2001 From: "microsoft-github-policy-service[bot]" <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:57:35 +0100 Subject: [PATCH 006/130] FabricBot: Onboarding to GitOps.ResourceManagement because of FabricBot decommissioning (#1925) * Add prIssueManagement.yml to onboard repo to GitOps.ResourceManagement as FabricBot replacement Owners of the FabricBot configuration should have received email notification. The same information contained in the email is published internally at: https://aka.ms/gim/fabricbot. Details on the replacement service and the syntax of the new yaml configuration file is available publicly at: https://microsoft.github.io/GitOps/policies/resource-management.html Please review and merge this PR to complete the process of onboarding to the new service. * Deleting fabricbot.json --------- Co-authored-by: microsoft-github-policy-service[bot] <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> --- .github/fabricbot.json | 711 ------------------------ .github/policies/resourceManagement.yml | 92 +++ 2 files changed, 92 insertions(+), 711 deletions(-) delete mode 100644 .github/fabricbot.json create mode 100644 .github/policies/resourceManagement.yml diff --git a/.github/fabricbot.json b/.github/fabricbot.json deleted file mode 100644 index fbe8db42c..000000000 --- a/.github/fabricbot.json +++ /dev/null @@ -1,711 +0,0 @@ -{ - "version": "1.0", - "tasks": [ - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "taskName": "Add needs triage label to new issues", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "opened" - } - }, - { - "operator": "not", - "operands": [ - { - "name": "isPartOfProject", - "parameters": {} - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "isAssignedToSomeone", - "parameters": {} - } - ] - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs: Triage :mag:" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ] - }, - "id": "gEuS61IET", - "disabled": false - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Close duplicate issues", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution - Duplicate" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 1 - } - } - ], - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes." - } - }, - { - "name": "closeIssue", - "parameters": {} - } - ] - }, - "id": "exOradLcY", - "disabled": false - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "id": "Mx1BeKlyM", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution - Answered" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ], - "taskName": "Close Answered Issues" - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "id": "-3BK34t2l", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution - External" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "taskName": "Close External Issues", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "id": "HJ_mnzjns", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution - Fixed" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "taskName": "Close Fixed Issues", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "id": "UXVP56PHc", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution - Won't Fix" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "taskName": "Close Wont Fix", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "id": "eklMZQWVo", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Need Repro Info" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 14 - } - }, - { - "name": "noLabel", - "parameters": { - "label": "Issue - Bug" - } - } - ], - "taskName": "Close Stale Issues ", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "Closing due to inactivity" - } - }, - { - "name": "closeIssue", - "parameters": {} - } - ] - } - } - ], - "userGroups": [] -} diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 000000000..c5262aa28 --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,92 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isIssue + - isOpen + - hasLabel: + label: Resolution - Duplicate + - noActivitySince: + days: 1 + actions: + - addReply: + reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Answered + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - External + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Fixed + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Won't Fix + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Need Repro Info + - noActivitySince: + days: 14 + - isNotLabeledWith: + label: Issue - Bug + actions: + - addReply: + reply: Closing due to inactivity + - closeIssue + eventResponderTasks: [] +onFailure: +onSuccess: From 94c47ba6235aa6693d80d7341a9b75e8044689d3 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Thu, 10 Aug 2023 15:06:02 -0500 Subject: [PATCH 007/130] Sync change from Docs repository (#1929) --- docs/Rules/AlignAssignmentStatement.md | 2 +- docs/Rules/AvoidAssignmentToAutomaticVariable.md | 2 +- docs/Rules/AvoidDefaultValueForMandatoryParameter.md | 2 +- docs/Rules/AvoidDefaultValueSwitchParameter.md | 2 +- docs/Rules/AvoidGlobalAliases.md | 2 +- docs/Rules/AvoidGlobalFunctions.md | 2 +- docs/Rules/AvoidGlobalVars.md | 2 +- docs/Rules/AvoidInvokingEmptyMembers.md | 2 +- docs/Rules/AvoidLongLines.md | 2 +- docs/Rules/AvoidMultipleTypeAttributes.md | 2 +- docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md | 2 +- docs/Rules/AvoidOverwritingBuiltInCmdlets.md | 2 +- docs/Rules/AvoidSemicolonsAsLineTerminators.md | 2 +- docs/Rules/AvoidShouldContinueWithoutForce.md | 2 +- docs/Rules/AvoidTrailingWhitespace.md | 2 +- docs/Rules/AvoidUsingBrokenHashAlgorithms.md | 2 +- docs/Rules/AvoidUsingCmdletAliases.md | 2 +- docs/Rules/AvoidUsingComputerNameHardcoded.md | 2 +- docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md | 2 +- docs/Rules/AvoidUsingDeprecatedManifestFields.md | 2 +- docs/Rules/AvoidUsingDoubleQuotesForConstantString.md | 2 +- docs/Rules/AvoidUsingEmptyCatchBlock.md | 2 +- docs/Rules/AvoidUsingInvokeExpression.md | 2 +- docs/Rules/AvoidUsingPlainTextForPassword.md | 2 +- docs/Rules/AvoidUsingPositionalParameters.md | 2 +- docs/Rules/AvoidUsingUsernameAndPasswordParams.md | 2 +- docs/Rules/AvoidUsingWMICmdlet.md | 6 +++--- docs/Rules/AvoidUsingWriteHost.md | 2 +- docs/Rules/DSCDscExamplesPresent.md | 2 +- docs/Rules/DSCDscTestsPresent.md | 2 +- docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md | 2 +- docs/Rules/DSCStandardDSCFunctionsInResource.md | 2 +- docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md | 2 +- docs/Rules/DSCUseIdenticalParametersForDSC.md | 2 +- docs/Rules/DSCUseVerboseMessageInDSCResource.md | 2 +- docs/Rules/MisleadingBacktick.md | 2 +- docs/Rules/MissingModuleManifestField.md | 2 +- docs/Rules/PlaceCloseBrace.md | 2 +- docs/Rules/PlaceOpenBrace.md | 2 +- docs/Rules/PossibleIncorrectComparisonWithNull.md | 2 +- docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md | 2 +- docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md | 2 +- docs/Rules/ProvideCommentHelp.md | 2 +- docs/Rules/README.md | 2 +- docs/Rules/ReservedCmdletChar.md | 2 +- docs/Rules/ReservedParams.md | 2 +- docs/Rules/ReviewUnusedParameter.md | 2 +- docs/Rules/ShouldProcess.md | 2 +- docs/Rules/UseApprovedVerbs.md | 2 +- docs/Rules/UseBOMForUnicodeEncodedFile.md | 2 +- docs/Rules/UseCmdletCorrectly.md | 2 +- docs/Rules/UseCompatibleCmdlets.md | 2 +- docs/Rules/UseCompatibleCommands.md | 2 +- docs/Rules/UseCompatibleSyntax.md | 2 +- docs/Rules/UseCompatibleTypes.md | 2 +- docs/Rules/UseConsistentIndentation.md | 2 +- docs/Rules/UseConsistentWhitespace.md | 2 +- docs/Rules/UseCorrectCasing.md | 2 +- docs/Rules/UseDeclaredVarsMoreThanAssignments.md | 2 +- docs/Rules/UseLiteralInitializerForHashtable.md | 2 +- docs/Rules/UseOutputTypeCorrectly.md | 2 +- docs/Rules/UsePSCredentialType.md | 2 +- docs/Rules/UseProcessBlockForPipelineCommand.md | 2 +- docs/Rules/UseShouldProcessForStateChangingFunctions.md | 2 +- docs/Rules/UseSingularNouns.md | 2 +- docs/Rules/UseSupportsShouldProcess.md | 2 +- docs/Rules/UseToExportFieldsInManifest.md | 2 +- docs/Rules/UseUTF8EncodingForHelpFile.md | 2 +- docs/Rules/UseUsingScopeModifierInNewRunspaces.md | 2 +- 69 files changed, 71 insertions(+), 71 deletions(-) diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index 1a3b7426f..21d5cfbc7 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -1,7 +1,7 @@ --- description: Align assignment statement ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AlignAssignmentStatement --- diff --git a/docs/Rules/AvoidAssignmentToAutomaticVariable.md b/docs/Rules/AvoidAssignmentToAutomaticVariable.md index 6e5179525..7a978e32a 100644 --- a/docs/Rules/AvoidAssignmentToAutomaticVariable.md +++ b/docs/Rules/AvoidAssignmentToAutomaticVariable.md @@ -1,7 +1,7 @@ --- description: Changing automatic variables might have undesired side effects ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidAssignmentToAutomaticVariable --- diff --git a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md index 8ad74a1a8..71a82605f 100644 --- a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md +++ b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md @@ -1,7 +1,7 @@ --- description: Avoid Default Value For Mandatory Parameter ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueForMandatoryParameter --- diff --git a/docs/Rules/AvoidDefaultValueSwitchParameter.md b/docs/Rules/AvoidDefaultValueSwitchParameter.md index 8a797629d..f08ac7732 100644 --- a/docs/Rules/AvoidDefaultValueSwitchParameter.md +++ b/docs/Rules/AvoidDefaultValueSwitchParameter.md @@ -1,7 +1,7 @@ --- description: Switch Parameters Should Not Default To True ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueSwitchParameter --- diff --git a/docs/Rules/AvoidGlobalAliases.md b/docs/Rules/AvoidGlobalAliases.md index 8ceeeaf45..9b9463196 100644 --- a/docs/Rules/AvoidGlobalAliases.md +++ b/docs/Rules/AvoidGlobalAliases.md @@ -1,7 +1,7 @@ --- description: Avoid global aliases. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalAliases --- diff --git a/docs/Rules/AvoidGlobalFunctions.md b/docs/Rules/AvoidGlobalFunctions.md index 86870b31b..cf4ee8fad 100644 --- a/docs/Rules/AvoidGlobalFunctions.md +++ b/docs/Rules/AvoidGlobalFunctions.md @@ -1,7 +1,7 @@ --- description: Avoid global functions and aliases ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalFunctions --- diff --git a/docs/Rules/AvoidGlobalVars.md b/docs/Rules/AvoidGlobalVars.md index 0927c2b2f..54da52443 100644 --- a/docs/Rules/AvoidGlobalVars.md +++ b/docs/Rules/AvoidGlobalVars.md @@ -1,7 +1,7 @@ --- description: No Global Variables ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalVars --- diff --git a/docs/Rules/AvoidInvokingEmptyMembers.md b/docs/Rules/AvoidInvokingEmptyMembers.md index 1d5324c0e..88665f1af 100644 --- a/docs/Rules/AvoidInvokingEmptyMembers.md +++ b/docs/Rules/AvoidInvokingEmptyMembers.md @@ -1,7 +1,7 @@ --- description: Avoid Invoking Empty Members ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidInvokingEmptyMembers --- diff --git a/docs/Rules/AvoidLongLines.md b/docs/Rules/AvoidLongLines.md index edc5e30e4..eab82ad64 100644 --- a/docs/Rules/AvoidLongLines.md +++ b/docs/Rules/AvoidLongLines.md @@ -1,7 +1,7 @@ --- description: Avoid long lines ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidLongLines --- diff --git a/docs/Rules/AvoidMultipleTypeAttributes.md b/docs/Rules/AvoidMultipleTypeAttributes.md index 03b6af820..5e5dbea69 100644 --- a/docs/Rules/AvoidMultipleTypeAttributes.md +++ b/docs/Rules/AvoidMultipleTypeAttributes.md @@ -1,7 +1,7 @@ --- description: Avoid multiple type specifiers on parameters. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidMultipleTypeAttributes --- diff --git a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md index fa9459275..4ba4d8e9c 100644 --- a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md +++ b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md @@ -1,7 +1,7 @@ --- description: Avoid using null or empty HelpMessage parameter attribute. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidNullOrEmptyHelpMessageAttribute --- diff --git a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md index e579694b9..7ac93454e 100644 --- a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md +++ b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md @@ -1,7 +1,7 @@ --- description: Avoid overwriting built in cmdlets ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidOverwritingBuiltInCmdlets --- diff --git a/docs/Rules/AvoidSemicolonsAsLineTerminators.md b/docs/Rules/AvoidSemicolonsAsLineTerminators.md index 65fa0d940..0a496678c 100644 --- a/docs/Rules/AvoidSemicolonsAsLineTerminators.md +++ b/docs/Rules/AvoidSemicolonsAsLineTerminators.md @@ -1,7 +1,7 @@ --- description: Avoid semicolons as line terminators ms.custom: PSSA v1.21.0 -ms.date: 06/15/2022 +ms.date: 06/28/2023 ms.topic: reference title: AvoidSemicolonsAsLineTerminators --- diff --git a/docs/Rules/AvoidShouldContinueWithoutForce.md b/docs/Rules/AvoidShouldContinueWithoutForce.md index 36f5d6061..1ffd4ed7b 100644 --- a/docs/Rules/AvoidShouldContinueWithoutForce.md +++ b/docs/Rules/AvoidShouldContinueWithoutForce.md @@ -1,7 +1,7 @@ --- description: Avoid Using ShouldContinue Without Boolean Force Parameter ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidShouldContinueWithoutForce --- diff --git a/docs/Rules/AvoidTrailingWhitespace.md b/docs/Rules/AvoidTrailingWhitespace.md index 1decf9a57..2067cee75 100644 --- a/docs/Rules/AvoidTrailingWhitespace.md +++ b/docs/Rules/AvoidTrailingWhitespace.md @@ -1,7 +1,7 @@ --- description: Avoid trailing whitespace ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidTrailingWhitespace --- diff --git a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md index 83b75929c..52510a5e3 100644 --- a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md +++ b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md @@ -1,7 +1,7 @@ --- description: Avoid using broken hash algorithms ms.custom: PSSA v1.21.0 -ms.date: 05/31/2022 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingBrokenHashAlgorithms --- diff --git a/docs/Rules/AvoidUsingCmdletAliases.md b/docs/Rules/AvoidUsingCmdletAliases.md index a2c2b010d..87c13b33c 100644 --- a/docs/Rules/AvoidUsingCmdletAliases.md +++ b/docs/Rules/AvoidUsingCmdletAliases.md @@ -1,7 +1,7 @@ --- description: Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingCmdletAliases --- diff --git a/docs/Rules/AvoidUsingComputerNameHardcoded.md b/docs/Rules/AvoidUsingComputerNameHardcoded.md index f6374a305..6ff30b54a 100644 --- a/docs/Rules/AvoidUsingComputerNameHardcoded.md +++ b/docs/Rules/AvoidUsingComputerNameHardcoded.md @@ -1,7 +1,7 @@ --- description: Avoid Using ComputerName Hardcoded ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingComputerNameHardcoded --- diff --git a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md index 7e4088238..3c651a400 100644 --- a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md +++ b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md @@ -1,7 +1,7 @@ --- description: Avoid Using SecureString With Plain Text ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingConvertToSecureStringWithPlainText --- diff --git a/docs/Rules/AvoidUsingDeprecatedManifestFields.md b/docs/Rules/AvoidUsingDeprecatedManifestFields.md index 9c64d6a0c..bac27c065 100644 --- a/docs/Rules/AvoidUsingDeprecatedManifestFields.md +++ b/docs/Rules/AvoidUsingDeprecatedManifestFields.md @@ -1,7 +1,7 @@ --- description: Avoid Using Deprecated Manifest Fields ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDeprecatedManifestFields --- diff --git a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md index 464f1c0a1..ab732fc48 100644 --- a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md +++ b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md @@ -1,7 +1,7 @@ --- description: Avoid using double quotes if the string is constant. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDoubleQuotesForConstantString --- diff --git a/docs/Rules/AvoidUsingEmptyCatchBlock.md b/docs/Rules/AvoidUsingEmptyCatchBlock.md index 16e5ea5d0..0d0749f06 100644 --- a/docs/Rules/AvoidUsingEmptyCatchBlock.md +++ b/docs/Rules/AvoidUsingEmptyCatchBlock.md @@ -1,7 +1,7 @@ --- description: Avoid Using Empty Catch Block ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingEmptyCatchBlock --- diff --git a/docs/Rules/AvoidUsingInvokeExpression.md b/docs/Rules/AvoidUsingInvokeExpression.md index eb51e2ee6..de35d16a4 100644 --- a/docs/Rules/AvoidUsingInvokeExpression.md +++ b/docs/Rules/AvoidUsingInvokeExpression.md @@ -1,7 +1,7 @@ --- description: Avoid Using Invoke-Expression ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingInvokeExpression --- diff --git a/docs/Rules/AvoidUsingPlainTextForPassword.md b/docs/Rules/AvoidUsingPlainTextForPassword.md index 6f6ba0492..2d53fbbbe 100644 --- a/docs/Rules/AvoidUsingPlainTextForPassword.md +++ b/docs/Rules/AvoidUsingPlainTextForPassword.md @@ -1,7 +1,7 @@ --- description: Avoid Using Plain Text For Password Parameter ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingPlainTextForPassword --- diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index 4cecb2965..6373e11e6 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -1,7 +1,7 @@ --- description: Avoid Using Positional Parameters ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingPositionalParameters --- diff --git a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md index cb8a6cf5b..f7c634ff2 100644 --- a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md +++ b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md @@ -1,7 +1,7 @@ --- description: Avoid Using Username and Password Parameters ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingUsernameAndPasswordParams --- diff --git a/docs/Rules/AvoidUsingWMICmdlet.md b/docs/Rules/AvoidUsingWMICmdlet.md index 3e81b6444..f1e633fe7 100644 --- a/docs/Rules/AvoidUsingWMICmdlet.md +++ b/docs/Rules/AvoidUsingWMICmdlet.md @@ -1,7 +1,7 @@ --- description: Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWMICmdlet --- @@ -17,7 +17,7 @@ The following cmdlets should not be used: - `Get-WmiObject` - `Remove-WmiObject` -- `Invoke-WmiObject` +- `Invoke-WmiMethod` - `Register-WmiEvent` - `Set-WmiInstance` @@ -38,7 +38,7 @@ Change to the equivalent CIM based cmdlet. - `Get-WmiObject` -> `Get-CimInstance` - `Remove-WmiObject` -> `Remove-CimInstance` -- `Invoke-WmiObject` -> `Invoke-CimMethod` +- `Invoke-WmiMethod` -> `Invoke-CimMethod` - `Register-WmiEvent` -> `Register-CimIndicationEvent` - `Set-WmiInstance` -> `Set-CimInstance` diff --git a/docs/Rules/AvoidUsingWriteHost.md b/docs/Rules/AvoidUsingWriteHost.md index e04d4f9d0..31396af81 100644 --- a/docs/Rules/AvoidUsingWriteHost.md +++ b/docs/Rules/AvoidUsingWriteHost.md @@ -1,7 +1,7 @@ --- description: Avoid Using Write-Host ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWriteHost --- diff --git a/docs/Rules/DSCDscExamplesPresent.md b/docs/Rules/DSCDscExamplesPresent.md index 084eb556a..a4ab2c792 100644 --- a/docs/Rules/DSCDscExamplesPresent.md +++ b/docs/Rules/DSCDscExamplesPresent.md @@ -1,7 +1,7 @@ --- description: DSC examples are present ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCDscExamplesPresent --- diff --git a/docs/Rules/DSCDscTestsPresent.md b/docs/Rules/DSCDscTestsPresent.md index 583a8050b..f96ecbc77 100644 --- a/docs/Rules/DSCDscTestsPresent.md +++ b/docs/Rules/DSCDscTestsPresent.md @@ -1,7 +1,7 @@ --- description: Dsc tests are present ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCDscTestsPresent --- diff --git a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md index 2b92d9873..2b0a4e058 100644 --- a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md +++ b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md @@ -1,7 +1,7 @@ --- description: Return Correct Types For DSC Functions ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCReturnCorrectTypesForDSCFunctions --- diff --git a/docs/Rules/DSCStandardDSCFunctionsInResource.md b/docs/Rules/DSCStandardDSCFunctionsInResource.md index 843f2d6c5..9fbc2c672 100644 --- a/docs/Rules/DSCStandardDSCFunctionsInResource.md +++ b/docs/Rules/DSCStandardDSCFunctionsInResource.md @@ -1,7 +1,7 @@ --- description: Use Standard Get/Set/Test TargetResource functions in DSC Resource ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCStandardDSCFunctionsInResource --- diff --git a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md index 5f6f021ab..65a612712 100644 --- a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md @@ -1,7 +1,7 @@ --- description: Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalMandatoryParametersForDSC --- diff --git a/docs/Rules/DSCUseIdenticalParametersForDSC.md b/docs/Rules/DSCUseIdenticalParametersForDSC.md index 35d3bb459..ea72db0ba 100644 --- a/docs/Rules/DSCUseIdenticalParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalParametersForDSC.md @@ -1,7 +1,7 @@ --- description: Use Identical Parameters For DSC Test and Set Functions ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalParametersForDSC --- diff --git a/docs/Rules/DSCUseVerboseMessageInDSCResource.md b/docs/Rules/DSCUseVerboseMessageInDSCResource.md index e8e4c725b..0af52de06 100644 --- a/docs/Rules/DSCUseVerboseMessageInDSCResource.md +++ b/docs/Rules/DSCUseVerboseMessageInDSCResource.md @@ -1,7 +1,7 @@ --- description: Use verbose message in DSC resource ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: DSCUseVerboseMessageInDSCResource --- diff --git a/docs/Rules/MisleadingBacktick.md b/docs/Rules/MisleadingBacktick.md index 3f8950376..02ec622dc 100644 --- a/docs/Rules/MisleadingBacktick.md +++ b/docs/Rules/MisleadingBacktick.md @@ -1,7 +1,7 @@ --- description: Misleading Backtick ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: MisleadingBacktick --- diff --git a/docs/Rules/MissingModuleManifestField.md b/docs/Rules/MissingModuleManifestField.md index d72f09e4b..a508e7191 100644 --- a/docs/Rules/MissingModuleManifestField.md +++ b/docs/Rules/MissingModuleManifestField.md @@ -1,7 +1,7 @@ --- description: Module Manifest Fields ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: MissingModuleManifestField --- diff --git a/docs/Rules/PlaceCloseBrace.md b/docs/Rules/PlaceCloseBrace.md index 8c3ef3fcd..11eefc9d8 100644 --- a/docs/Rules/PlaceCloseBrace.md +++ b/docs/Rules/PlaceCloseBrace.md @@ -1,7 +1,7 @@ --- description: Place close braces ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: PlaceCloseBrace --- diff --git a/docs/Rules/PlaceOpenBrace.md b/docs/Rules/PlaceOpenBrace.md index 876c25131..b3e48e0ce 100644 --- a/docs/Rules/PlaceOpenBrace.md +++ b/docs/Rules/PlaceOpenBrace.md @@ -1,7 +1,7 @@ --- description: Place open braces consistently ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: PlaceOpenBrace --- diff --git a/docs/Rules/PossibleIncorrectComparisonWithNull.md b/docs/Rules/PossibleIncorrectComparisonWithNull.md index fbeed7318..1c9ea8d24 100644 --- a/docs/Rules/PossibleIncorrectComparisonWithNull.md +++ b/docs/Rules/PossibleIncorrectComparisonWithNull.md @@ -1,7 +1,7 @@ --- description: Null Comparison ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectComparisonWithNull --- diff --git a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md index 5737b53f7..7981512ff 100644 --- a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md @@ -1,7 +1,7 @@ --- description: Equal sign is not an assignment operator. Did you mean the equality operator \'-eq\'? ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfAssignmentOperator --- diff --git a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md index 9956b92d9..759252cd3 100644 --- a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md @@ -1,7 +1,7 @@ --- description: \'>\' is not a comparison operator. Use \'-gt\' (greater than) or \'-ge\' (greater or equal). ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfRedirectionOperator --- diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index 5c1f225b4..e8a45b152 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -1,7 +1,7 @@ --- description: Basic Comment Help ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: ProvideCommentHelp --- diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 44062d7c3..4f8a0b4ad 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,7 +1,7 @@ --- description: List of PSScriptAnalyzer rules ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: List of PSScriptAnalyzer rules --- diff --git a/docs/Rules/ReservedCmdletChar.md b/docs/Rules/ReservedCmdletChar.md index fc9dc6496..2fc57ca3e 100644 --- a/docs/Rules/ReservedCmdletChar.md +++ b/docs/Rules/ReservedCmdletChar.md @@ -1,7 +1,7 @@ --- description: Reserved Cmdlet Chars ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: ReservedCmdletChar --- diff --git a/docs/Rules/ReservedParams.md b/docs/Rules/ReservedParams.md index 768b660f3..965881d76 100644 --- a/docs/Rules/ReservedParams.md +++ b/docs/Rules/ReservedParams.md @@ -1,7 +1,7 @@ --- description: Reserved Parameters ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: ReservedParams --- diff --git a/docs/Rules/ReviewUnusedParameter.md b/docs/Rules/ReviewUnusedParameter.md index 309bcd4d2..2408c5535 100644 --- a/docs/Rules/ReviewUnusedParameter.md +++ b/docs/Rules/ReviewUnusedParameter.md @@ -1,7 +1,7 @@ --- description: ReviewUnusedParameter ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: ReviewUnusedParameter --- diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index 86c894553..d35ca8d40 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -1,7 +1,7 @@ --- description: Should Process ms.custom: PSSA v1.21.0 -ms.date: 03/24/2022 +ms.date: 06/28/2023 ms.topic: reference title: ShouldProcess --- diff --git a/docs/Rules/UseApprovedVerbs.md b/docs/Rules/UseApprovedVerbs.md index 932795f77..2f51bfdb4 100644 --- a/docs/Rules/UseApprovedVerbs.md +++ b/docs/Rules/UseApprovedVerbs.md @@ -1,7 +1,7 @@ --- description: Cmdlet Verbs ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseApprovedVerbs --- diff --git a/docs/Rules/UseBOMForUnicodeEncodedFile.md b/docs/Rules/UseBOMForUnicodeEncodedFile.md index 1c4c0f40a..118158773 100644 --- a/docs/Rules/UseBOMForUnicodeEncodedFile.md +++ b/docs/Rules/UseBOMForUnicodeEncodedFile.md @@ -1,7 +1,7 @@ --- description: Use BOM encoding for non-ASCII files ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseBOMForUnicodeEncodedFile --- diff --git a/docs/Rules/UseCmdletCorrectly.md b/docs/Rules/UseCmdletCorrectly.md index 158dade6d..205c6a971 100644 --- a/docs/Rules/UseCmdletCorrectly.md +++ b/docs/Rules/UseCmdletCorrectly.md @@ -1,7 +1,7 @@ --- description: Use Cmdlet Correctly ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCmdletCorrectly --- diff --git a/docs/Rules/UseCompatibleCmdlets.md b/docs/Rules/UseCompatibleCmdlets.md index 0dcfb22e3..93be36a34 100644 --- a/docs/Rules/UseCompatibleCmdlets.md +++ b/docs/Rules/UseCompatibleCmdlets.md @@ -1,7 +1,7 @@ --- description: Use compatible cmdlets ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCmdlets --- diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index 779956391..1fb47eae7 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -1,7 +1,7 @@ --- description: Use compatible commands ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCommands --- diff --git a/docs/Rules/UseCompatibleSyntax.md b/docs/Rules/UseCompatibleSyntax.md index ce93d2623..71974b62a 100644 --- a/docs/Rules/UseCompatibleSyntax.md +++ b/docs/Rules/UseCompatibleSyntax.md @@ -1,7 +1,7 @@ --- description: Use compatible syntax ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleSyntax --- diff --git a/docs/Rules/UseCompatibleTypes.md b/docs/Rules/UseCompatibleTypes.md index 836607979..8917ef5eb 100644 --- a/docs/Rules/UseCompatibleTypes.md +++ b/docs/Rules/UseCompatibleTypes.md @@ -1,7 +1,7 @@ --- description: Use compatible types ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleTypes --- diff --git a/docs/Rules/UseConsistentIndentation.md b/docs/Rules/UseConsistentIndentation.md index de5979ff7..f9b5d2d81 100644 --- a/docs/Rules/UseConsistentIndentation.md +++ b/docs/Rules/UseConsistentIndentation.md @@ -1,7 +1,7 @@ --- description: Use consistent indentation ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseConsistentIndentation --- diff --git a/docs/Rules/UseConsistentWhitespace.md b/docs/Rules/UseConsistentWhitespace.md index 3438c60ff..76119293d 100644 --- a/docs/Rules/UseConsistentWhitespace.md +++ b/docs/Rules/UseConsistentWhitespace.md @@ -1,7 +1,7 @@ --- description: Use whitespaces ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseConsistentWhitespace --- diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 8d8dd2895..7fa3cc5b3 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -1,7 +1,7 @@ --- description: Use exact casing of cmdlet/function/parameter name. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseCorrectCasing --- diff --git a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md index 2c37e4efc..080fbb671 100644 --- a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md +++ b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md @@ -1,7 +1,7 @@ --- description: Extra Variables ms.custom: PSSA v1.21.0 -ms.date: 06/30/2022 +ms.date: 06/28/2023 ms.topic: reference title: UseDeclaredVarsMoreThanAssignments --- diff --git a/docs/Rules/UseLiteralInitializerForHashtable.md b/docs/Rules/UseLiteralInitializerForHashtable.md index d7ff68f80..31462c743 100644 --- a/docs/Rules/UseLiteralInitializerForHashtable.md +++ b/docs/Rules/UseLiteralInitializerForHashtable.md @@ -1,7 +1,7 @@ --- description: Create hashtables with literal initializers ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseLiteralInitializerForHashtable --- diff --git a/docs/Rules/UseOutputTypeCorrectly.md b/docs/Rules/UseOutputTypeCorrectly.md index 64482db93..4ce9994da 100644 --- a/docs/Rules/UseOutputTypeCorrectly.md +++ b/docs/Rules/UseOutputTypeCorrectly.md @@ -1,7 +1,7 @@ --- description: Use OutputType Correctly ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseOutputTypeCorrectly --- diff --git a/docs/Rules/UsePSCredentialType.md b/docs/Rules/UsePSCredentialType.md index 85e7ed2d8..79e1a194b 100644 --- a/docs/Rules/UsePSCredentialType.md +++ b/docs/Rules/UsePSCredentialType.md @@ -1,7 +1,7 @@ --- description: Use PSCredential type. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UsePSCredentialType --- diff --git a/docs/Rules/UseProcessBlockForPipelineCommand.md b/docs/Rules/UseProcessBlockForPipelineCommand.md index 3bb5d2f4c..bcf11a8b4 100644 --- a/docs/Rules/UseProcessBlockForPipelineCommand.md +++ b/docs/Rules/UseProcessBlockForPipelineCommand.md @@ -1,7 +1,7 @@ --- description: Use process block for command that accepts input from pipeline. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseProcessBlockForPipelineCommand --- diff --git a/docs/Rules/UseShouldProcessForStateChangingFunctions.md b/docs/Rules/UseShouldProcessForStateChangingFunctions.md index 84f79bbbf..45017dba8 100644 --- a/docs/Rules/UseShouldProcessForStateChangingFunctions.md +++ b/docs/Rules/UseShouldProcessForStateChangingFunctions.md @@ -1,7 +1,7 @@ --- description: Use ShouldProcess For State Changing Functions ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseShouldProcessForStateChangingFunctions --- diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index 9dc24a017..4e7576bf0 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,7 +1,7 @@ --- description: Cmdlet Singular Noun ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseSingularNouns --- diff --git a/docs/Rules/UseSupportsShouldProcess.md b/docs/Rules/UseSupportsShouldProcess.md index 1c3426366..9d894b85b 100644 --- a/docs/Rules/UseSupportsShouldProcess.md +++ b/docs/Rules/UseSupportsShouldProcess.md @@ -1,7 +1,7 @@ --- description: Use SupportsShouldProcess ms.custom: PSSA v1.21.0 -ms.date: 12/06/2022 +ms.date: 06/28/2023 ms.topic: reference title: UseSupportsShouldProcess --- diff --git a/docs/Rules/UseToExportFieldsInManifest.md b/docs/Rules/UseToExportFieldsInManifest.md index 9b3bbaaef..dcbf890b7 100644 --- a/docs/Rules/UseToExportFieldsInManifest.md +++ b/docs/Rules/UseToExportFieldsInManifest.md @@ -1,7 +1,7 @@ --- description: Use the *ToExport module manifest fields. ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseToExportFieldsInManifest --- diff --git a/docs/Rules/UseUTF8EncodingForHelpFile.md b/docs/Rules/UseUTF8EncodingForHelpFile.md index 9452d2b9b..56b6f0010 100644 --- a/docs/Rules/UseUTF8EncodingForHelpFile.md +++ b/docs/Rules/UseUTF8EncodingForHelpFile.md @@ -1,7 +1,7 @@ --- description: Use UTF8 Encoding For Help File ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseUTF8EncodingForHelpFile --- diff --git a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md index dfab8d1a2..88769d80b 100644 --- a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md +++ b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md @@ -1,7 +1,7 @@ --- description: Use 'Using:' scope modifier in RunSpace ScriptBlocks ms.custom: PSSA v1.21.0 -ms.date: 10/18/2021 +ms.date: 06/28/2023 ms.topic: reference title: UseUsingScopeModifierInNewRunspaces --- From da2c4fa64b313c570e510f0d95e22f930088a46d Mon Sep 17 00:00:00 2001 From: Joel Tipke Date: Fri, 18 Aug 2023 08:44:54 -0700 Subject: [PATCH 008/130] minor documention fixes (#1928) --- README.md | 2 +- Rules/Strings.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75dd8668b..a9ad2ae36 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ To install **PSScriptAnalyzer** from source code: - Import the module ```powershell - Import-Module .\out\PSScriptAnalyzer\PSScriptAnalyzer.psd1 + Import-Module .\out\PSScriptAnalyzer\[version]\PSScriptAnalyzer.psd1 ``` To confirm installation: run `Get-ScriptAnalyzerRule` in the PowerShell console to obtain the diff --git a/Rules/Strings.resx b/Rules/Strings.resx index dbba6e570..db7062d6f 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1069,7 +1069,7 @@ '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal). - When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell. + When switching between different languages it is easy to forget that '>' does not mean 'greater than' in PowerShell. Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal). From 4b976a27035e7b38a6c2b1cb975fa1e29ae92635 Mon Sep 17 00:00:00 2001 From: ImportTaste <53661808+ImportTaste@users.noreply.github.com> Date: Fri, 18 Aug 2023 10:48:10 -0500 Subject: [PATCH 009/130] corrections in AvoidUsingPositionalParameters.md (#1917) --- docs/Rules/AvoidUsingPositionalParameters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index 6373e11e6..2d1dab690 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -24,7 +24,7 @@ supplied. A simple example where the risk of using positional parameters is negl ```powershell Rules = @{ - AvoidUsingPositionalParameters = @{ + PSAvoidUsingPositionalParameters = @{ CommandAllowList = 'az', 'Join-Path' Enable = $true } @@ -33,7 +33,7 @@ Rules = @{ ### Parameters -#### AvoidUsingPositionalParameters: string[] (Default value is 'az') +#### CommandAllowList: string[] (Default value is 'az') Commands to be excluded from this rule. `az` is excluded by default because starting with version 2.40.0 the entrypoint of the AZ CLI became an `az.ps1` script but this script does not have any named parameters and just passes them on using `$args` as is to the Python process that it starts, therefore it is still a CLI and not a PowerShell command. From b0383ee52b45dfd6fbb78aba363e6cb3e1ebc8fe Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 6 Sep 2023 18:48:37 +0100 Subject: [PATCH 010/130] Add the AvoidExclaimOperator rule to warn about the use of the ! negation operator. Fixes #1826 (#1922) * Added the AvoidExclamationPointOperator rule to warn about the use of the negation operator !. Fixes #1826 * Updated license header of Rules/AvoidExclamationPointOperator.cs * Updated the description of the rule in docs Co-authored-by: Christoph Bergmeister * Fix alignment Co-authored-by: Christoph Bergmeister * Update Rules/Strings.resx Co-authored-by: Christoph Bergmeister * Renamed rule from AvoidExclamationPointOperator to AvoidExclaimOperator * Added comment explanation of the replacement suggestion logic and renamed loop variable for clarity --------- Co-authored-by: Christoph Bergmeister --- Engine/Formatter.cs | 1 + Rules/AvoidExclaimOperator.cs | 147 +++++++++++++++++++ Rules/Strings.Designer.cs | 47 +++++- Rules/Strings.resx | 17 ++- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 2 +- Tests/Rules/AvoidExclaimOperator.tests.ps1 | 54 +++++++ docs/Rules/AvoidExclaimOperator.md | 44 ++++++ docs/Rules/README.md | 1 + 8 files changed, 310 insertions(+), 3 deletions(-) create mode 100644 Rules/AvoidExclaimOperator.cs create mode 100644 Tests/Rules/AvoidExclaimOperator.tests.ps1 create mode 100644 docs/Rules/AvoidExclaimOperator.md diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index 3c1325a5d..3db99e79d 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -46,6 +46,7 @@ public static string Format( "PSAvoidUsingCmdletAliases", "PSAvoidUsingDoubleQuotesForConstantString", "PSAvoidSemicolonsAsLineTerminators", + "PSAvoidExclaimOperator", }; var text = new EditableText(scriptDefinition); diff --git a/Rules/AvoidExclaimOperator.cs b/Rules/AvoidExclaimOperator.cs new file mode 100644 index 000000000..5521463ea --- /dev/null +++ b/Rules/AvoidExclaimOperator.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// AvoidExclaimOperator: Checks for use of the exclaim operator + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class AvoidExclaimOperator : ConfigurableRule + { + + /// + /// Construct an object of AvoidExclaimOperator type. + /// + public AvoidExclaimOperator() { + Enable = false; + } + + /// + /// Analyzes the given ast to find the [violation] + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + var diagnosticRecords = new List(); + + IEnumerable foundAsts = ast.FindAll(testAst => testAst is UnaryExpressionAst, true); + if (foundAsts != null) { + var correctionDescription = Strings.AvoidExclaimOperatorCorrectionDescription; + foreach (UnaryExpressionAst unaryExpressionAst in foundAsts) { + if (unaryExpressionAst.TokenKind == TokenKind.Exclaim) { + var replaceWith = "-not"; + // The UnaryExpressionAST should have a single child, the argument that the unary operator is acting upon. + // If the child's extent starts 1 after the parent's extent then there's no whitespace between the exclaim + // token and any variable/expression; in that case the replacement -not should include a space + if (unaryExpressionAst.Child != null && unaryExpressionAst.Child.Extent.StartColumnNumber == unaryExpressionAst.Extent.StartColumnNumber + 1) { + replaceWith = "-not "; + } + var corrections = new List { + new CorrectionExtent( + unaryExpressionAst.Extent.StartLineNumber, + unaryExpressionAst.Extent.EndLineNumber, + unaryExpressionAst.Extent.StartColumnNumber, + unaryExpressionAst.Extent.StartColumnNumber + 1, + replaceWith, + fileName, + correctionDescription + ) + }; + diagnosticRecords.Add(new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidExclaimOperatorError + ), + unaryExpressionAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName, + suggestedCorrections: corrections + )); + } + } + } + return diagnosticRecords; + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidExclaimOperatorCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidExclaimOperatorDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidExclaimOperatorName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} + diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 30d0a7321..f346e9084 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -285,6 +285,51 @@ internal static string AvoidEmptyCatchBlockError { } } + /// + /// Looks up a localized string similar to Avoid exclaim operator. + /// + internal static string AvoidExclaimOperatorCommonName { + get { + return ResourceManager.GetString("AvoidExclaimOperatorCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace ! with -not. + /// + internal static string AvoidExclaimOperatorCorrectionDescription { + get { + return ResourceManager.GetString("AvoidExclaimOperatorCorrectionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The negation operator ! should not be used for readability purposes. Use -not instead.. + /// + internal static string AvoidExclaimOperatorDescription { + get { + return ResourceManager.GetString("AvoidExclaimOperatorDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Avoid using the ! negation operator. + /// + internal static string AvoidExclaimOperatorError { + get { + return ResourceManager.GetString("AvoidExclaimOperatorError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AvoidExclaimOperator. + /// + internal static string AvoidExclaimOperatorName { + get { + return ResourceManager.GetString("AvoidExclaimOperatorName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Avoid global aliases.. /// @@ -1132,7 +1177,7 @@ internal static string AvoidUsingPlainTextForPasswordDescription { } /// - /// Looks up a localized string similar to Parameter '{0}' should not use String type but either SecureString or PSCredential, otherwise it increases the chance to to expose this sensitive information.. + /// Looks up a localized string similar to Parameter '{0}' should not use String type but either SecureString or PSCredential, otherwise it increases the chance to expose this sensitive information.. /// internal static string AvoidUsingPlainTextForPasswordError { get { diff --git a/Rules/Strings.resx b/Rules/Strings.resx index db7062d6f..479ca1f7a 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1191,4 +1191,19 @@ Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'. - + + AvoidExclaimOperator + + + Avoid exclaim operator + + + The negation operator ! should not be used for readability purposes. Use -not instead. + + + Avoid using the ! negation operator + + + Replace ! with -not + + \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 0ca6e569f..960a2fcd5 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -63,7 +63,7 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 68 + $expectedNumRules = 69 if ($PSVersionTable.PSVersion.Major -le 4) { # for PSv3 PSAvoidGlobalAliases is not shipped because diff --git a/Tests/Rules/AvoidExclaimOperator.tests.ps1 b/Tests/Rules/AvoidExclaimOperator.tests.ps1 new file mode 100644 index 000000000..8307aef5a --- /dev/null +++ b/Tests/Rules/AvoidExclaimOperator.tests.ps1 @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $ruleName = "PSAvoidExclaimOperator" + + $ruleSettings = @{ + Enable = $true + } + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = $ruleSettings } + } +} + +Describe "AvoidExclaimOperator" { + Context "When the rule is not enabled explicitly" { + It "Should not find violations" { + $def = '!$true' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def + $violations.Count | Should -Be 0 + } + } + + Context "Given a line with the exclaim operator" { + It "Should find one violation" { + $def = '!$true' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 1 + } + } + + Context "Given a line with the exclaim operator" { + It "Should replace the exclaim operator with the -not operator" { + $def = '!$true' + $expected = '-not $true' + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + } + Context "Given a line with the exlaim operator followed by a space" { + It "Should replace the exclaim operator without adding an additional space" { + $def = '! $true' + $expected = '-not $true' + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + } + Context "Given a line with a string containing an exclamation point" { + It "Should not replace it" { + $def = '$MyVar = "Should not replace!"' + $expected = '$MyVar = "Should not replace!"' + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + } +} \ No newline at end of file diff --git a/docs/Rules/AvoidExclaimOperator.md b/docs/Rules/AvoidExclaimOperator.md new file mode 100644 index 000000000..5f858feca --- /dev/null +++ b/docs/Rules/AvoidExclaimOperator.md @@ -0,0 +1,44 @@ +--- +description: Avoid exclaim operator +ms.custom: PSSA v1.21.0 +ms.date: 06/14/2023 +ms.topic: reference +title: AvoidExclaimOperator +--- +# AvoidExclaimOperator +**Severity Level: Warning** + +## Description + +The negation operator `!` should not be used for readability purposes. Use `-not` instead. + +**Note**: This rule is not enabled by default. The user needs to enable it through settings. + +## How to Fix + +## Example +### Wrong: +```PowerShell +$MyVar = !$true +``` + +### Correct: +```PowerShell +$MyVar = -not $true +``` + +## Configuration + +```powershell +Rules = @{ + PSAvoidExclaimOperator = @{ + Enable = $true + } +} +``` + +### Parameters + +#### Enable: bool (Default value is `$false`) + +Enable or disable the rule during ScriptAnalyzer invocation. \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 4f8a0b4ad..f5c8d8bd3 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -15,6 +15,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidAssignmentToAutomaticVariable](./AvoidAssignmentToAutomaticVariable.md) | Warning | Yes | | | [AvoidDefaultValueForMandatoryParameter](./AvoidDefaultValueForMandatoryParameter.md) | Warning | Yes | | | [AvoidDefaultValueSwitchParameter](./AvoidDefaultValueSwitchParameter.md) | Warning | Yes | | +| [AvoidExclaimOperator](./AvoidExclaimOperator.md) | Warning | No | | | [AvoidGlobalAliases1](./AvoidGlobalAliases.md) | Warning | Yes | | | [AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | Yes | | | [AvoidGlobalVars](./AvoidGlobalVars.md) | Warning | Yes | | From 85ad15b8c2807caa6c6feeb9872d6b16803e1826 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Mon, 11 Sep 2023 20:21:01 +0100 Subject: [PATCH 011/130] Update minimum PowerShell Core version to 7.2.11 as 7.0 is now EOL (#1872) * Update PSScriptAnalyzer.psm1 * use latest patch version 7.2.11 --- Engine/PSScriptAnalyzer.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index 91f096c25..166e48e61 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -9,7 +9,7 @@ $PSModuleRoot = $PSModule.ModuleBase # Import the appropriate nested binary module based on the current PowerShell version $binaryModuleRoot = $PSModuleRoot -[Version] $minimumPowerShellCoreVersion = '7.0.11' +[Version] $minimumPowerShellCoreVersion = '7.2.11' if ($PSVersionTable.PSVersion.Major -ge 6) { $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" # Minimum PowerShell Core version given by PowerShell Core support itself and From bd79460c3d2d5e6fd2da3ebe0bbdd0698f888235 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Mon, 11 Sep 2023 21:07:38 +0100 Subject: [PATCH 012/130] Remove dead code and simplify (#1856) * Remove unused variables * simplify code and remove unused method * remove unused member * more cleanup * more cleanup * more * more --- Engine/CommandInfoCache.cs | 4 +--- Engine/Commands/GetScriptAnalyzerRuleCommand.cs | 5 ++--- Engine/Commands/InvokeFormatterCommand.cs | 6 ------ Engine/Commands/InvokeScriptAnalyzerCommand.cs | 3 +-- Engine/Formatter.cs | 2 +- Engine/Generic/ModuleDependencyHandler.cs | 3 --- Engine/Helper.cs | 12 +++--------- Engine/ScriptAnalyzer.cs | 3 +-- Engine/Settings.cs | 11 ----------- .../Collection/PlatformInformationCollector.cs | 12 ------------ .../Commands/CommandUtilities.cs | 2 -- Rules/AvoidUserNameAndPasswordParams.cs | 4 ++-- Rules/AvoidUsingDeprecatedManifestFields.cs | 3 +-- Rules/CompatibilityRules/UseCompatibleCommands.cs | 3 +-- Rules/UseConsistentIndentation.cs | 5 ----- Rules/UseIdenticalMandatoryParametersDSC.cs | 2 -- Rules/UseToExportFieldsInManifest.cs | 5 ++--- 17 files changed, 15 insertions(+), 70 deletions(-) diff --git a/Engine/CommandInfoCache.cs b/Engine/CommandInfoCache.cs index dbcb41eda..71c37d83c 100644 --- a/Engine/CommandInfoCache.cs +++ b/Engine/CommandInfoCache.cs @@ -15,17 +15,15 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer internal class CommandInfoCache : IDisposable { private readonly ConcurrentDictionary> _commandInfoCache; - private readonly Helper _helperInstance; private readonly RunspacePool _runspacePool; private bool disposed = false; /// /// Create a fresh command info cache instance. /// - public CommandInfoCache(Helper pssaHelperInstance) + public CommandInfoCache() { _commandInfoCache = new ConcurrentDictionary>(); - _helperInstance = pssaHelperInstance; _runspacePool = RunspaceFactory.CreateRunspacePool(1, 10); _runspacePool.Open(); } diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs index 7a9d50561..3219affa7 100644 --- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs +++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs @@ -84,13 +84,12 @@ protected override void BeginProcessing() // Initialize helper Helper.Instance = new Helper( - SessionState.InvokeCommand, - this); + SessionState.InvokeCommand); Helper.Instance.Initialize(); string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, this.SessionState, recurseCustomRulePath); - ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths ? true : false); + ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths); } /// diff --git a/Engine/Commands/InvokeFormatterCommand.cs b/Engine/Commands/InvokeFormatterCommand.cs index d08d8c817..25a2d364e 100644 --- a/Engine/Commands/InvokeFormatterCommand.cs +++ b/Engine/Commands/InvokeFormatterCommand.cs @@ -124,11 +124,5 @@ protected override void StopProcessing() ScriptAnalyzer.Instance.CleanUp(); base.StopProcessing(); } - - private void ValidateInputSettings() - { - // todo implement this - return; - } } } diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 77364ac9c..3be9cd7fc 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -285,8 +285,7 @@ protected override void BeginProcessing() } #endif Helper.Instance = new Helper( - SessionState.InvokeCommand, - this); + SessionState.InvokeCommand); Helper.Instance.Initialize(); var psVersionTable = this.SessionState.PSVariable.GetValue("PSVersionTable") as Hashtable; diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index 3db99e79d..5a93854c5 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -32,7 +32,7 @@ public static string Format( ValidateNotNull(settings, "settings"); ValidateNotNull(cmdlet, "cmdlet"); - Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand, cmdlet); + Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand); Helper.Instance.Initialize(); var ruleOrder = new string[] diff --git a/Engine/Generic/ModuleDependencyHandler.cs b/Engine/Generic/ModuleDependencyHandler.cs index 347b9a9a1..31a43d6ca 100644 --- a/Engine/Generic/ModuleDependencyHandler.cs +++ b/Engine/Generic/ModuleDependencyHandler.cs @@ -22,7 +22,6 @@ public class ModuleDependencyHandler : IDisposable private string moduleRepository; private string tempPath; // path to the user temporary directory private string tempModulePath; // path to temp directory containing modules - Dictionary modulesFound; private string localAppdataPath; private string pssaAppDataPath; private const string symLinkName = "TempModuleDir"; @@ -271,8 +270,6 @@ public ModuleDependencyHandler( ? "PSScriptAnalyzer" : pssaAppDataPath); - modulesFound = new Dictionary(StringComparer.OrdinalIgnoreCase); - // TODO Add PSSA Version in the path symLinkPath = Path.Combine(pssaAppDataPath, symLinkName); SetupPSSAAppData(); diff --git a/Engine/Helper.cs b/Engine/Helper.cs index feea8a5ec..528a3fe88 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -24,7 +24,6 @@ public class Helper #region Private members private CommandInvocationIntrinsics invokeCommand; - private IOutputWriter outputWriter; private readonly static Version minSupportedPSVersion = new Version(3, 0); private Dictionary> ruleArguments; private PSVersionTable psVersionTable; @@ -115,7 +114,7 @@ internal set /// private Helper() { - _commandInfoCacheLazy = new Lazy(() => new CommandInfoCache(pssaHelperInstance: this)); + _commandInfoCacheLazy = new Lazy(() => new CommandInfoCache()); } /// @@ -125,16 +124,11 @@ private Helper() /// A CommandInvocationIntrinsics instance for use in gathering /// information about available commands and aliases. /// - /// - /// An IOutputWriter instance for use in writing output - /// to the PowerShell environment. - /// public Helper( - CommandInvocationIntrinsics invokeCommand, - IOutputWriter outputWriter) : this() + CommandInvocationIntrinsics invokeCommand + ): this() { this.invokeCommand = invokeCommand; - this.outputWriter = outputWriter; } #region Methods diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index d35ab7e0a..1a885eabe 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -168,8 +168,7 @@ public void Initialize( //initialize helper Helper.Instance = new Helper( - runspace.SessionStateProxy.InvokeCommand, - outputWriter); + runspace.SessionStateProxy.InvokeCommand); Helper.Instance.Initialize(); SuppressionPreference suppressionPreference = suppressedOnly diff --git a/Engine/Settings.cs b/Engine/Settings.cs index b13f9405b..a4931978c 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -285,17 +285,6 @@ private Dictionary GetDictionaryFromHashtable(Hashtable hashtabl return dictionary; } - private bool IsStringOrStringArray(object val) - { - if (val is string) - { - return true; - } - - var valArr = val as object[]; - return val == null ? false : valArr.All(x => x is string); - } - private List GetData(object val, string key) { // value must be either string or or an array of strings diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs index 9a7eab952..26aa0ee7a 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs @@ -351,18 +351,6 @@ private Architecture GetOSArchitecture() #endif } - private DotnetRuntime GetDotnetRuntime() - { -#if CoreCLR - // Our CoreCLR is actuall .NET Standard, so we could be loaded into net47 - return RuntimeInformation.FrameworkDescription.StartsWith(".NET Core") - ? DotnetRuntime.Core - : DotnetRuntime.Framework; -#else - return DotnetRuntime.Framework; -#endif - } - /// /// Get the Windows SKU ID of the current PowerShell session. /// diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs index 3e14494f1..6a957f7cc 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs @@ -13,8 +13,6 @@ namespace Microsoft.PowerShell.CrossCompatibility.Commands /// internal static class CommandUtilities { - private const string COMPATIBILITY_ERROR_ID = "CompatibilityAnalysisError"; - public const string MODULE_PREFIX = "PSCompatibility"; /// diff --git a/Rules/AvoidUserNameAndPasswordParams.cs b/Rules/AvoidUserNameAndPasswordParams.cs index 0609505fd..ced0c8ee1 100644 --- a/Rules/AvoidUserNameAndPasswordParams.cs +++ b/Rules/AvoidUserNameAndPasswordParams.cs @@ -86,7 +86,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { yield return new DiagnosticRecord( String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsernameAndPasswordParamsError, funcAst.Name), - GetExtent(usernameAst, passwordAst, ast), GetName(), DiagnosticSeverity.Error, fileName); + GetExtent(usernameAst, passwordAst), GetName(), DiagnosticSeverity.Error, fileName); } } } @@ -111,7 +111,7 @@ private bool IsAttributeOfType(AttributeBaseAst attributeAst, Type type) /// /// /// IScriptExtent - private IScriptExtent GetExtent(ParameterAst usernameAst, ParameterAst passwordAst, Ast scriptAst) + private IScriptExtent GetExtent(ParameterAst usernameAst, ParameterAst passwordAst) { var usrExt = usernameAst.Extent; var pwdExt = passwordAst.Extent; diff --git a/Rules/AvoidUsingDeprecatedManifestFields.cs b/Rules/AvoidUsingDeprecatedManifestFields.cs index 8856745d2..fd3431251 100644 --- a/Rules/AvoidUsingDeprecatedManifestFields.cs +++ b/Rules/AvoidUsingDeprecatedManifestFields.cs @@ -70,10 +70,9 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (value != null) { - Version psVersion = null; // get the version - if (Version.TryParse((value as StringConstantExpressionAst).Value, out psVersion)) + if (Version.TryParse((value as StringConstantExpressionAst).Value, out Version psVersion)) { // if version exists and version less than 3, don't raise rule if (psVersion.Major < 3) diff --git a/Rules/CompatibilityRules/UseCompatibleCommands.cs b/Rules/CompatibilityRules/UseCompatibleCommands.cs index 0f7cfadbf..bad708e2c 100644 --- a/Rules/CompatibilityRules/UseCompatibleCommands.cs +++ b/Rules/CompatibilityRules/UseCompatibleCommands.cs @@ -263,8 +263,7 @@ public static CommandCompatibilityDiagnostic CreateForParameter( PlatformData platform, IScriptExtent extent, string analyzedFileName, - IRule rule, - IEnumerable suggestedCorrections = null) + IRule rule) { string message = string.Format( CultureInfo.CurrentCulture, diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index da5cfb8a5..bbb12bd41 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -563,11 +563,6 @@ private static int ClipNegative(int x) return x > 0 ? x : 0; } - private int GetIndentationColumnNumber(int indentationLevel) - { - return GetIndentation(indentationLevel) + 1; - } - private int GetIndentation(int indentationLevel) { // todo if condition can be evaluated during rule configuration diff --git a/Rules/UseIdenticalMandatoryParametersDSC.cs b/Rules/UseIdenticalMandatoryParametersDSC.cs index 713b86814..56acb5b48 100644 --- a/Rules/UseIdenticalMandatoryParametersDSC.cs +++ b/Rules/UseIdenticalMandatoryParametersDSC.cs @@ -32,7 +32,6 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules public class UseIdenticalMandatoryParametersDSC : IDSCResourceRule { private bool isDSCClassCacheInitialized = false; - private Ast ast; private string fileName; private IDictionary propAttrDict; private IEnumerable resourceFunctions; @@ -94,7 +93,6 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName } // Get the keys in the corresponding mof file - this.ast = ast; this.fileName = fileName; this.propAttrDict = GetKeys(fileName); this.resourceFunctions = Helper.Instance.DscResourceFunctions(ast) diff --git a/Rules/UseToExportFieldsInManifest.cs b/Rules/UseToExportFieldsInManifest.cs index bfd99db9e..9bf612f83 100644 --- a/Rules/UseToExportFieldsInManifest.cs +++ b/Rules/UseToExportFieldsInManifest.cs @@ -69,7 +69,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) foreach(string field in manifestFields) { IScriptExtent extent; - if (!HasAcceptableExportField(field, hashtableAst, ast.Extent.Text, out extent) && extent != null) + if (!HasAcceptableExportField(field, hashtableAst, out extent) && extent != null) { yield return new DiagnosticRecord( GetError(field), @@ -200,10 +200,9 @@ private bool HasNullInExpression(Ast ast) /// /// /// - /// /// /// A boolean value indicating if the the ToExport fields are explicitly set to arrays or not. - private bool HasAcceptableExportField(string key, HashtableAst hast, string scriptText, out IScriptExtent extent) + private bool HasAcceptableExportField(string key, HashtableAst hast, out IScriptExtent extent) { extent = null; foreach (var pair in hast.KeyValuePairs) From 2e77c2a64a4ec262a6905fd843ae526f3a2f1aed Mon Sep 17 00:00:00 2001 From: Peter Vandivier Date: Mon, 11 Sep 2023 18:42:00 -0400 Subject: [PATCH 013/130] PSReservedParams - link about_CommonParameters (#1908) * PSReservedParams - link about_CommonParameters Knowing that I'm not allowed to use certain words is more helpful if I have a list of those forbidden words. * Update docs/Rules/ReservedParams.md * change link markdown to reference-style syntax Co-authored-by: Mikey Lombardi (He/Him) --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Mikey Lombardi (He/Him) --- docs/Rules/ReservedParams.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/Rules/ReservedParams.md b/docs/Rules/ReservedParams.md index 965881d76..2a254683f 100644 --- a/docs/Rules/ReservedParams.md +++ b/docs/Rules/ReservedParams.md @@ -11,7 +11,9 @@ title: ReservedParams ## Description -You cannot use reserved common parameters in an advanced function. +You cannot use [reserved common parameters][01] in an advanced function. + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_commonparameters ## How @@ -24,7 +26,7 @@ Change the name of the parameter. ```powershell function Test { - [CmdletBinding] + [CmdletBinding()] Param ( $ErrorVariable, @@ -38,7 +40,7 @@ function Test ```powershell function Test { - [CmdletBinding] + [CmdletBinding()] Param ( $Err, From 40be5932808940f8cd89f8d8be7c35772fcdb5d5 Mon Sep 17 00:00:00 2001 From: Frode Flaten <3436158+fflaten@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:36:52 +0200 Subject: [PATCH 014/130] Enable suppression of PSAvoidAssignmentToAutomaticVariable for specific variable or parameter (#1896) * add ruleId to PSAvoidAssignmentToAutomaticVariable Enables suppression for specific variable/parameter. Fix #1589 * add tests --- Rules/AvoidAssignmentToAutomaticVariable.cs | 12 +- ...oidAssignmentToAutomaticVariable.tests.ps1 | 126 +++++++++++------- 2 files changed, 81 insertions(+), 57 deletions(-) diff --git a/Rules/AvoidAssignmentToAutomaticVariable.cs b/Rules/AvoidAssignmentToAutomaticVariable.cs index 1f2a784dd..c188da341 100644 --- a/Rules/AvoidAssignmentToAutomaticVariable.cs +++ b/Rules/AvoidAssignmentToAutomaticVariable.cs @@ -62,20 +62,20 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName), - variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName); + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName, variableName); } if (_readOnlyAutomaticVariablesIntroducedInVersion6_0.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { var severity = IsPowerShellVersion6OrGreater() ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error, variableName), - variableExpressionAst.Extent, GetName(), severity, fileName); + variableExpressionAst.Extent, GetName(), severity, fileName, variableName); } if (_writableAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToWritableAutomaticVariableError, variableName), - variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName, variableName); } } @@ -93,20 +93,20 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName), - variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName); + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName, variableName); } if (_readOnlyAutomaticVariablesIntroducedInVersion6_0.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { var severity = IsPowerShellVersion6OrGreater() ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error, variableName), - variableExpressionAst.Extent, GetName(), severity, fileName); + variableExpressionAst.Extent, GetName(), severity, fileName, variableName); } if (_writableAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToWritableAutomaticVariableError, variableName), - variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName, variableName); } } } diff --git a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 index 50aea15cd..8130990c2 100644 --- a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 +++ b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 @@ -7,60 +7,61 @@ BeforeAll { Describe "AvoidAssignmentToAutomaticVariables" { Context "ReadOnly Variables" { + BeforeDiscovery { + $excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Warning' + if ($PSVersionTable.PSVersion.Major -ge 6) + { + $excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Error' + } - $excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Warning' - if ($PSVersionTable.PSVersion.Major -ge 6) - { - $excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Error' + $testCases_AutomaticVariables = @( + @{ VariableName = '?'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'Error' ; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'ExecutionContext'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'false'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'Home'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'Host'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PID'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PSCulture'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PSEdition'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PSHome'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PSUICulture'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'PSVersionTable'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'ShellId'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + @{ VariableName = 'true'; ExpectedSeverity = 'Error'; IsReadOnly = $true } + # Variables introduced only in PowerShell 6+ have a Severity of Warning only + @{ VariableName = 'IsCoreCLR'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } + @{ VariableName = 'IsLinux'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } + @{ VariableName = 'IsMacOS'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } + @{ VariableName = 'IsWindows'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } + @{ VariableName = '_'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'AllNodes'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Args'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ConsoleFilename'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Event'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'EventArgs'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'EventSubscriber'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ForEach'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Input'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Matches'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'MyInvocation'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'NestedPromptLevel'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Profile'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'PSBoundParameters'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'PsCmdlet'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'PSCommandPath'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ReportErrorShowExceptionClass'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ReportErrorShowInnerException'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ReportErrorShowSource'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'ReportErrorShowStackTrace'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'Sender'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'StackTrace'; ExpectedSeverity = 'Warning' } + @{ VariableName = 'This'; ExpectedSeverity = 'Warning' } + ) + + $testCases_ReadOnlyAutomaticVariables = $testCases_AutomaticVariables | Where-Object { $_.IsReadonly } } - $testCases_AutomaticVariables = @( - @{ VariableName = '?'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'Error' ; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'ExecutionContext'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'false'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'Home'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'Host'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PID'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PSCulture'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PSEdition'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PSHome'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PSUICulture'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'PSVersionTable'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'ShellId'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - @{ VariableName = 'true'; ExpectedSeverity = 'Error'; IsReadOnly = $true } - # Variables introduced only in PowerShell 6+ have a Severity of Warning only - @{ VariableName = 'IsCoreCLR'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } - @{ VariableName = 'IsLinux'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } - @{ VariableName = 'IsMacOS'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } - @{ VariableName = 'IsWindows'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true } - @{ VariableName = '_'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'AllNodes'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Args'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ConsoleFilename'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Event'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'EventArgs'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'EventSubscriber'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ForEach'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Input'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Matches'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'MyInvocation'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'NestedPromptLevel'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Profile'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'PSBoundParameters'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'PsCmdlet'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'PSCommandPath'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ReportErrorShowExceptionClass'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ReportErrorShowInnerException'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ReportErrorShowSource'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'ReportErrorShowStackTrace'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'Sender'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'StackTrace'; ExpectedSeverity = 'Warning' } - @{ VariableName = 'This'; ExpectedSeverity = 'Warning' } - ) - - $testCases_ReadOnlyAutomaticVariables = $testCases_AutomaticVariables | Where-Object { $_.IsReadonly } - It "Variable produces warning of Severity " -TestCases $testCases_AutomaticVariables { param ($VariableName, $ExpectedSeverity) @@ -133,6 +134,29 @@ Describe "AvoidAssignmentToAutomaticVariables" { } } } + } + + Context 'Suppression' { + BeforeDiscovery { + $testCases_RuleSuppression = @( + @{ VariableName = 'true'; Type = 'ReadOnlyAutomaticVariableError' } + @{ VariableName = 'IsWindows'; Type = 'ReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error' } + @{ VariableName = 'ForEach'; Type = 'WritableAutomaticVariableError' } + ) + } + + It 'Variable of type can be suppressed by RuleSuppressionId' -TestCases $testCases_RuleSuppression { + $scriptDef = @" +function suppressionTest { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('$ruleName', '$VariableName')] + param( + `$$VariableName + ) +} +"@ + $warnings = @(Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -ExcludeRule PSReviewUnusedParameter) + $warnings.Count | Should -Be 0 + } } } From 772997f6ca780421fb5277818a729007ccd16ec4 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Sun, 17 Sep 2023 19:39:23 +0100 Subject: [PATCH 015/130] Upgrade to use .NET 6 (#1873) --- Engine/Engine.csproj | 10 +++++----- README.md | 2 +- Rules/Rules.csproj | 6 +++--- appveyor.yml | 2 +- build.psm1 | 2 +- global.json | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index 10ebf1222..ac8f389e7 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -2,7 +2,7 @@ 1.21.0 - netcoreapp3.1;net462 + net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer 1.21.0 Engine @@ -18,11 +18,11 @@ portable - + $(DefineConstants);CORECLR - + @@ -63,10 +63,10 @@ - + - + $(DefineConstants);PSV7;CORECLR diff --git a/README.md b/README.md index a9ad2ae36..0e82f34be 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ To install **PSScriptAnalyzer** from source code: ### Requirements -- [.NET Core 3.1.424 SDK](https://www.microsoft.com/net/download/dotnet-core/3.1#sdk-3.1.424) or +- [.NET 6.0.11 containing 6.0.403 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) or newer patch release * If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows. * Optionally but recommended for development: [Visual Studio 2017/2019](https://www.visualstudio.com/downloads) diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 038d4b149..c52b04ad2 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -2,7 +2,7 @@ 1.21.0 - netcoreapp3.1;net462 + net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules 1.21.0 Rules @@ -16,7 +16,7 @@ - + @@ -57,7 +57,7 @@ $(DefineConstants);PSV3;PSV4 - + $(DefineConstants);PSV7;CORECLR diff --git a/appveyor.yml b/appveyor.yml index e311eaa3a..4ba8d02c8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -36,7 +36,7 @@ build_script: if ($env:PowerShellEdition -eq 'PowerShellCore') { Set-Location $env:APPVEYOR_BUILD_FOLDER ./build.ps1 -Configuration "$env:BuildConfiguration" -PSVersion 7 - ./PSCompatibilityCollector/build.ps1 -Configuration "$env:BuildConfiguration" -Framework 'netcoreapp3.1' + ./PSCompatibilityCollector/build.ps1 -Configuration "$env:BuildConfiguration" -Framework 'net6' } test_script: diff --git a/build.psm1 b/build.psm1 index 9f27d99a8..86dfbd763 100644 --- a/build.psm1 +++ b/build.psm1 @@ -203,7 +203,7 @@ function Start-ScriptAnalyzerBuild $framework = 'net462' if ($PSVersion -eq 7) { - $framework = 'netcoreapp3.1' + $framework = 'net6' } # build the appropriate assembly diff --git a/global.json b/global.json index e758de528..6d86a5b04 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.424" + "version": "6.0.403" } } From af02e801de8d4700e0f9c9aea08e1ce8d22a2443 Mon Sep 17 00:00:00 2001 From: ewisniew0 <114481734+ewisniew0@users.noreply.github.com> Date: Sun, 17 Sep 2023 18:43:53 +0000 Subject: [PATCH 016/130] Add ErrorView to SpecialVars.cs (#1865) * add ErrorView to SpecialVars and create test * Changed type of ErrorView --- Engine/SpecialVars.cs | 5 ++++- Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index 26f1afcf7..0b820c0d4 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -91,6 +91,7 @@ static SpecialVars() internal const string ConfirmPreference = "ConfirmPreference"; internal const string ProgressPreference = "ProgressPreference"; internal const string InformationPreference = "InformationPreference"; + internal const string ErrorView = "ErrorView"; internal static readonly string[] PreferenceVariables = new string[] { @@ -101,7 +102,8 @@ static SpecialVars() WarningPreference, ConfirmPreference, ProgressPreference, - InformationPreference + InformationPreference, + ErrorView }; internal static readonly Type[] PreferenceVariableTypes = new Type[] @@ -114,6 +116,7 @@ static SpecialVars() /* ConfirmPreference */ typeof(ConfirmImpact), /* ProgressPreference */ typeof(Enum), /* InformationPreference */ typeof(ActionPreference), + /* ErrorView */ typeof(Enum), //ErrorView type not available on PS3 }; internal enum AutomaticVariable diff --git a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 index 983ac017f..823334afb 100644 --- a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 +++ b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 @@ -72,6 +72,11 @@ function MyFunc2() { } Context "When there are no violations" { + It "No warning is issued for assignment without use of preference variable ErrorView" { + $results = Invoke-ScriptAnalyzer -ScriptDefinition '$ErrorView = NormalView' + $results.Count | Should -Be 0 + } + It "returns no violations" { $noViolations.Count | Should -Be 0 } From 847b146a08a1dd53857b6894ca79a1ad6ca5fc62 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 16 Jan 2024 22:30:00 +0000 Subject: [PATCH 017/130] Generate strongly typed resources as part of build (#1855) * first working version * cleanup * apply change to rules project as well --- Engine/Engine.csproj | 24 +- Engine/Strings.Designer.cs | 684 -------- Rules/Rules.csproj | 25 +- Rules/Strings.Designer.cs | 3330 ------------------------------------ 4 files changed, 30 insertions(+), 4033 deletions(-) delete mode 100644 Engine/Strings.Designer.cs delete mode 100644 Rules/Strings.Designer.cs diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index ac8f389e7..a22df13ee 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -48,20 +48,26 @@ - - - True - True - Strings.resx - - - + - ResXFileCodeGenerator + + MSBuild:Compile Strings.Designer.cs + $(IntermediateOutputPath)\Strings.Designer.cs + CSharp + Microsoft.Windows.PowerShell.ScriptAnalyzer + Strings + + + PrepareResources;$(CompileDependsOn) + + diff --git a/Engine/Strings.Designer.cs b/Engine/Strings.Designer.cs deleted file mode 100644 index 1fed6e234..000000000 --- a/Engine/Strings.Designer.cs +++ /dev/null @@ -1,684 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Windows.PowerShell.ScriptAnalyzer { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Windows.PowerShell.ScriptAnalyzer.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Checking assembly file '{0}' .... - /// - internal static string CheckAssemblyFile { - get { - return ResourceManager.GetString("CheckAssemblyFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checking module '{0}' .... - /// - internal static string CheckModuleName { - get { - return ResourceManager.GetString("CheckModuleName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CommandInfo not found for function: {0}. - /// - internal static string CommandInfoNotFound { - get { - return ResourceManager.GetString("CommandInfoNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "Argument should not be null.".. - /// - internal static string ConfigurableScriptRuleNRE { - get { - return ResourceManager.GetString("ConfigurableScriptRuleNRE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "Cannot find a ConfigurableRuleProperty attribute on property {0}".. - /// - internal static string ConfigurableScriptRulePropertyHasNotAttribute { - get { - return ResourceManager.GetString("ConfigurableScriptRulePropertyHasNotAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileHasInvalidHashtable. - /// - internal static string ConfigurationFileHasInvalidHashtable { - get { - return ResourceManager.GetString("ConfigurationFileHasInvalidHashtable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileHasNoHashTable. - /// - internal static string ConfigurationFileHasNoHashTable { - get { - return ResourceManager.GetString("ConfigurationFileHasNoHashTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileNotFound. - /// - internal static string ConfigurationFileNotFound { - get { - return ResourceManager.GetString("ConfigurationFileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsKeyNotAString. - /// - internal static string ConfigurationKeyNotAString { - get { - return ResourceManager.GetString("ConfigurationKeyNotAString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsValueNotAString. - /// - internal static string ConfigurationValueNotAString { - get { - return ResourceManager.GetString("ConfigurationValueNotAString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsValueWrongFormat. - /// - internal static string ConfigurationValueWrongFormat { - get { - return ResourceManager.GetString("ConfigurationValueWrongFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Writes all diagnostics to WriteObject.. - /// - internal static string DefaultLoggerDescription { - get { - return ResourceManager.GetString("DefaultLoggerDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WriteObjects. - /// - internal static string DefaultLoggerName { - get { - return ResourceManager.GetString("DefaultLoggerName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edge from {0} to {1} already exists.. - /// - internal static string DigraphEdgeAlreadyExists { - get { - return ResourceManager.GetString("DigraphEdgeAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Vertex {0} already exists! Cannot add it to the digraph.. - /// - internal static string DigraphVertexAlreadyExists { - get { - return ResourceManager.GetString("DigraphVertexAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Vertex {0} does not exist in the digraph.. - /// - internal static string DigraphVertexDoesNotExists { - get { - return ResourceManager.GetString("DigraphVertexDoesNotExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot determine line endings as the text probably contain mixed line endings.. - /// - internal static string EditableTextInvalidLineEnding { - get { - return ResourceManager.GetString("EditableTextInvalidLineEnding", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to TextEdit extent not completely contained in EditableText.. - /// - internal static string EditableTextRangeIsNotContained { - get { - return ResourceManager.GetString("EditableTextRangeIsNotContained", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find file '{0}'.. - /// - internal static string FileNotFound { - get { - return ResourceManager.GetString("FileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find the path '{0}'.. - /// - internal static string InvalidPath { - get { - return ResourceManager.GetString("InvalidPath", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings file '{0}' is invalid because it does not contain a hashtable.. - /// - internal static string InvalidProfile { - get { - return ResourceManager.GetString("InvalidProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Key {0} in the settings is not a string.. - /// - internal static string KeyNotString { - get { - return ResourceManager.GetString("KeyNotString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No loggers found.. - /// - internal static string LoggersNotFound { - get { - return ResourceManager.GetString("LoggersNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find rule extension '{0}'.. - /// - internal static string MissingRuleExtension { - get { - return ResourceManager.GetString("MissingRuleExtension", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Temporary module location: {0}.. - /// - internal static string ModuleDepHandlerTempLocation { - get { - return ResourceManager.GetString("ModuleDepHandlerTempLocation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} cannot be set by both positional and named arguments.. - /// - internal static string NamedAndPositionalArgumentsConflictError { - get { - return ResourceManager.GetString("NamedAndPositionalArgumentsConflictError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Named arguments must always come after positional arguments.. - /// - internal static string NamedArgumentsBeforePositionalError { - get { - return ResourceManager.GetString("NamedArgumentsBeforePositionalError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RuleName must not be null.. - /// - internal static string NullRuleNameError { - get { - return ResourceManager.GetString("NullRuleNameError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parse error in script definition: {0} at line {1} column {2}.. - /// - internal static string ParseErrorFormatForScriptDefinition { - get { - return ResourceManager.GetString("ParseErrorFormatForScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parse error in file {0}: {1} at line {2} column {3}.. - /// - internal static string ParserErrorFormat { - get { - return ResourceManager.GetString("ParserErrorFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are too many parser errors in {0}. Please correct them before running ScriptAnalyzer.. - /// - internal static string ParserErrorMessage { - get { - return ResourceManager.GetString("ParserErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are too many parser errors in the script definition. Please correct them before running ScriptAnalyzer.. - /// - internal static string ParserErrorMessageForScriptDefinition { - get { - return ResourceManager.GetString("ParserErrorMessageForScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Column number cannot be less than 1.. - /// - internal static string PositionColumnLessThanOne { - get { - return ResourceManager.GetString("PositionColumnLessThanOne", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line number cannot be less than 1.. - /// - internal static string PositionLineLessThanOne { - get { - return ResourceManager.GetString("PositionLineLessThanOne", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input position should be less than that of the invoking object.. - /// - internal static string PositionRefPosLessThanInputPos { - get { - return ResourceManager.GetString("PositionRefPosLessThanInputPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reference Position should begin before start Position of Range.. - /// - internal static string RangeRefPosShouldStartBeforeRangeStartPos { - get { - return ResourceManager.GetString("RangeRefPosShouldStartBeforeRangeStartPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Start position cannot be before End position.. - /// - internal static string RangeStartPosGreaterThanEndPos { - get { - return ResourceManager.GetString("RangeStartPosGreaterThanEndPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RULE_ERROR. - /// - internal static string RuleErrorMessage { - get { - return ResourceManager.GetString("RuleErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find ScriptAnalyzer rules in the specified path. - /// - internal static string RulesNotFound { - get { - return ResourceManager.GetString("RulesNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Suppression Message Attribute error at line {0} in {1} : {2}. - /// - internal static string RuleSuppressionErrorFormat { - get { - return ResourceManager.GetString("RuleSuppressionErrorFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Suppression Message Attribute error at line {0} in script definition : {1}. - /// - internal static string RuleSuppressionErrorFormatScriptDefinition { - get { - return ResourceManager.GetString("RuleSuppressionErrorFormatScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find any DiagnosticRecord with the Rule Suppression ID {0}.. - /// - internal static string RuleSuppressionIDError { - get { - return ResourceManager.GetString("RuleSuppressionIDError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Found {0}. Will use it to provide settings for this invocation.. - /// - internal static string SettingsAutoDiscovered { - get { - return ResourceManager.GetString("SettingsAutoDiscovered", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot resolve settings file path '{0}'.. - /// - internal static string SettingsCannotFindFile { - get { - return ResourceManager.GetString("SettingsCannotFindFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dictionary should be indexable in a case-insensitive manner.. - /// - internal static string SettingsDictionaryShouldBeCaseInsesitive { - get { - return ResourceManager.GetString("SettingsDictionaryShouldBeCaseInsesitive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input should be a dictionary type.. - /// - internal static string SettingsInputShouldBeDictionary { - get { - return ResourceManager.GetString("SettingsInputShouldBeDictionary", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings should be either a file path, built-in preset or a hashtable.. - /// - internal static string SettingsInvalidType { - get { - return ResourceManager.GetString("SettingsInvalidType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot parse settings. Will abort the invocation.. - /// - internal static string SettingsNotParsable { - get { - return ResourceManager.GetString("SettingsNotParsable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings not provided. Will look for settings file in the given path {0}.. - /// - internal static string SettingsNotProvided { - get { - return ResourceManager.GetString("SettingsNotProvided", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings object could not be resolved.. - /// - internal static string SettingsObjectCouldNotBResolved { - get { - return ResourceManager.GetString("SettingsObjectCouldNotBResolved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using settings file at {0}.. - /// - internal static string SettingsUsingFile { - get { - return ResourceManager.GetString("SettingsUsingFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using settings hashtable.. - /// - internal static string SettingsUsingHashtable { - get { - return ResourceManager.GetString("SettingsUsingHashtable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} property must be of type bool.. - /// - internal static string SettingsValueTypeMustBeBool { - get { - return ResourceManager.GetString("SettingsValueTypeMustBeBool", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All the arguments of the Suppress Message Attribute should be string constants.. - /// - internal static string StringConstantArgumentsSuppressionAttributeError { - get { - return ResourceManager.GetString("StringConstantArgumentsSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find any Targets {0} that match the Scope {1} to apply the SuppressMessageAttribute.. - /// - internal static string TargetCannotBeFoundError { - get { - return ResourceManager.GetString("TargetCannotBeFoundError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If Target is specified, Scope must be specified.. - /// - internal static string TargetWithoutScopeSuppressionAttributeError { - get { - return ResourceManager.GetString("TargetWithoutScopeSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line element cannot be null.. - /// - internal static string TextEditNoNullItem { - get { - return ResourceManager.GetString("TextEditNoNullItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line element cannot be null.. - /// - internal static string TextLinesNoNullItem { - get { - return ResourceManager.GetString("TextLinesNoNullItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ignoring 'TypeNotFound' parse error on type '{0}'. Check if the specified type is correct. This can also be due the type not being known at parse time due to types imported by 'using' statements.. - /// - internal static string TypeNotFoundParseErrorFound { - get { - return ResourceManager.GetString("TypeNotFoundParseErrorFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Analyzing file: {0}. - /// - internal static string VerboseFileMessage { - get { - return ResourceManager.GetString("VerboseFileMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Running {0} rule.. - /// - internal static string VerboseRunningMessage { - get { - return ResourceManager.GetString("VerboseRunningMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Analyzing Script Definition.. - /// - internal static string VerboseScriptDefinitionMessage { - get { - return ResourceManager.GetString("VerboseScriptDefinitionMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WrongSettingsKey. - /// - internal static string WrongConfigurationKey { - get { - return ResourceManager.GetString("WrongConfigurationKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid key in the settings hashtable: file {3}. Valid keys are ExcludeRules, IncludeRules and Severity.. - /// - internal static string WrongKey { - get { - return ResourceManager.GetString("WrongKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Key in the settings hashtable should be a string: line {0} column {1} in file {2}. - /// - internal static string WrongKeyFormat { - get { - return ResourceManager.GetString("WrongKeyFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid key in the settings hashtable. Valid keys are CustomRulePath, ExcludeRules, IncludeRules, IncludeDefaultRules, RecurseCustomRulePath, Rules and Severity.. - /// - internal static string WrongKeyHashTable { - get { - return ResourceManager.GetString("WrongKeyHashTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scope can only be either function or class.. - /// - internal static string WrongScopeArgumentSuppressionAttributeError { - get { - return ResourceManager.GetString("WrongScopeArgumentSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value in the settings hashtable should be a string or an array of strings: line {0} column {1} in file {2}. - /// - internal static string WrongValueFormat { - get { - return ResourceManager.GetString("WrongValueFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value {0} for key {1} has the wrong data type.. - /// - internal static string WrongValueHashTable { - get { - return ResourceManager.GetString("WrongValueHashTable", resourceCulture); - } - } - } -} diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index c52b04ad2..e3ab388c7 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -34,21 +34,26 @@ portable - - - True - True - Strings.resx - - - + - ResXFileCodeGenerator + + MSBuild:Compile Strings.Designer.cs + $(IntermediateOutputPath)\Strings.Designer.cs + CSharp + Microsoft.Windows.PowerShell.ScriptAnalyzer + Strings - + + + PrepareResources;$(CompileDependsOn) + + $(DefineConstants);PSV3 diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs deleted file mode 100644 index f346e9084..000000000 --- a/Rules/Strings.Designer.cs +++ /dev/null @@ -1,3330 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Windows.PowerShell.ScriptAnalyzer { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Windows.PowerShell.ScriptAnalyzer.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Align assignment statement. - /// - internal static string AlignAssignmentStatementCommonName { - get { - return ResourceManager.GetString("AlignAssignmentStatementCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line up assignment statements such that the assignment operator are aligned.. - /// - internal static string AlignAssignmentStatementDescription { - get { - return ResourceManager.GetString("AlignAssignmentStatementDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Assignment statements are not aligned. - /// - internal static string AlignAssignmentStatementError { - get { - return ResourceManager.GetString("AlignAssignmentStatementError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AlignAssignmentStatement. - /// - internal static string AlignAssignmentStatementName { - get { - return ResourceManager.GetString("AlignAssignmentStatementName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidAssignmentToAutomaticVariable. - /// - internal static string AvoidAssignmentToAutomaticVariableName { - get { - return ResourceManager.GetString("AvoidAssignmentToAutomaticVariableName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use a different variable name. - /// - internal static string AvoidAssignmentToReadOnlyAutomaticVariable { - get { - return ResourceManager.GetString("AvoidAssignmentToReadOnlyAutomaticVariable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Changing automtic variables might have undesired side effects. - /// - internal static string AvoidAssignmentToReadOnlyAutomaticVariableCommonName { - get { - return ResourceManager.GetString("AvoidAssignmentToReadOnlyAutomaticVariableCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This automatic variables is built into PowerShell and readonly.. - /// - internal static string AvoidAssignmentToReadOnlyAutomaticVariableDescription { - get { - return ResourceManager.GetString("AvoidAssignmentToReadOnlyAutomaticVariableDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name.. - /// - internal static string AvoidAssignmentToReadOnlyAutomaticVariableError { - get { - return ResourceManager.GetString("AvoidAssignmentToReadOnlyAutomaticVariableError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Starting from PowerShell 6.0, the Variable '{0}' cannot be assigned any more since it is a readonly automatic variable that is built into PowerShell, please use a different name.. - /// - internal static string AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error { - get { - return ResourceManager.GetString("AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Variable '{0}' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name.. - /// - internal static string AvoidAssignmentToWritableAutomaticVariableError { - get { - return ResourceManager.GetString("AvoidAssignmentToWritableAutomaticVariableError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using ComputerName Hardcoded. - /// - internal static string AvoidComputerNameHardcodedCommonName { - get { - return ResourceManager.GetString("AvoidComputerNameHardcodedCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system.. - /// - internal static string AvoidComputerNameHardcodedDescription { - get { - return ResourceManager.GetString("AvoidComputerNameHardcodedDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ComputerName parameter of cmdlet '{0}' is hardcoded. This will expose sensitive information about the system if the script is shared.. - /// - internal static string AvoidComputerNameHardcodedError { - get { - return ResourceManager.GetString("AvoidComputerNameHardcodedError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingComputerNameHardcoded. - /// - internal static string AvoidComputerNameHardcodedName { - get { - return ResourceManager.GetString("AvoidComputerNameHardcodedName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Default Value For Mandatory Parameter. - /// - internal static string AvoidDefaultValueForMandatoryParameterCommonName { - get { - return ResourceManager.GetString("AvoidDefaultValueForMandatoryParameterCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored.. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block.. - /// - internal static string AvoidDefaultValueForMandatoryParameterDescription { - get { - return ResourceManager.GetString("AvoidDefaultValueForMandatoryParameterDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mandatory Parameter '{0}' is initialized in the Param block. To fix a violation of this rule, please leave it uninitialized.. - /// - internal static string AvoidDefaultValueForMandatoryParameterError { - get { - return ResourceManager.GetString("AvoidDefaultValueForMandatoryParameterError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidDefaultValueForMandatoryParameter. - /// - internal static string AvoidDefaultValueForMandatoryParameterName { - get { - return ResourceManager.GetString("AvoidDefaultValueForMandatoryParameterName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch Parameters Should Not Default To True. - /// - internal static string AvoidDefaultValueSwitchParameterCommonName { - get { - return ResourceManager.GetString("AvoidDefaultValueSwitchParameterCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch parameter should not default to true.. - /// - internal static string AvoidDefaultValueSwitchParameterDescription { - get { - return ResourceManager.GetString("AvoidDefaultValueSwitchParameterDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' has a switch parameter default to true.. - /// - internal static string AvoidDefaultValueSwitchParameterError { - get { - return ResourceManager.GetString("AvoidDefaultValueSwitchParameterError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Script definition has a switch parameter default to true.. - /// - internal static string AvoidDefaultValueSwitchParameterErrorScriptDefinition { - get { - return ResourceManager.GetString("AvoidDefaultValueSwitchParameterErrorScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidDefaultValueSwitchParameter. - /// - internal static string AvoidDefaultValueSwitchParameterName { - get { - return ResourceManager.GetString("AvoidDefaultValueSwitchParameterName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Empty catch block is used. Please use Write-Error or throw statements in catch blocks.. - /// - internal static string AvoidEmptyCatchBlockError { - get { - return ResourceManager.GetString("AvoidEmptyCatchBlockError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid exclaim operator. - /// - internal static string AvoidExclaimOperatorCommonName { - get { - return ResourceManager.GetString("AvoidExclaimOperatorCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Replace ! with -not. - /// - internal static string AvoidExclaimOperatorCorrectionDescription { - get { - return ResourceManager.GetString("AvoidExclaimOperatorCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The negation operator ! should not be used for readability purposes. Use -not instead.. - /// - internal static string AvoidExclaimOperatorDescription { - get { - return ResourceManager.GetString("AvoidExclaimOperatorDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using the ! negation operator. - /// - internal static string AvoidExclaimOperatorError { - get { - return ResourceManager.GetString("AvoidExclaimOperatorError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidExclaimOperator. - /// - internal static string AvoidExclaimOperatorName { - get { - return ResourceManager.GetString("AvoidExclaimOperatorName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid global aliases.. - /// - internal static string AvoidGlobalAliasesCommonName { - get { - return ResourceManager.GetString("AvoidGlobalAliasesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts.. - /// - internal static string AvoidGlobalAliasesDescription { - get { - return ResourceManager.GetString("AvoidGlobalAliasesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid creating aliases with a Global scope.. - /// - internal static string AvoidGlobalAliasesError { - get { - return ResourceManager.GetString("AvoidGlobalAliasesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidGlobalAliases. - /// - internal static string AvoidGlobalAliasesName { - get { - return ResourceManager.GetString("AvoidGlobalAliasesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid global functiosn and aliases. - /// - internal static string AvoidGlobalFunctionsCommonName { - get { - return ResourceManager.GetString("AvoidGlobalFunctionsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems.. - /// - internal static string AvoidGlobalFunctionsDescription { - get { - return ResourceManager.GetString("AvoidGlobalFunctionsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid creating functions with a Global scope.. - /// - internal static string AvoidGlobalFunctionsError { - get { - return ResourceManager.GetString("AvoidGlobalFunctionsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidGlobalFunctions. - /// - internal static string AvoidGlobalFunctionsName { - get { - return ResourceManager.GetString("AvoidGlobalFunctionsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No Global Variables. - /// - internal static string AvoidGlobalVarsCommonName { - get { - return ResourceManager.GetString("AvoidGlobalVarsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems.. - /// - internal static string AvoidGlobalVarsDescription { - get { - return ResourceManager.GetString("AvoidGlobalVarsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Found global variable '{0}'.. - /// - internal static string AvoidGlobalVarsError { - get { - return ResourceManager.GetString("AvoidGlobalVarsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidGlobalVars. - /// - internal static string AvoidGlobalVarsName { - get { - return ResourceManager.GetString("AvoidGlobalVarsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Invoking Empty Members. - /// - internal static string AvoidInvokingEmptyMembersCommonName { - get { - return ResourceManager.GetString("AvoidInvokingEmptyMembersCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant.. - /// - internal static string AvoidInvokingEmptyMembersDescription { - get { - return ResourceManager.GetString("AvoidInvokingEmptyMembersDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' has non-constant members. Invoking non-constant members may cause bugs in the script.. - /// - internal static string AvoidInvokingEmptyMembersError { - get { - return ResourceManager.GetString("AvoidInvokingEmptyMembersError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidInvokingEmptyMembers. - /// - internal static string AvoidInvokingEmptyMembersName { - get { - return ResourceManager.GetString("AvoidInvokingEmptyMembersName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid long lines. - /// - internal static string AvoidLongLinesCommonName { - get { - return ResourceManager.GetString("AvoidLongLinesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line lengths should be less than the configured maximum. - /// - internal static string AvoidLongLinesDescription { - get { - return ResourceManager.GetString("AvoidLongLinesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line exceeds the configured maximum length of {0} characters. - /// - internal static string AvoidLongLinesError { - get { - return ResourceManager.GetString("AvoidLongLinesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidLongLines. - /// - internal static string AvoidLongLinesName { - get { - return ResourceManager.GetString("AvoidLongLinesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid multiple type specifiers on parameters. - /// - internal static string AvoidMultipleTypeAttributesCommonName { - get { - return ResourceManager.GetString("AvoidMultipleTypeAttributesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prameter should not have more than one type specifier.. - /// - internal static string AvoidMultipleTypeAttributesDescription { - get { - return ResourceManager.GetString("AvoidMultipleTypeAttributesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter '{0}' has more than one type specifier.. - /// - internal static string AvoidMultipleTypeAttributesError { - get { - return ResourceManager.GetString("AvoidMultipleTypeAttributesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidMultipleTypeAttributes. - /// - internal static string AvoidMultipleTypeAttributesName { - get { - return ResourceManager.GetString("AvoidMultipleTypeAttributesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using null or empty HelpMessage parameter attribute.. - /// - internal static string AvoidNullOrEmptyHelpMessageAttributeCommonName { - get { - return ResourceManager.GetString("AvoidNullOrEmptyHelpMessageAttributeCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function.. - /// - internal static string AvoidNullOrEmptyHelpMessageAttributeDescription { - get { - return ResourceManager.GetString("AvoidNullOrEmptyHelpMessageAttributeDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to HelpMessage parameter attribute should not be null or empty. To fix a violation of this rule, please set its value to a non-empty string.. - /// - internal static string AvoidNullOrEmptyHelpMessageAttributeError { - get { - return ResourceManager.GetString("AvoidNullOrEmptyHelpMessageAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidNullOrEmptyHelpMessageAttribute. - /// - internal static string AvoidNullOrEmptyHelpMessageAttributeName { - get { - return ResourceManager.GetString("AvoidNullOrEmptyHelpMessageAttributeName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid overwriting built in cmdlets. - /// - internal static string AvoidOverwritingBuiltInCmdletsCommonName { - get { - return ResourceManager.GetString("AvoidOverwritingBuiltInCmdletsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not overwrite the definition of a cmdlet that is included with PowerShell. - /// - internal static string AvoidOverwritingBuiltInCmdletsDescription { - get { - return ResourceManager.GetString("AvoidOverwritingBuiltInCmdletsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden. - /// - internal static string AvoidOverwritingBuiltInCmdletsError { - get { - return ResourceManager.GetString("AvoidOverwritingBuiltInCmdletsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidOverwritingBuiltInCmdlets. - /// - internal static string AvoidOverwritingBuiltInCmdletsName { - get { - return ResourceManager.GetString("AvoidOverwritingBuiltInCmdletsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid semicolons as line terminators. - /// - internal static string AvoidSemicolonsAsLineTerminatorsCommonName { - get { - return ResourceManager.GetString("AvoidSemicolonsAsLineTerminatorsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line should not end with a semicolon. - /// - internal static string AvoidSemicolonsAsLineTerminatorsDescription { - get { - return ResourceManager.GetString("AvoidSemicolonsAsLineTerminatorsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line ends with a semicolon. - /// - internal static string AvoidSemicolonsAsLineTerminatorsError { - get { - return ResourceManager.GetString("AvoidSemicolonsAsLineTerminatorsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidSemicolonsAsLineTerminators. - /// - internal static string AvoidSemicolonsAsLineTerminatorsName { - get { - return ResourceManager.GetString("AvoidSemicolonsAsLineTerminatorsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using ShouldContinue Without Boolean Force Parameter. - /// - internal static string AvoidShouldContinueWithoutForceCommonName { - get { - return ResourceManager.GetString("AvoidShouldContinueWithoutForceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it.. - /// - internal static string AvoidShouldContinueWithoutForceDescription { - get { - return ResourceManager.GetString("AvoidShouldContinueWithoutForceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Function '{0}' in file '{1}' uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt. - /// - internal static string AvoidShouldContinueWithoutForceError { - get { - return ResourceManager.GetString("AvoidShouldContinueWithoutForceError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt. - /// - internal static string AvoidShouldContinueWithoutForceErrorScriptDefinition { - get { - return ResourceManager.GetString("AvoidShouldContinueWithoutForceErrorScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidShouldContinueWithoutForce. - /// - internal static string AvoidShouldContinueWithoutForceName { - get { - return ResourceManager.GetString("AvoidShouldContinueWithoutForceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid trailing whitespace. - /// - internal static string AvoidTrailingWhitespaceCommonName { - get { - return ResourceManager.GetString("AvoidTrailingWhitespaceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Each line should have no trailing whitespace.. - /// - internal static string AvoidTrailingWhitespaceDescription { - get { - return ResourceManager.GetString("AvoidTrailingWhitespaceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line has trailing whitespace. - /// - internal static string AvoidTrailingWhitespaceError { - get { - return ResourceManager.GetString("AvoidTrailingWhitespaceError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidTrailingWhitespace. - /// - internal static string AvoidTrailingWhitespaceName { - get { - return ResourceManager.GetString("AvoidTrailingWhitespaceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Module Must Be Loadable. - /// - internal static string AvoidUnloadableModuleCommonName { - get { - return ResourceManager.GetString("AvoidUnloadableModuleCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If a script file is in a PowerShell module folder, then that folder must be loadable.. - /// - internal static string AvoidUnloadableModuleDescription { - get { - return ResourceManager.GetString("AvoidUnloadableModuleDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot load the module '{0}' that file '{1}' is in.. - /// - internal static string AvoidUnloadableModuleError { - get { - return ResourceManager.GetString("AvoidUnloadableModuleError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUnloadableModule. - /// - internal static string AvoidUnloadableModuleName { - get { - return ResourceManager.GetString("AvoidUnloadableModuleName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Username and Password Parameters. - /// - internal static string AvoidUsernameAndPasswordParamsCommonName { - get { - return ResourceManager.GetString("AvoidUsernameAndPasswordParamsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString.. - /// - internal static string AvoidUsernameAndPasswordParamsDescription { - get { - return ResourceManager.GetString("AvoidUsernameAndPasswordParamsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Function '{0}' has both Username and Password parameters. Either set the type of the Password parameter to SecureString or replace the Username and Password parameters with a Credential parameter of type PSCredential. If using a Credential parameter in PowerShell 4.0 or earlier, please define a credential transformation attribute after the PSCredential type attribute.. - /// - internal static string AvoidUsernameAndPasswordParamsError { - get { - return ResourceManager.GetString("AvoidUsernameAndPasswordParamsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingUsernameAndPasswordParams. - /// - internal static string AvoidUsernameAndPasswordParamsName { - get { - return ResourceManager.GetString("AvoidUsernameAndPasswordParamsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Broken Hash Algorithms. - /// - internal static string AvoidUsingBrokenHashAlgorithmsCommonName { - get { - return ResourceManager.GetString("AvoidUsingBrokenHashAlgorithmsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using the broken algorithms MD5 or SHA-1.. - /// - internal static string AvoidUsingBrokenHashAlgorithmsDescription { - get { - return ResourceManager.GetString("AvoidUsingBrokenHashAlgorithmsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Algorithm parameter of cmdlet '{0}' was used with the broken algorithm '{1}'.. - /// - internal static string AvoidUsingBrokenHashAlgorithmsError { - get { - return ResourceManager.GetString("AvoidUsingBrokenHashAlgorithmsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingBrokenHashAlgorithms. - /// - internal static string AvoidUsingBrokenHashAlgorithmsName { - get { - return ResourceManager.GetString("AvoidUsingBrokenHashAlgorithmsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Clear-Host. - /// - internal static string AvoidUsingClearHostCommonName { - get { - return ResourceManager.GetString("AvoidUsingClearHostCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using Clear-Host is not recommended because the cmdlet may not work in some hosts or there may even be no hosts at all.. - /// - internal static string AvoidUsingClearHostDescription { - get { - return ResourceManager.GetString("AvoidUsingClearHostDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' uses Clear-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all.. - /// - internal static string AvoidUsingClearHostError { - get { - return ResourceManager.GetString("AvoidUsingClearHostError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingClearHost. - /// - internal static string AvoidUsingClearHostName { - get { - return ResourceManager.GetString("AvoidUsingClearHostName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix.. - /// - internal static string AvoidUsingCmdletAliasesCommonName { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Replace {0} with {1}. - /// - internal static string AvoidUsingCmdletAliasesCorrectionDescription { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availa [rest of string was truncated]";. - /// - internal static string AvoidUsingCmdletAliasesDescription { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is an alias of '{1}'. Alias can introduce possible problems and make scripts hard to maintain. Please consider changing alias to its full content.. - /// - internal static string AvoidUsingCmdletAliasesError { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is implicitly aliasing '{1}' because it is missing the 'Get-' prefix. This can introduce possible problems and make scripts hard to maintain. Please consider changing command to its full name.. - /// - internal static string AvoidUsingCmdletAliasesMissingGetPrefixError { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesMissingGetPrefixError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingCmdletAliases. - /// - internal static string AvoidUsingCmdletAliasesName { - get { - return ResourceManager.GetString("AvoidUsingCmdletAliasesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' uses Console.'{1}'. Using Console to write is not recommended because it may not work in all hosts or there may even be no hosts at all. Use Write-Output instead.. - /// - internal static string AvoidUsingConsoleWriteError { - get { - return ResourceManager.GetString("AvoidUsingConsoleWriteError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using SecureString With Plain Text. - /// - internal static string AvoidUsingConvertToSecureStringWithPlainTextCommonName { - get { - return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using ConvertTo-SecureString with plain text will expose secure information.. - /// - internal static string AvoidUsingConvertToSecureStringWithPlainTextDescription { - get { - return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead.. - /// - internal static string AvoidUsingConvertToSecureStringWithPlainTextError { - get { - return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead.. - /// - internal static string AvoidUsingConvertToSecureStringWithPlainTextErrorScriptDefinition { - get { - return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextErrorScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingConvertToSecureStringWithPlainText. - /// - internal static string AvoidUsingConvertToSecureStringWithPlainTextName { - get { - return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Deprecated Manifest Fields. - /// - internal static string AvoidUsingDeprecatedManifestFieldsCommonName { - get { - return ResourceManager.GetString("AvoidUsingDeprecatedManifestFieldsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "ModuleToProcess" is obsolete in the latest PowerShell version. Please update with the latest field "RootModule" in manifest files to avoid PowerShell version inconsistency.. - /// - internal static string AvoidUsingDeprecatedManifestFieldsDescription { - get { - return ResourceManager.GetString("AvoidUsingDeprecatedManifestFieldsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingDeprecatedManifestFields. - /// - internal static string AvoidUsingDeprecatedManifestFieldsName { - get { - return ResourceManager.GetString("AvoidUsingDeprecatedManifestFieldsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using double quotes if the string is constant.. - /// - internal static string AvoidUsingDoubleQuotesForConstantStringCommonName { - get { - return ResourceManager.GetString("AvoidUsingDoubleQuotesForConstantStringCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use single quotes if the string is constant.. - /// - internal static string AvoidUsingDoubleQuotesForConstantStringDescription { - get { - return ResourceManager.GetString("AvoidUsingDoubleQuotesForConstantStringDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use single quotes when a string is constant.. - /// - internal static string AvoidUsingDoubleQuotesForConstantStringError { - get { - return ResourceManager.GetString("AvoidUsingDoubleQuotesForConstantStringError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingDoubleQuotesForConstantString. - /// - internal static string AvoidUsingDoubleQuotesForConstantStringName { - get { - return ResourceManager.GetString("AvoidUsingDoubleQuotesForConstantStringName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Empty Catch Block. - /// - internal static string AvoidUsingEmptyCatchBlockCommonName { - get { - return ResourceManager.GetString("AvoidUsingEmptyCatchBlockCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks.. - /// - internal static string AvoidUsingEmptyCatchBlockDescription { - get { - return ResourceManager.GetString("AvoidUsingEmptyCatchBlockDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingEmptyCatchBlock. - /// - internal static string AvoidUsingEmptyCatchBlockName { - get { - return ResourceManager.GetString("AvoidUsingEmptyCatchBlockName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Internal URLs. - /// - internal static string AvoidUsingInternalURLsCommonName { - get { - return ResourceManager.GetString("AvoidUsingInternalURLsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using Internal URLs in the scripts may cause security problems.. - /// - internal static string AvoidUsingInternalURLsDescription { - get { - return ResourceManager.GetString("AvoidUsingInternalURLsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' could be an internal URL. Using internal URL directly in the script may cause potential information disclosure.. - /// - internal static string AvoidUsingInternalURLsError { - get { - return ResourceManager.GetString("AvoidUsingInternalURLsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingInternalURLs. - /// - internal static string AvoidUsingInternalURLsName { - get { - return ResourceManager.GetString("AvoidUsingInternalURLsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead.. - /// - internal static string AvoidUsingInvokeExpressionError { - get { - return ResourceManager.GetString("AvoidUsingInvokeExpressionError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Invoke-Expression. - /// - internal static string AvoidUsingInvokeExpressionRuleCommonName { - get { - return ResourceManager.GetString("AvoidUsingInvokeExpressionRuleCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please remove Invoke-Exp [rest of string was truncated]";. - /// - internal static string AvoidUsingInvokeExpressionRuleDescription { - get { - return ResourceManager.GetString("AvoidUsingInvokeExpressionRuleDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingInvokeExpression. - /// - internal static string AvoidUsingInvokeExpressionRuleName { - get { - return ResourceManager.GetString("AvoidUsingInvokeExpressionRuleName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Plain Text For Password Parameter. - /// - internal static string AvoidUsingPlainTextForPasswordCommonName { - get { - return ResourceManager.GetString("AvoidUsingPlainTextForPasswordCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set {0} type to {1}. - /// - internal static string AvoidUsingPlainTextForPasswordCorrectionDescription { - get { - return ResourceManager.GetString("AvoidUsingPlainTextForPasswordCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Password parameters that take in plaintext will expose passwords and compromise the security of your system.. - /// - internal static string AvoidUsingPlainTextForPasswordDescription { - get { - return ResourceManager.GetString("AvoidUsingPlainTextForPasswordDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter '{0}' should not use String type but either SecureString or PSCredential, otherwise it increases the chance to expose this sensitive information.. - /// - internal static string AvoidUsingPlainTextForPasswordError { - get { - return ResourceManager.GetString("AvoidUsingPlainTextForPasswordError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingPlainTextForPassword. - /// - internal static string AvoidUsingPlainTextForPasswordName { - get { - return ResourceManager.GetString("AvoidUsingPlainTextForPasswordName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Positional Parameters. - /// - internal static string AvoidUsingPositionalParametersCommonName { - get { - return ResourceManager.GetString("AvoidUsingPositionalParametersCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command.. - /// - internal static string AvoidUsingPositionalParametersDescription { - get { - return ResourceManager.GetString("AvoidUsingPositionalParametersDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet '{0}' has positional parameter. Please use named parameters instead of positional parameters when calling a command.. - /// - internal static string AvoidUsingPositionalParametersError { - get { - return ResourceManager.GetString("AvoidUsingPositionalParametersError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingPositionalParameters. - /// - internal static string AvoidUsingPositionalParametersName { - get { - return ResourceManager.GetString("AvoidUsingPositionalParametersName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance. - /// - internal static string AvoidUsingWMICmdletCommonName { - get { - return ResourceManager.GetString("AvoidUsingWMICmdletCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets.. - /// - internal static string AvoidUsingWMICmdletDescription { - get { - return ResourceManager.GetString("AvoidUsingWMICmdletDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems.. - /// - internal static string AvoidUsingWMICmdletError { - get { - return ResourceManager.GetString("AvoidUsingWMICmdletError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Script definition uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems.. - /// - internal static string AvoidUsingWMICmdletErrorScriptDefinition { - get { - return ResourceManager.GetString("AvoidUsingWMICmdletErrorScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingWMICmdlet. - /// - internal static string AvoidUsingWMICmdletName { - get { - return ResourceManager.GetString("AvoidUsingWMICmdletName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid Using Write-Host. - /// - internal static string AvoidUsingWriteHostCommonName { - get { - return ResourceManager.GetString("AvoidUsingWriteHostCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it.. - /// - internal static string AvoidUsingWriteHostDescription { - get { - return ResourceManager.GetString("AvoidUsingWriteHostDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File '{0}' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.. - /// - internal static string AvoidUsingWriteHostError { - get { - return ResourceManager.GetString("AvoidUsingWriteHostError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.. - /// - internal static string AvoidUsingWriteHostErrorScriptDefinition { - get { - return ResourceManager.GetString("AvoidUsingWriteHostErrorScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUsingWriteHost. - /// - internal static string AvoidUsingWriteHostName { - get { - return ResourceManager.GetString("AvoidUsingWriteHostName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command Not Found. - /// - internal static string CommandNotFoundCommonName { - get { - return ResourceManager.GetString("CommandNotFoundCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Commands that are undefined or do not exist should not be used.. - /// - internal static string CommandNotFoundDescription { - get { - return ResourceManager.GetString("CommandNotFoundDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command '{0}' Is Not Found. - /// - internal static string CommandNotFoundError { - get { - return ResourceManager.GetString("CommandNotFoundError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CommandNotFound. - /// - internal static string CommandNotFoundName { - get { - return ResourceManager.GetString("CommandNotFoundName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DscExamplesPresent. - /// - internal static string DscExamplesPresent { - get { - return ResourceManager.GetString("DscExamplesPresent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DSC examples are present. - /// - internal static string DscExamplesPresentCommonName { - get { - return ResourceManager.GetString("DscExamplesPresentCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title.. - /// - internal static string DscExamplesPresentDescription { - get { - return ResourceManager.GetString("DscExamplesPresentDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No examples found for resource '{0}'. - /// - internal static string DscExamplesPresentNoExamplesError { - get { - return ResourceManager.GetString("DscExamplesPresentNoExamplesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PSDSC. - /// - internal static string DSCSourceName { - get { - return ResourceManager.GetString("DSCSourceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DscTestsPresent. - /// - internal static string DscTestsPresent { - get { - return ResourceManager.GetString("DscTestsPresent", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dsc tests are present. - /// - internal static string DscTestsPresentCommonName { - get { - return ResourceManager.GetString("DscTestsPresentCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name.. - /// - internal static string DscTestsPresentDescription { - get { - return ResourceManager.GetString("DscTestsPresentDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No tests found for resource '{0}'. - /// - internal static string DscTestsPresentNoTestsError { - get { - return ResourceManager.GetString("DscTestsPresentNoTestsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When using an explicit process block, no preceding code is allowed, only begin, end and dynamicparams blocks.. - /// - internal static string InvalidSyntaxAroundProcessBlockError { - get { - return ResourceManager.GetString("InvalidSyntaxAroundProcessBlockError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Misleading Backtick. - /// - internal static string MisleadingBacktickCommonName { - get { - return ResourceManager.GetString("MisleadingBacktickCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace.. - /// - internal static string MisleadingBacktickDescription { - get { - return ResourceManager.GetString("MisleadingBacktickDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This line has a backtick at the end trailed by a whitespace character. Did you mean for this to be a line continuation?. - /// - internal static string MisleadingBacktickError { - get { - return ResourceManager.GetString("MisleadingBacktickError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MisleadingBacktick. - /// - internal static string MisleadingBacktickName { - get { - return ResourceManager.GetString("MisleadingBacktickName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Module Manifest Fields. - /// - internal static string MissingModuleManifestFieldCommonName { - get { - return ResourceManager.GetString("MissingModuleManifestFieldCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Add {0} = {1} to the module manifest. - /// - internal static string MissingModuleManifestFieldCorrectionDescription { - get { - return ResourceManager.GetString("MissingModuleManifestFieldCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Some fields of the module manifest (such as ModuleVersion) are required.. - /// - internal static string MissingModuleManifestFieldDescription { - get { - return ResourceManager.GetString("MissingModuleManifestFieldDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MissingModuleManifestField. - /// - internal static string MissingModuleManifestFieldName { - get { - return ResourceManager.GetString("MissingModuleManifestFieldName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0}{1}. - /// - internal static string NameSpaceFormat { - get { - return ResourceManager.GetString("NameSpaceFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Not all code path in {0} function in DSC Class {1} returns a value. - /// - internal static string NotAllCodePathReturnsDSCFunctionsError { - get { - return ResourceManager.GetString("NotAllCodePathReturnsDSCFunctionsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot process null Ast. - /// - internal static string NullAstErrorMessage { - get { - return ResourceManager.GetString("NullAstErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot process null CommandInfo. - /// - internal static string NullCommandInfoError { - get { - return ResourceManager.GetString("NullCommandInfoError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error Message is Null.. - /// - internal static string NullErrorMessage { - get { - return ResourceManager.GetString("NullErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to One Char. - /// - internal static string OneCharCommonName { - get { - return ResourceManager.GetString("OneCharCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that cmdlets and parameters have more than one character.. - /// - internal static string OneCharDescription { - get { - return ResourceManager.GetString("OneCharDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet name '{0}' only has one character.. - /// - internal static string OneCharErrorCmdlet { - get { - return ResourceManager.GetString("OneCharErrorCmdlet", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' has a parameter '{1}' that only has one character.. - /// - internal static string OneCharErrorParameter { - get { - return ResourceManager.GetString("OneCharErrorParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A script block has a parameter '{0}' that only has one character.. - /// - internal static string OneCharErrorParameterSB { - get { - return ResourceManager.GetString("OneCharErrorParameterSB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OneChar. - /// - internal static string OneCharName { - get { - return ResourceManager.GetString("OneCharName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Place close braces. - /// - internal static string PlaceCloseBraceCommonName { - get { - return ResourceManager.GetString("PlaceCloseBraceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close brace should be on a new line by itself.. - /// - internal static string PlaceCloseBraceDescription { - get { - return ResourceManager.GetString("PlaceCloseBraceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close brace is not on a new line.. - /// - internal static string PlaceCloseBraceErrorShouldBeOnNewLine { - get { - return ResourceManager.GetString("PlaceCloseBraceErrorShouldBeOnNewLine", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close brace before a branch statement is followed by a new line.. - /// - internal static string PlaceCloseBraceErrorShouldCuddleBranchStatement { - get { - return ResourceManager.GetString("PlaceCloseBraceErrorShouldCuddleBranchStatement", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close brace does not follow a new line.. - /// - internal static string PlaceCloseBraceErrorShouldFollowNewLine { - get { - return ResourceManager.GetString("PlaceCloseBraceErrorShouldFollowNewLine", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close brace does not follow a non-empty line.. - /// - internal static string PlaceCloseBraceErrorShouldNotFollowEmptyLine { - get { - return ResourceManager.GetString("PlaceCloseBraceErrorShouldNotFollowEmptyLine", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PlaceCloseBrace. - /// - internal static string PlaceCloseBraceName { - get { - return ResourceManager.GetString("PlaceCloseBraceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Place open braces consistently. - /// - internal static string PlaceOpenBraceCommonName { - get { - return ResourceManager.GetString("PlaceOpenBraceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Place open braces either on the same line as the preceding expression or on a new line.. - /// - internal static string PlaceOpenBraceDescription { - get { - return ResourceManager.GetString("PlaceOpenBraceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There is no new line after open brace.. - /// - internal static string PlaceOpenBraceErrorNoNewLineAfterBrace { - get { - return ResourceManager.GetString("PlaceOpenBraceErrorNoNewLineAfterBrace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open brace not on same line as preceding keyword. It should be on the same line.. - /// - internal static string PlaceOpenBraceErrorShouldBeOnSameLine { - get { - return ResourceManager.GetString("PlaceOpenBraceErrorShouldBeOnSameLine", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open brace is not on a new line.. - /// - internal static string PlaceOpenBraceErrorShouldNotBeOnSameLine { - get { - return ResourceManager.GetString("PlaceOpenBraceErrorShouldNotBeOnSameLine", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PlaceOpenBrace. - /// - internal static string PlaceOpenBraceName { - get { - return ResourceManager.GetString("PlaceOpenBraceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Null Comparison. - /// - internal static string PossibleIncorrectComparisonWithNullCommonName { - get { - return ResourceManager.GetString("PossibleIncorrectComparisonWithNullCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case.. - /// - internal static string PossibleIncorrectComparisonWithNullDescription { - get { - return ResourceManager.GetString("PossibleIncorrectComparisonWithNullDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to $null should be on the left side of equality comparisons.. - /// - internal static string PossibleIncorrectComparisonWithNullError { - get { - return ResourceManager.GetString("PossibleIncorrectComparisonWithNullError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PossibleIncorrectComparisonWithNull. - /// - internal static string PossibleIncorrectComparisonWithNullName { - get { - return ResourceManager.GetString("PossibleIncorrectComparisonWithNullName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use $null on the left hand side for safe comparison with $null.. - /// - internal static string PossibleIncorrectComparisonWithNullSuggesteCorrectionDescription { - get { - return ResourceManager.GetString("PossibleIncorrectComparisonWithNullSuggesteCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '=' is not an assignment operator. Did you mean the equality operator '-eq'?. - /// - internal static string PossibleIncorrectUsageOfAssignmentOperatorCommonName { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfAssignmentOperatorCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements.. - /// - internal static string PossibleIncorrectUsageOfAssignmentOperatorDescription { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfAssignmentOperatorDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Did you mean to use the assignment operator '='? The equality operator in PowerShell is 'eq'.. - /// - internal static string PossibleIncorrectUsageOfAssignmentOperatorError { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfAssignmentOperatorError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PossibleIncorrectUsageOfAssignmentOperator. - /// - internal static string PossibleIncorrectUsageOfAssignmentOperatorName { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfAssignmentOperatorName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal).. - /// - internal static string PossibleIncorrectUsageOfRedirectionOperatorCommonName { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfRedirectionOperatorCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell.. - /// - internal static string PossibleIncorrectUsageOfRedirectionOperatorDescription { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfRedirectionOperatorDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal).. - /// - internal static string PossibleIncorrectUsageOfRedirectionOperatorError { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfRedirectionOperatorError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PossibleIncorrectUsageOfRedirectionOperator. - /// - internal static string PossibleIncorrectUsageOfRedirectionOperatorName { - get { - return ResourceManager.GetString("PossibleIncorrectUsageOfRedirectionOperatorName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Basic Comment Help. - /// - internal static string ProvideCommentHelpCommonName { - get { - return ResourceManager.GetString("ProvideCommentHelpCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment.. - /// - internal static string ProvideCommentHelpDescription { - get { - return ResourceManager.GetString("ProvideCommentHelpDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' does not have a help comment.. - /// - internal static string ProvideCommentHelpError { - get { - return ResourceManager.GetString("ProvideCommentHelpError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ProvideCommentHelp. - /// - internal static string ProvideCommentHelpName { - get { - return ResourceManager.GetString("ProvideCommentHelpName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reserved Cmdlet Chars. - /// - internal static string ReservedCmdletCharCommonName { - get { - return ResourceManager.GetString("ReservedCmdletCharCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors.. - /// - internal static string ReservedCmdletCharDescription { - get { - return ResourceManager.GetString("ReservedCmdletCharDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' uses a reserved char in its name.. - /// - internal static string ReservedCmdletCharError { - get { - return ResourceManager.GetString("ReservedCmdletCharError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ReservedCmdletChar. - /// - internal static string ReservedCmdletCharName { - get { - return ResourceManager.GetString("ReservedCmdletCharName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}'. - /// - internal static string ReservedParamsCmdletPrefix { - get { - return ResourceManager.GetString("ReservedParamsCmdletPrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reserved Parameters. - /// - internal static string ReservedParamsCommonName { - get { - return ResourceManager.GetString("ReservedParamsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs.. - /// - internal static string ReservedParamsDescription { - get { - return ResourceManager.GetString("ReservedParamsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' defines the reserved common parameter '{1}'.. - /// - internal static string ReservedParamsError { - get { - return ResourceManager.GetString("ReservedParamsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ReservedParams. - /// - internal static string ReservedParamsName { - get { - return ResourceManager.GetString("ReservedParamsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The script. - /// - internal static string ReservedParamsScriptPrefix { - get { - return ResourceManager.GetString("ReservedParamsScriptPrefix", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to #,(){}[]&/\\$^;:\"'<>|?@`*%+=~. - /// - internal static string ReserverCmdletChars { - get { - return ResourceManager.GetString("ReserverCmdletChars", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ReturnCorrectTypesForDSCFunctions. - /// - internal static string ReturnCorrectTypeDSCFunctionsName { - get { - return ResourceManager.GetString("ReturnCorrectTypeDSCFunctionsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Return Correct Types For DSC Functions. - /// - internal static string ReturnCorrectTypesForDSCFunctionsCommonName { - get { - return ResourceManager.GetString("ReturnCorrectTypesForDSCFunctionsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean.. - /// - internal static string ReturnCorrectTypesForDSCFunctionsDescription { - get { - return ResourceManager.GetString("ReturnCorrectTypesForDSCFunctionsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} function in DSC Class {1} should return object of type {2}. - /// - internal static string ReturnCorrectTypesForDSCFunctionsNoTypeError { - get { - return ResourceManager.GetString("ReturnCorrectTypesForDSCFunctionsNoTypeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} function in DSC Class {1} should return object of type {2} instead of type {3}. - /// - internal static string ReturnCorrectTypesForDSCFunctionsWrongTypeError { - get { - return ResourceManager.GetString("ReturnCorrectTypesForDSCFunctionsWrongTypeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} function in DSC Resource should return object of type {1} instead of {2}. - /// - internal static string ReturnCorrectTypesForGetTestTargetResourceFunctionsDSCResourceError { - get { - return ResourceManager.GetString("ReturnCorrectTypesForGetTestTargetResourceFunctionsDSCResourceError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set function in DSC Class {0} should not return anything. - /// - internal static string ReturnCorrectTypesForSetFunctionsDSCError { - get { - return ResourceManager.GetString("ReturnCorrectTypesForSetFunctionsDSCError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set-TargetResource function in DSC Resource should not output anything to the pipeline.. - /// - internal static string ReturnCorrectTypesForSetTargetResourceFunctionsDSCError { - get { - return ResourceManager.GetString("ReturnCorrectTypesForSetTargetResourceFunctionsDSCError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ReviewUnusedParameter. - /// - internal static string ReviewUnusedParameterCommonName { - get { - return ResourceManager.GetString("ReviewUnusedParameterCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ensure all parameters are used within the same script, scriptblock, or function where they are declared.. - /// - internal static string ReviewUnusedParameterDescription { - get { - return ResourceManager.GetString("ReviewUnusedParameterDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The parameter '{0}' has been declared but not used. . - /// - internal static string ReviewUnusedParameterError { - get { - return ResourceManager.GetString("ReviewUnusedParameterError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ReviewUnusedParameter. - /// - internal static string ReviewUnusedParameterName { - get { - return ResourceManager.GetString("ReviewUnusedParameterName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ScriptDefinition. - /// - internal static string ScriptDefinitionName { - get { - return ResourceManager.GetString("ScriptDefinitionName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to www.sharepoint.com. - /// - internal static string SharepointURL { - get { - return ResourceManager.GetString("SharepointURL", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Should Process. - /// - internal static string ShouldProcessCommonName { - get { - return ResourceManager.GetString("ShouldProcessCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior.. - /// - internal static string ShouldProcessDescription { - get { - return ResourceManager.GetString("ShouldProcessDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue.. - /// - internal static string ShouldProcessErrorHasAttribute { - get { - return ResourceManager.GetString("ShouldProcessErrorHasAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A script block has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue.. - /// - internal static string ShouldProcessErrorHasAttributeSB { - get { - return ResourceManager.GetString("ShouldProcessErrorHasAttributeSB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute.. - /// - internal static string ShouldProcessErrorHasCmdlet { - get { - return ResourceManager.GetString("ShouldProcessErrorHasCmdlet", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A script block calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute.. - /// - internal static string ShouldProcessErrorHasCmdletSB { - get { - return ResourceManager.GetString("ShouldProcessErrorHasCmdletSB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ShouldProcess. - /// - internal static string ShouldProcessName { - get { - return ResourceManager.GetString("ShouldProcessName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PS. - /// - internal static string SourceName { - get { - return ResourceManager.GetString("SourceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type Not Found. - /// - internal static string TypeNotFoundCommonName { - get { - return ResourceManager.GetString("TypeNotFoundCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Undefined type should not be used. - /// - internal static string TypeNotFoundDescription { - get { - return ResourceManager.GetString("TypeNotFoundDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type '{0}' is not found. Please check that it is defined.. - /// - internal static string TypeNotFoundError { - get { - return ResourceManager.GetString("TypeNotFoundError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to TypeNotFound. - /// - internal static string TypeNotFoundName { - get { - return ResourceManager.GetString("TypeNotFoundName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet Verbs. - /// - internal static string UseApprovedVerbsCommonName { - get { - return ResourceManager.GetString("UseApprovedVerbsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices.. - /// - internal static string UseApprovedVerbsDescription { - get { - return ResourceManager.GetString("UseApprovedVerbsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' uses an unapproved verb.. - /// - internal static string UseApprovedVerbsError { - get { - return ResourceManager.GetString("UseApprovedVerbsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseApprovedVerbs. - /// - internal static string UseApprovedVerbsName { - get { - return ResourceManager.GetString("UseApprovedVerbsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use BOM encoding for non-ASCII files. - /// - internal static string UseBOMForUnicodeEncodedFileCommonName { - get { - return ResourceManager.GetString("UseBOMForUnicodeEncodedFileCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly.. - /// - internal static string UseBOMForUnicodeEncodedFileDescription { - get { - return ResourceManager.GetString("UseBOMForUnicodeEncodedFileDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing BOM encoding for non-ASCII encoded file '{0}'. - /// - internal static string UseBOMForUnicodeEncodedFileError { - get { - return ResourceManager.GetString("UseBOMForUnicodeEncodedFileError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseBOMForUnicodeEncodedFile. - /// - internal static string UseBOMForUnicodeEncodedFileName { - get { - return ResourceManager.GetString("UseBOMForUnicodeEncodedFileName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use Cmdlet Correctly. - /// - internal static string UseCmdletCorrectlyCommonName { - get { - return ResourceManager.GetString("UseCmdletCorrectlyCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet should be called with the mandatory parameters.. - /// - internal static string UseCmdletCorrectlyDescription { - get { - return ResourceManager.GetString("UseCmdletCorrectlyDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet '{0}' may be used incorrectly. Please check that all mandatory parameters are supplied.. - /// - internal static string UseCmdletCorrectlyError { - get { - return ResourceManager.GetString("UseCmdletCorrectlyError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCmdletCorrectly. - /// - internal static string UseCmdletCorrectlyName { - get { - return ResourceManager.GetString("UseCmdletCorrectlyName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use compatible cmdlets. - /// - internal static string UseCompatibleCmdletsCommonName { - get { - return ResourceManager.GetString("UseCompatibleCmdletsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use cmdlets compatible with the given PowerShell version and edition and operating system. - /// - internal static string UseCompatibleCmdletsDescription { - get { - return ResourceManager.GetString("UseCompatibleCmdletsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}'. - /// - internal static string UseCompatibleCmdletsError { - get { - return ResourceManager.GetString("UseCompatibleCmdletsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCompatibleCmdlets. - /// - internal static string UseCompatibleCmdletsName { - get { - return ResourceManager.GetString("UseCompatibleCmdletsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The command '{0}' is not available by default in PowerShell version '{1}' on platform '{2}'. - /// - internal static string UseCompatibleCommandsCommandError { - get { - return ResourceManager.GetString("UseCompatibleCommandsCommandError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use compatible commands. - /// - internal static string UseCompatibleCommandsCommonName { - get { - return ResourceManager.GetString("UseCompatibleCommandsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use commands compatible with the given PowerShell version and operating system. - /// - internal static string UseCompatibleCommandsDescription { - get { - return ResourceManager.GetString("UseCompatibleCommandsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCompatibleCommands. - /// - internal static string UseCompatibleCommandsName { - get { - return ResourceManager.GetString("UseCompatibleCommandsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The parameter '{0}' is not available for command '{1}' by default in PowerShell version '{2}' on platform '{3}'. - /// - internal static string UseCompatibleCommandsParameterError { - get { - return ResourceManager.GetString("UseCompatibleCommandsParameterError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use compatible syntax. - /// - internal static string UseCompatibleSyntaxCommonName { - get { - return ResourceManager.GetString("UseCompatibleSyntaxCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use the '{0}' syntax instead for compatibility with PowerShell versions {1}. - /// - internal static string UseCompatibleSyntaxCorrection { - get { - return ResourceManager.GetString("UseCompatibleSyntaxCorrection", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use script syntax compatible with the given PowerShell versions. - /// - internal static string UseCompatibleSyntaxDescription { - get { - return ResourceManager.GetString("UseCompatibleSyntaxDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The {0} syntax '{1}' is not available by default in PowerShell versions {2}. - /// - internal static string UseCompatibleSyntaxError { - get { - return ResourceManager.GetString("UseCompatibleSyntaxError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCompatibleSyntax. - /// - internal static string UseCompatibleSyntaxName { - get { - return ResourceManager.GetString("UseCompatibleSyntaxName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use compatible types. - /// - internal static string UseCompatibleTypesCommonName { - get { - return ResourceManager.GetString("UseCompatibleTypesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use types compatible with the given PowerShell version and operating system. - /// - internal static string UseCompatibleTypesDescription { - get { - return ResourceManager.GetString("UseCompatibleTypesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The member '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}'. - /// - internal static string UseCompatibleTypesMemberError { - get { - return ResourceManager.GetString("UseCompatibleTypesMemberError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The method '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}'. - /// - internal static string UseCompatibleTypesMethodError { - get { - return ResourceManager.GetString("UseCompatibleTypesMethodError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCompatibleTypes. - /// - internal static string UseCompatibleTypesName { - get { - return ResourceManager.GetString("UseCompatibleTypesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}'. - /// - internal static string UseCompatibleTypesTypeAcceleratorError { - get { - return ResourceManager.GetString("UseCompatibleTypesTypeAcceleratorError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The type '{0}' is not available by default in PowerShell version '{1}' on platform '{2}'. - /// - internal static string UseCompatibleTypesTypeError { - get { - return ResourceManager.GetString("UseCompatibleTypesTypeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use consistent indentation. - /// - internal static string UseConsistentIndentationCommonName { - get { - return ResourceManager.GetString("UseConsistentIndentationCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Each statement block should have a consistent indenation.. - /// - internal static string UseConsistentIndentationDescription { - get { - return ResourceManager.GetString("UseConsistentIndentationDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Indentation not consistent. - /// - internal static string UseConsistentIndentationError { - get { - return ResourceManager.GetString("UseConsistentIndentationError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseConsistentIndentation. - /// - internal static string UseConsistentIndentationName { - get { - return ResourceManager.GetString("UseConsistentIndentationName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use whitespaces. - /// - internal static string UseConsistentWhitespaceCommonName { - get { - return ResourceManager.GetString("UseConsistentWhitespaceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';'). - /// - internal static string UseConsistentWhitespaceDescription { - get { - return ResourceManager.GetString("UseConsistentWhitespaceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space after open brace.. - /// - internal static string UseConsistentWhitespaceErrorAfterOpeningBrace { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorAfterOpeningBrace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space before closing brace.. - /// - internal static string UseConsistentWhitespaceErrorBeforeClosingInnerBrace { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorBeforeClosingInnerBrace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space before open brace.. - /// - internal static string UseConsistentWhitespaceErrorBeforeOpeningBrace { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorBeforeOpeningBrace", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space before open parenthesis.. - /// - internal static string UseConsistentWhitespaceErrorBeforeParen { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorBeforeParen", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space before and after binary and assignment operators.. - /// - internal static string UseConsistentWhitespaceErrorOperator { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorOperator", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space after a comma.. - /// - internal static string UseConsistentWhitespaceErrorSeparatorComma { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorSeparatorComma", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space after a semicolon.. - /// - internal static string UseConsistentWhitespaceErrorSeparatorSemi { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorSeparatorSemi", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space after pipe.. - /// - internal static string UseConsistentWhitespaceErrorSpaceAfterPipe { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorSpaceAfterPipe", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use space before pipe.. - /// - internal static string UseConsistentWhitespaceErrorSpaceBeforePipe { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorSpaceBeforePipe", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use only 1 whitespace between parameter names or values.. - /// - internal static string UseConsistentWhitespaceErrorSpaceBetweenParameter { - get { - return ResourceManager.GetString("UseConsistentWhitespaceErrorSpaceBetweenParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseConsistentWhitespace. - /// - internal static string UseConsistentWhitespaceName { - get { - return ResourceManager.GetString("UseConsistentWhitespaceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use exact casing of cmdlet/function/parameter name.. - /// - internal static string UseCorrectCasingCommonName { - get { - return ResourceManager.GetString("UseCorrectCasingCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to For better readability and consistency, use the exact casing of the cmdlet/function/parameter.. - /// - internal static string UseCorrectCasingDescription { - get { - return ResourceManager.GetString("UseCorrectCasingDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Function/Cmdlet '{0}' does not match its exact casing '{1}'.. - /// - internal static string UseCorrectCasingError { - get { - return ResourceManager.GetString("UseCorrectCasingError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseCorrectCasing. - /// - internal static string UseCorrectCasingName { - get { - return ResourceManager.GetString("UseCorrectCasingName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'.. - /// - internal static string UseCorrectCasingParameterError { - get { - return ResourceManager.GetString("UseCorrectCasingParameterError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Extra Variables. - /// - internal static string UseDeclaredVarsMoreThanAssignmentsCommonName { - get { - return ResourceManager.GetString("UseDeclaredVarsMoreThanAssignmentsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ensure declared variables are used elsewhere in the script and not just during assignment.. - /// - internal static string UseDeclaredVarsMoreThanAssignmentsDescription { - get { - return ResourceManager.GetString("UseDeclaredVarsMoreThanAssignmentsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The variable '{0}' is assigned but never used.. - /// - internal static string UseDeclaredVarsMoreThanAssignmentsError { - get { - return ResourceManager.GetString("UseDeclaredVarsMoreThanAssignmentsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseDeclaredVarsMoreThanAssignments. - /// - internal static string UseDeclaredVarsMoreThanAssignmentsName { - get { - return ResourceManager.GetString("UseDeclaredVarsMoreThanAssignmentsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions. - /// - internal static string UseIdenticalMandatoryParametersDSCCommonName { - get { - return ResourceManager.GetString("UseIdenticalMandatoryParametersDSCCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters.. - /// - internal static string UseIdenticalMandatoryParametersDSCDescription { - get { - return ResourceManager.GetString("UseIdenticalMandatoryParametersDSCDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The '{0}' parameter '{1}' is not present in '{2}' DSC resource function(s).. - /// - internal static string UseIdenticalMandatoryParametersDSCError { - get { - return ResourceManager.GetString("UseIdenticalMandatoryParametersDSCError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseIdenticalMandatoryParametersForDSC. - /// - internal static string UseIdenticalMandatoryParametersDSCName { - get { - return ResourceManager.GetString("UseIdenticalMandatoryParametersDSCName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use Identical Parameters For DSC Test and Set Functions. - /// - internal static string UseIdenticalParametersDSCCommonName { - get { - return ResourceManager.GetString("UseIdenticalParametersDSCCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Test and Set-TargetResource functions of DSC Resource must have the same parameters.. - /// - internal static string UseIdenticalParametersDSCDescription { - get { - return ResourceManager.GetString("UseIdenticalParametersDSCDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Test and Set-TargetResource functions of DSC Resource must have the same parameters.. - /// - internal static string UseIdenticalParametersDSCError { - get { - return ResourceManager.GetString("UseIdenticalParametersDSCError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseIdenticalParametersForDSC. - /// - internal static string UseIdenticalParametersDSCName { - get { - return ResourceManager.GetString("UseIdenticalParametersDSCName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Create hashtables with literal initializers. - /// - internal static string UseLiteralInitilializerForHashtableCommonName { - get { - return ResourceManager.GetString("UseLiteralInitilializerForHashtableCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default. - /// - internal static string UseLiteralInitilializerForHashtableDescription { - get { - return ResourceManager.GetString("UseLiteralInitilializerForHashtableDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Create hashtables with literal initliazers. - /// - internal static string UseLiteralInitilializerForHashtableError { - get { - return ResourceManager.GetString("UseLiteralInitilializerForHashtableError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseLiteralInitializerForHashtable. - /// - internal static string UseLiteralInitilializerForHashtableName { - get { - return ResourceManager.GetString("UseLiteralInitilializerForHashtableName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use OutputType Correctly. - /// - internal static string UseOutputTypeCorrectlyCommonName { - get { - return ResourceManager.GetString("UseOutputTypeCorrectlyCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The return types of a cmdlet should be declared using the OutputType attribute.. - /// - internal static string UseOutputTypeCorrectlyDescription { - get { - return ResourceManager.GetString("UseOutputTypeCorrectlyDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute.. - /// - internal static string UseOutputTypeCorrectlyError { - get { - return ResourceManager.GetString("UseOutputTypeCorrectlyError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseOutputTypeCorrectly. - /// - internal static string UseOutputTypeCorrectlyName { - get { - return ResourceManager.GetString("UseOutputTypeCorrectlyName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use process block for command that accepts input from pipeline.. - /// - internal static string UseProcessBlockForPipelineCommandCommonName { - get { - return ResourceManager.GetString("UseProcessBlockForPipelineCommandCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter.. - /// - internal static string UseProcessBlockForPipelineCommandDescription { - get { - return ResourceManager.GetString("UseProcessBlockForPipelineCommandDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command accepts pipeline input but has not defined a process block.. - /// - internal static string UseProcessBlockForPipelineCommandError { - get { - return ResourceManager.GetString("UseProcessBlockForPipelineCommandError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseProcessBlockForPipelineCommand. - /// - internal static string UseProcessBlockForPipelineCommandName { - get { - return ResourceManager.GetString("UseProcessBlockForPipelineCommandName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use PSCredential type.. - /// - internal static string UsePSCredentialTypeCommonName { - get { - return ResourceManager.GetString("UsePSCredentialTypeCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. . - /// - internal static string UsePSCredentialTypeDescription { - get { - return ResourceManager.GetString("UsePSCredentialTypeDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute.. - /// - internal static string UsePSCredentialTypeError { - get { - return ResourceManager.GetString("UsePSCredentialTypeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. . - /// - internal static string UsePSCredentialTypeErrorSB { - get { - return ResourceManager.GetString("UsePSCredentialTypeErrorSB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UsePSCredentialType. - /// - internal static string UsePSCredentialTypeName { - get { - return ResourceManager.GetString("UsePSCredentialTypeName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use ShouldProcess For State Changing Functions. - /// - internal static string UseShouldProcessForStateChangingFunctionsCommonName { - get { - return ResourceManager.GetString("UseShouldProcessForStateChangingFunctionsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'.. - /// - internal static string UseShouldProcessForStateChangingFunctionsDescrption { - get { - return ResourceManager.GetString("UseShouldProcessForStateChangingFunctionsDescrption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Function '{0}' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'.. - /// - internal static string UseShouldProcessForStateChangingFunctionsError { - get { - return ResourceManager.GetString("UseShouldProcessForStateChangingFunctionsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseShouldProcessForStateChangingFunctions. - /// - internal static string UseShouldProcessForStateChangingFunctionsName { - get { - return ResourceManager.GetString("UseShouldProcessForStateChangingFunctionsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet Singular Noun. - /// - internal static string UseSingularNounsCommonName { - get { - return ResourceManager.GetString("UseSingularNounsCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cmdlet should use singular instead of plural nouns.. - /// - internal static string UseSingularNounsDescription { - get { - return ResourceManager.GetString("UseSingularNounsDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The cmdlet '{0}' uses a plural noun. A singular noun should be used instead.. - /// - internal static string UseSingularNounsError { - get { - return ResourceManager.GetString("UseSingularNounsError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseSingularNouns. - /// - internal static string UseSingularNounsName { - get { - return ResourceManager.GetString("UseSingularNounsName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing '{0}' function. DSC Class must implement Get, Set and Test functions.. - /// - internal static string UseStandardDSCFunctionsInClassError { - get { - return ResourceManager.GetString("UseStandardDSCFunctionsInClassError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use Standard Get/Set/Test TargetResource functions in DSC Resource . - /// - internal static string UseStandardDSCFunctionsInResourceCommonName { - get { - return ResourceManager.GetString("UseStandardDSCFunctionsInResourceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions.. - /// - internal static string UseStandardDSCFunctionsInResourceDescription { - get { - return ResourceManager.GetString("UseStandardDSCFunctionsInResourceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Missing '{0}' function. DSC Resource must implement Get, Set and Test-TargetResource functions.. - /// - internal static string UseStandardDSCFunctionsInResourceError { - get { - return ResourceManager.GetString("UseStandardDSCFunctionsInResourceError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to StandardDSCFunctionsInResource. - /// - internal static string UseStandardDSCFunctionsInResourceName { - get { - return ResourceManager.GetString("UseStandardDSCFunctionsInResourceName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use SupportsShouldProcess. - /// - internal static string UseSupportsShouldProcessCommonName { - get { - return ResourceManager.GetString("UseSupportsShouldProcessCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess.. - /// - internal static string UseSupportsShouldProcessDescription { - get { - return ResourceManager.GetString("UseSupportsShouldProcessDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute.. - /// - internal static string UseSupportsShouldProcessError { - get { - return ResourceManager.GetString("UseSupportsShouldProcessError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseSupportsShouldProcess. - /// - internal static string UseSupportsShouldProcessName { - get { - return ResourceManager.GetString("UseSupportsShouldProcessName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use the *ToExport module manifest fields.. - /// - internal static string UseToExportFieldsInManifestCommonName { - get { - return ResourceManager.GetString("UseToExportFieldsInManifestCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Replace {0} with {1}. - /// - internal static string UseToExportFieldsInManifestCorrectionDescription { - get { - return ResourceManager.GetString("UseToExportFieldsInManifestCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module.. - /// - internal static string UseToExportFieldsInManifestDescription { - get { - return ResourceManager.GetString("UseToExportFieldsInManifestDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not use wildcard or $null in this field. Explicitly specify a list for {0}. . - /// - internal static string UseToExportFieldsInManifestError { - get { - return ResourceManager.GetString("UseToExportFieldsInManifestError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseToExportFieldsInManifest. - /// - internal static string UseToExportFieldsInManifestName { - get { - return ResourceManager.GetString("UseToExportFieldsInManifestName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use Type At Variable Assignment. - /// - internal static string UseTypeAtVariableAssignmentCommonName { - get { - return ResourceManager.GetString("UseTypeAtVariableAssignmentCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Types should be specified at variable assignments to maintain readability and maintainability of script.. - /// - internal static string UseTypeAtVariableAssignmentDescription { - get { - return ResourceManager.GetString("UseTypeAtVariableAssignmentDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Specify type at the assignment of variable '{0}'. - /// - internal static string UseTypeAtVariableAssignmentError { - get { - return ResourceManager.GetString("UseTypeAtVariableAssignmentError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseTypeAtVariableAssignment. - /// - internal static string UseTypeAtVariableAssignmentName { - get { - return ResourceManager.GetString("UseTypeAtVariableAssignmentName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use 'Using:' scope modifier in RunSpace ScriptBlocks. - /// - internal static string UseUsingScopeModifierInNewRunspacesCommonName { - get { - return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Replace {0} with {1}. - /// - internal static string UseUsingScopeModifierInNewRunspacesCorrectionDescription { - get { - return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesCorrectionDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock.. - /// - internal static string UseUsingScopeModifierInNewRunspacesDescription { - get { - return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier.. - /// - internal static string UseUsingScopeModifierInNewRunspacesError { - get { - return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseUsingScopeModifierInNewRunspaces. - /// - internal static string UseUsingScopeModifierInNewRunspacesName { - get { - return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use UTF8 Encoding For Help File. - /// - internal static string UseUTF8EncodingForHelpFileCommonName { - get { - return ResourceManager.GetString("UseUTF8EncodingForHelpFileCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PowerShell help file needs to use UTF8 Encoding.. - /// - internal static string UseUTF8EncodingForHelpFileDescription { - get { - return ResourceManager.GetString("UseUTF8EncodingForHelpFileDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file.. - /// - internal static string UseUTF8EncodingForHelpFileError { - get { - return ResourceManager.GetString("UseUTF8EncodingForHelpFileError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseUTF8EncodingForHelpFile. - /// - internal static string UseUTF8EncodingForHelpFileName { - get { - return ResourceManager.GetString("UseUTF8EncodingForHelpFileName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use verbose message in DSC resource. - /// - internal static string UseVerboseMessageInDSCResourceCommonName { - get { - return ResourceManager.GetString("UseVerboseMessageInDSCResourceCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed.. - /// - internal static string UseVerboseMessageInDSCResourceDescription { - get { - return ResourceManager.GetString("UseVerboseMessageInDSCResourceDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There is no call to Write-Verbose in DSC function '{0}'. If you are using Write-Verbose in a helper function, suppress this rule application.. - /// - internal static string UseVerboseMessageInDSCResourceErrorFunction { - get { - return ResourceManager.GetString("UseVerboseMessageInDSCResourceErrorFunction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to UseVerboseMessageInDSCResource. - /// - internal static string UseVerboseMessageInDSCResourceName { - get { - return ResourceManager.GetString("UseVerboseMessageInDSCResourceName", resourceCulture); - } - } - } -} From 45edb8d9e06e6c6756e070513645ac22f813c682 Mon Sep 17 00:00:00 2001 From: Frode Flaten <3436158+fflaten@users.noreply.github.com> Date: Tue, 16 Jan 2024 23:30:20 +0100 Subject: [PATCH 018/130] Allow suppression of PSUseSingularNouns for specific function (#1903) * Add ruleId to PSUseSingularNouns * Update UseSingularNouns.md * Update UseSingularNouns.md --------- Co-authored-by: Christoph Bergmeister --- Rules/UseSingularNouns.cs | 1 + Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 | 12 ++++++++++++ docs/Rules/UseSingularNouns.md | 9 +++++++++ 3 files changed, 22 insertions(+) diff --git a/Rules/UseSingularNouns.cs b/Rules/UseSingularNouns.cs index de9264d35..218537459 100644 --- a/Rules/UseSingularNouns.cs +++ b/Rules/UseSingularNouns.cs @@ -88,6 +88,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) GetName(), DiagnosticSeverity.Warning, fileName, + funcAst.Name, suggestedCorrections: new CorrectionExtent[] { GetCorrection(pluralizer, extent, funcAst.Name, noun) }); } } diff --git a/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 b/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 index 534e6df4d..db1af36ce 100644 --- a/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 +++ b/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 @@ -86,6 +86,18 @@ Write-Output "Adding some data" $diagnostics.SuggestedCorrections.Text | Should -BeExactly $Correction } } + Context 'Suppression' { + It 'Can be suppressed by RuleSuppressionId' { + $scriptDef = @" +function Get-Elements { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('$nounViolationName', 'Get-Elements')] + param() +} +"@ + $warnings = @(Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef) + $warnings.Count | Should -Be 0 + } + } } Describe "UseApprovedVerbs" { diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index 4e7576bf0..caf032e03 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -13,6 +13,15 @@ title: UseSingularNouns PowerShell team best practices state cmdlets should use singular nouns and not plurals. +Suppression allows to suppress just specific function names, for example + +``` +function Get-Elements { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Get-Elements')] + Param() +} +``` + ## How Change plurals to singular. From e22c65f0b0f4c2904e6f273b16098526ba6f4909 Mon Sep 17 00:00:00 2001 From: James Brundage Date: Tue, 16 Jan 2024 14:31:15 -0800 Subject: [PATCH 019/130] Adding ToString() methods to [CorrectionExtent] and [DiagnosticRecord] (#1946) * Adding DiagnosticRecord.ToString() (Fixes #1945) * Adding CorrectionExtent.ToString() (Fixes #1944) --------- Co-authored-by: James Brundage <@github.com> --- Engine/Generic/CorrectionExtent.cs | 8 ++++++++ Engine/Generic/DiagnosticRecord.cs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Engine/Generic/CorrectionExtent.cs b/Engine/Generic/CorrectionExtent.cs index caad49cdb..f92070844 100644 --- a/Engine/Generic/CorrectionExtent.cs +++ b/Engine/Generic/CorrectionExtent.cs @@ -104,5 +104,13 @@ public CorrectionExtent( { } + + /// + /// Outputs a CorrectionExtent as a string. + /// + /// Returns the text in a CorrectionExtent. + public override string ToString() { + return this.Text; + } } } diff --git a/Engine/Generic/DiagnosticRecord.cs b/Engine/Generic/DiagnosticRecord.cs index cd325ecf2..0673e1391 100644 --- a/Engine/Generic/DiagnosticRecord.cs +++ b/Engine/Generic/DiagnosticRecord.cs @@ -127,6 +127,14 @@ public DiagnosticRecord( RuleSuppressionID = ruleId; this.suggestedCorrections = suggestedCorrections; } + + /// + /// Outputs a DiagnosticRecord as a string. + /// + /// Returns the message in a DiagnosticRecord. + public override string ToString() { + return this.Message; + } } From 59190fe3ff4f26e8387243ec2928934a44543a96 Mon Sep 17 00:00:00 2001 From: Eli Arbel <496737+aelij@users.noreply.github.com> Date: Wed, 17 Jan 2024 00:31:45 +0200 Subject: [PATCH 020/130] Add PSNativeCommandUseErrorActionPreference preference variable (#1954) --- Engine/SpecialVars.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index 0b820c0d4..a8be18b92 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -92,6 +92,7 @@ static SpecialVars() internal const string ProgressPreference = "ProgressPreference"; internal const string InformationPreference = "InformationPreference"; internal const string ErrorView = "ErrorView"; + internal const string PSNativeCommandUseErrorActionPreference = "PSNativeCommandUseErrorActionPreference"; internal static readonly string[] PreferenceVariables = new string[] { @@ -103,7 +104,8 @@ static SpecialVars() ConfirmPreference, ProgressPreference, InformationPreference, - ErrorView + ErrorView, + PSNativeCommandUseErrorActionPreference, }; internal static readonly Type[] PreferenceVariableTypes = new Type[] @@ -117,6 +119,7 @@ static SpecialVars() /* ProgressPreference */ typeof(Enum), /* InformationPreference */ typeof(ActionPreference), /* ErrorView */ typeof(Enum), //ErrorView type not available on PS3 + /* PSNativeCommandUseErrorActionPreference */ typeof(bool), }; internal enum AutomaticVariable From 9314e69a24f1f98aa976ba0ac3a96784ed0956e9 Mon Sep 17 00:00:00 2001 From: Hubert Bukowski Date: Tue, 16 Jan 2024 23:32:10 +0100 Subject: [PATCH 021/130] Prevent NullReferenceException for null analysis type. (#1949) Many analysis objects here have null Type field value. This leads to premature exiting the method. --- Engine/VariableAnalysisBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/VariableAnalysisBase.cs b/Engine/VariableAnalysisBase.cs index b55119d7a..77c421f3c 100644 --- a/Engine/VariableAnalysisBase.cs +++ b/Engine/VariableAnalysisBase.cs @@ -995,7 +995,7 @@ internal static Tuple, Dictionary String.Equals(item.Name, analysis.Type.FullName, StringComparison.OrdinalIgnoreCase)); + TypeDefinitionAst psClass = Classes.FirstOrDefault(item => String.Equals(item.Name, analysis.Type?.FullName, StringComparison.OrdinalIgnoreCase)); Type possibleType = AssignmentTarget.GetTypeFromMemberExpressionAst(memAst, analysis, psClass); #endif From da646722916d8a650a5e908b1b0001d8485c12fb Mon Sep 17 00:00:00 2001 From: Michael Van Leeuwen Date: Thu, 18 Jan 2024 10:18:07 -0800 Subject: [PATCH 022/130] Convert UseSingularNouns to configurable rule and add Windows to allowlist (#1858) * Add Windows to the UseSingularNouns allow list * Add test case for Windows verb * Refactor UseSingularNouns to configurable rule and add tests * Update UseSingularNouns docs with configuration information * Remove extra test code --------- Co-authored-by: Christoph Bergmeister --- Rules/UseSingularNouns.cs | 26 +++++++++-------- .../UseSingularNounsReservedVerbs.tests.ps1 | 29 ++++++++++++++++++- docs/Rules/README.md | 2 +- docs/Rules/UseSingularNouns.md | 21 ++++++++++++++ 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Rules/UseSingularNouns.cs b/Rules/UseSingularNouns.cs index 218537459..21a6afa90 100644 --- a/Rules/UseSingularNouns.cs +++ b/Rules/UseSingularNouns.cs @@ -32,13 +32,15 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #if !CORECLR [Export(typeof(IScriptRule))] #endif - public class CmdletSingularNoun : IScriptRule + public class CmdletSingularNoun : ConfigurableRule { + [ConfigurableRuleProperty(defaultValue: new string[] { "Data", "Windows" })] + public string[] NounAllowList { get; set; } - private readonly string[] nounAllowList = + public CmdletSingularNoun() { - "Data" - }; + Enable = true; + } /// /// Checks that all defined cmdlet use singular noun @@ -46,7 +48,7 @@ public class CmdletSingularNoun : IScriptRule /// /// /// - public IEnumerable AnalyzeScript(Ast ast, string fileName) + public override IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullCommandInfoError); @@ -70,7 +72,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (pluralizer.CanOnlyBePlural(noun)) { - if (nounAllowList.Contains(noun, StringComparer.OrdinalIgnoreCase)) + if (NounAllowList.Contains(noun, StringComparer.OrdinalIgnoreCase)) { continue; } @@ -99,7 +101,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) /// GetName: Retrieves the name of this rule. /// /// The name of this rule - public string GetName() + public override string GetName() { return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseSingularNounsName); } @@ -108,7 +110,7 @@ public string GetName() /// GetName: Retrieves the common name of this rule. /// /// The common name of this rule - public string GetCommonName() + public override string GetCommonName() { return string.Format(CultureInfo.CurrentCulture, Strings.UseSingularNounsCommonName); } @@ -117,7 +119,7 @@ public string GetCommonName() /// GetDescription: Retrieves the description of this rule. /// /// The description of this rule - public string GetDescription() + public override string GetDescription() { return string.Format(CultureInfo.CurrentCulture, Strings.UseSingularNounsDescription); } @@ -125,7 +127,7 @@ public string GetDescription() /// /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. /// - public SourceType GetSourceType() + public override SourceType GetSourceType() { return SourceType.Builtin; } @@ -134,7 +136,7 @@ public SourceType GetSourceType() /// GetSeverity: Retrieves the severity of the rule: error, warning of information. /// /// - public RuleSeverity GetSeverity() + public override RuleSeverity GetSeverity() { return RuleSeverity.Warning; } @@ -142,7 +144,7 @@ public RuleSeverity GetSeverity() /// /// GetSourceName: Retrieves the module/assembly name the rule is from. /// - public string GetSourceName() + public override string GetSourceName() { return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); } diff --git a/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 b/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 index db1af36ce..6d4724236 100644 --- a/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 +++ b/Tests/Rules/UseSingularNounsReservedVerbs.tests.ps1 @@ -32,7 +32,7 @@ Describe "UseSingularNouns" { Context "When function names have nouns from allowlist" { - It "ignores function name ending with Data" { + It "ignores function name ending with Data by default" { $nounViolationScript = @' Function Add-SomeData { @@ -44,6 +44,33 @@ Write-Output "Adding some data" -OutVariable violations $violations.Count | Should -Be 0 } + + It "ignores function name ending with Windows by default" { + $nounViolationScript = @' +Function Test-Windows +{ +Write-Output "Testing Microsoft Windows" +} +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $nounViolationScript ` + -IncludeRule "PSUseSingularNouns" ` + -OutVariable violations + $violations.Count | Should -Be 0 + } + + It "ignores function names defined in settings" { + $nounViolationScript = @' +Function Get-Bananas +{ +Write-Output "Bananas" +} +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $nounViolationScript -Settings @{ + IncludeRules = @("PSUseSingularNouns") + Rules = @{ PSUseSingularNouns = @{ NounAllowList = "Bananas" } } + } | Should -BeNullOrEmpty + } + } Context "When there are no violations" { diff --git a/docs/Rules/README.md b/docs/Rules/README.md index f5c8d8bd3..42c2003ad 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -75,7 +75,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [UseProcessBlockForPipelineCommand](./UseProcessBlockForPipelineCommand.md) | Warning | Yes | | | [UsePSCredentialType](./UsePSCredentialType.md) | Warning | Yes | | | [UseShouldProcessForStateChangingFunctions](./UseShouldProcessForStateChangingFunctions.md) | Warning | Yes | | -| [UseSingularNouns](./UseSingularNouns.md) | Warning | Yes | | +| [UseSingularNouns](./UseSingularNouns.md) | Warning | Yes | Yes | | [UseSupportsShouldProcess](./UseSupportsShouldProcess.md) | Warning | Yes | | | [UseToExportFieldsInManifest](./UseToExportFieldsInManifest.md) | Warning | Yes | | | [UseUsingScopeModifierInNewRunspaces](./UseUsingScopeModifierInNewRunspaces.md) | Warning | Yes | | diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index caf032e03..387d48c2d 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -22,6 +22,27 @@ function Get-Elements { } ``` +## Configuration + +```powershell +Rules = @{ + UseSingularNouns = @{ + NounAllowList = 'Data', 'Windows', 'Foos' + Enable = $true + } +} +``` + +### Parameters + +#### `UseSingularNouns: string[]` (Default value is `{'Data', 'Windows'}`) + +Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are excluded by default + +#### Enable: `bool` (Default value is `$true`) + +Enable or disable the rule during ScriptAnalyzer invocation. + ## How Change plurals to singular. From 2245064e0067a4a50310951fa8351b7b1819525b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:44:22 -0800 Subject: [PATCH 023/130] Bump Newtonsoft.Json to 13.0.3 (#1866) * Bump Newtonsoft.Json from 12.0.3 to 13.0.2 Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 12.0.3 to 13.0.2. - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/12.0.3...13.0.2) --- updated-dependencies: - dependency-name: Newtonsoft.Json dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Apply suggestions from code review * Bump all version to 13.0.3 * Apply suggestions from code review --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christoph Bergmeister --- .../Microsoft.PowerShell.CrossCompatibility.csproj | 6 +++--- Rules/Rules.csproj | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index eaa88be73..737af527f 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -11,15 +11,15 @@ - - + + - + diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index e3ab388c7..7a953d297 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -17,15 +17,15 @@ - - + + - + From cc2f9b7f17a44a68f30f76fe116a811718482161 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 18 Jan 2024 18:45:07 +0000 Subject: [PATCH 024/130] Use latest .NET 6.0 SDK patch version and update devcontainer to use .NET 6 as well (#1955) * Update global.json * Update README.md * Update Dockerfile * Update devcontainer.json * Update devcontainer.json * Update devcontainer.json * Update global.json --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 24 +++++++++++++----------- README.md | 3 +-- global.json | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 06cc1380a..b731d046f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -FROM mcr.microsoft.com/dotnet/core/sdk:3.1.419 +FROM mcr.microsoft.com/dotnet/sdk:6.0 RUN pwsh --command Install-Module platyPS,Pester -Force diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0ee4c2049..d48d48c38 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,14 +1,16 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at -// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/dotnetcore-3.0 +// For format details, see https://aka.ms/vscode-remote/devcontainer.json { - "name": "C# (.NET Core 3.1)", + "name": "C# (.NET 6.0)", "dockerFile": "Dockerfile", - "settings": { - "terminal.integrated.shell.linux": "/usr/bin/pwsh" - }, - "postCreateCommand": "dotnet restore", - "extensions": [ - "ms-dotnettools.csharp", - "ms-vscode.powershell-preview" - ] + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "pwsh" + }, + "extensions": [ + "ms-dotnettools.csharp", + "ms-vscode.powershell" + ] + } + } } diff --git a/README.md b/README.md index 0e82f34be..97c66848d 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,7 @@ To install **PSScriptAnalyzer** from source code: ### Requirements -- [.NET 6.0.11 containing 6.0.403 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) or - newer patch release +- [Latest .NET 6.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) * If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows. * Optionally but recommended for development: [Visual Studio 2017/2019](https://www.visualstudio.com/downloads) - [Pester v5 PowerShell module, available on PowerShell Gallery](https://github.com/pester/Pester) diff --git a/global.json b/global.json index 6d86a5b04..1cec616b3 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.403" + "version": "6.0.418" } } From 1e79ca362d13adbeed95e6092b1b19cb7a38f32c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:56:42 -0800 Subject: [PATCH 025/130] Bump Microsoft.Management.Infrastructure from 1.0.0 to 3.0.0 for PowerShell 7 only (#1947) * Bump Microsoft.Management.Infrastructure from 1.0.0 to 3.0.0 Bumps [Microsoft.Management.Infrastructure](https://github.com/PowerShell/MMI) from 1.0.0 to 3.0.0. - [Commits](https://github.com/PowerShell/MMI/commits) --- updated-dependencies: - dependency-name: Microsoft.Management.Infrastructure dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christoph Bergmeister --- .../Microsoft.PowerShell.CrossCompatibility.csproj | 4 ++-- Rules/Rules.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index 737af527f..20dc8d226 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -1,4 +1,4 @@ - + 1.21.0 @@ -14,7 +14,7 @@ - + diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 7a953d297..9cd5a19bc 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -1,4 +1,4 @@ - + 1.21.0 @@ -20,7 +20,7 @@ - + From 5c32f5559d4096ae2dc0ffc0f9d59fcbfbd02ccb Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 1 Feb 2024 17:35:40 +0000 Subject: [PATCH 026/130] Add common parameter ProgressAction (added in PS 7.4) to help test exclusion list --- Tests/Engine/ModuleHelp.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Engine/ModuleHelp.Tests.ps1 b/Tests/Engine/ModuleHelp.Tests.ps1 index e2dbea8c4..85e93fe3e 100644 --- a/Tests/Engine/ModuleHelp.Tests.ps1 +++ b/Tests/Engine/ModuleHelp.Tests.ps1 @@ -175,7 +175,7 @@ Describe 'Cmdlet parameter help' { ) BEGIN { - $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' + $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'ProgressAction', 'Verbose', 'WarningAction', 'WarningVariable' $parameters = @() } PROCESS { From df3551eda2dce113e1eecf8a3ff03654c9bf24a4 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 13 Feb 2024 18:14:28 +0000 Subject: [PATCH 027/130] AvoidUsingPositionalParameter: Check if command has parameters to avoid having az in default CommandAllowList (#1850) * AvoidUsingPositionalParameter : Check if command has parameters * fix syntax * remove unneeded test * Update Rules/AvoidPositionalParameters.cs --- Engine/Helper.cs | 5 +++-- Rules/AvoidPositionalParameters.cs | 7 +++++-- Rules/UseCmdletCorrectly.cs | 2 +- docs/Rules/AvoidUsingPositionalParameters.md | 6 +++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Engine/Helper.cs b/Engine/Helper.cs index 528a3fe88..ded37b011 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -609,14 +609,15 @@ public bool HasSplattedVariable(CommandAst cmdAst) /// /// /// - public bool IsKnownCmdletFunctionOrExternalScript(CommandAst cmdAst) + public bool IsKnownCmdletFunctionOrExternalScript(CommandAst cmdAst, out CommandInfo commandInfo) { + commandInfo = null; if (cmdAst == null) { return false; } - var commandInfo = GetCommandInfo(cmdAst.GetCommandName()); + commandInfo = GetCommandInfo(cmdAst.GetCommandName()); if (commandInfo == null) { return false; diff --git a/Rules/AvoidPositionalParameters.cs b/Rules/AvoidPositionalParameters.cs index 3c6ec9626..2071baebd 100644 --- a/Rules/AvoidPositionalParameters.cs +++ b/Rules/AvoidPositionalParameters.cs @@ -6,6 +6,7 @@ using System.Management.Automation.Language; using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; using System.Linq; +using System.Management.Automation; #if !CORECLR using System.ComponentModel.Composition; #endif @@ -21,7 +22,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif public class AvoidPositionalParameters : ConfigurableRule { - [ConfigurableRuleProperty(defaultValue: new string[] { "az" })] + [ConfigurableRuleProperty(defaultValue: new string[] { })] public string[] CommandAllowList { get; set; } public AvoidPositionalParameters() @@ -61,9 +62,11 @@ public override IEnumerable AnalyzeScript(Ast ast, string file // MSDN: CommandAst.GetCommandName Method if (cmdAst.GetCommandName() == null) continue; - if ((Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst) || declaredFunctionNames.Contains(cmdAst.GetCommandName())) && + if ((Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst, out CommandInfo commandInfo) || declaredFunctionNames.Contains(cmdAst.GetCommandName())) && (Helper.Instance.PositionalParameterUsed(cmdAst, true))) { + if (commandInfo?.CommandType == CommandTypes.Application) continue; + PipelineAst parent = cmdAst.Parent as PipelineAst; string commandName = cmdAst.GetCommandName(); diff --git a/Rules/UseCmdletCorrectly.cs b/Rules/UseCmdletCorrectly.cs index 0239d95fc..ccec27e0b 100644 --- a/Rules/UseCmdletCorrectly.cs +++ b/Rules/UseCmdletCorrectly.cs @@ -100,7 +100,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst) } // Positional parameters could be mandatory, so we assume all is well - if (Helper.Instance.PositionalParameterUsed(cmdAst) && Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst)) + if (Helper.Instance.PositionalParameterUsed(cmdAst) && Helper.Instance.IsKnownCmdletFunctionOrExternalScript(cmdAst, out _)) { return true; } diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index 2d1dab690..5d1706a4b 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -25,7 +25,7 @@ supplied. A simple example where the risk of using positional parameters is negl ```powershell Rules = @{ PSAvoidUsingPositionalParameters = @{ - CommandAllowList = 'az', 'Join-Path' + CommandAllowList = 'Join-Path', 'MyCmdletOrScript' Enable = $true } } @@ -33,9 +33,9 @@ Rules = @{ ### Parameters -#### CommandAllowList: string[] (Default value is 'az') +#### CommandAllowList: string[] (Default value is @()') -Commands to be excluded from this rule. `az` is excluded by default because starting with version 2.40.0 the entrypoint of the AZ CLI became an `az.ps1` script but this script does not have any named parameters and just passes them on using `$args` as is to the Python process that it starts, therefore it is still a CLI and not a PowerShell command. +Commands or scripts to be excluded from this rule. #### Enable: bool (Default value is `$true`) From 9d57bad4903aa0e9a902b84bdc260b2f8c316684 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 13 Feb 2024 18:15:54 +0000 Subject: [PATCH 028/130] Bump version from 1.21.0 to 1.22.0 (#1965) --- Engine/Engine.csproj | 4 ++-- Engine/PSScriptAnalyzer.psd1 | 2 +- .../Microsoft.PowerShell.CrossCompatibility.csproj | 6 +++--- Rules/Rules.csproj | 4 ++-- docs/Cmdlets/Get-ScriptAnalyzerRule.md | 2 +- docs/Cmdlets/Invoke-Formatter.md | 2 +- docs/Cmdlets/Invoke-ScriptAnalyzer.md | 2 +- docs/Cmdlets/PSScriptAnalyzer.md | 4 ++-- docs/Rules/AlignAssignmentStatement.md | 2 +- docs/Rules/AvoidAssignmentToAutomaticVariable.md | 2 +- docs/Rules/AvoidDefaultValueForMandatoryParameter.md | 2 +- docs/Rules/AvoidDefaultValueSwitchParameter.md | 2 +- docs/Rules/AvoidExclaimOperator.md | 2 +- docs/Rules/AvoidGlobalAliases.md | 2 +- docs/Rules/AvoidGlobalFunctions.md | 2 +- docs/Rules/AvoidGlobalVars.md | 2 +- docs/Rules/AvoidInvokingEmptyMembers.md | 2 +- docs/Rules/AvoidLongLines.md | 2 +- docs/Rules/AvoidMultipleTypeAttributes.md | 2 +- docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md | 2 +- docs/Rules/AvoidOverwritingBuiltInCmdlets.md | 2 +- docs/Rules/AvoidSemicolonsAsLineTerminators.md | 2 +- docs/Rules/AvoidShouldContinueWithoutForce.md | 2 +- docs/Rules/AvoidTrailingWhitespace.md | 2 +- docs/Rules/AvoidUsingBrokenHashAlgorithms.md | 2 +- docs/Rules/AvoidUsingCmdletAliases.md | 2 +- docs/Rules/AvoidUsingComputerNameHardcoded.md | 2 +- docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md | 2 +- docs/Rules/AvoidUsingDeprecatedManifestFields.md | 2 +- docs/Rules/AvoidUsingDoubleQuotesForConstantString.md | 2 +- docs/Rules/AvoidUsingEmptyCatchBlock.md | 2 +- docs/Rules/AvoidUsingInvokeExpression.md | 2 +- docs/Rules/AvoidUsingPlainTextForPassword.md | 2 +- docs/Rules/AvoidUsingPositionalParameters.md | 2 +- docs/Rules/AvoidUsingUsernameAndPasswordParams.md | 2 +- docs/Rules/AvoidUsingWMICmdlet.md | 2 +- docs/Rules/AvoidUsingWriteHost.md | 2 +- docs/Rules/DSCDscExamplesPresent.md | 2 +- docs/Rules/DSCDscTestsPresent.md | 2 +- docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md | 2 +- docs/Rules/DSCStandardDSCFunctionsInResource.md | 2 +- docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md | 2 +- docs/Rules/DSCUseIdenticalParametersForDSC.md | 2 +- docs/Rules/DSCUseVerboseMessageInDSCResource.md | 2 +- docs/Rules/MisleadingBacktick.md | 2 +- docs/Rules/MissingModuleManifestField.md | 2 +- docs/Rules/PlaceCloseBrace.md | 2 +- docs/Rules/PlaceOpenBrace.md | 2 +- docs/Rules/PossibleIncorrectComparisonWithNull.md | 2 +- docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md | 2 +- docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md | 2 +- docs/Rules/ProvideCommentHelp.md | 2 +- docs/Rules/README.md | 2 +- docs/Rules/ReservedCmdletChar.md | 2 +- docs/Rules/ReservedParams.md | 2 +- docs/Rules/ReviewUnusedParameter.md | 2 +- docs/Rules/ShouldProcess.md | 2 +- docs/Rules/UseApprovedVerbs.md | 2 +- docs/Rules/UseBOMForUnicodeEncodedFile.md | 2 +- docs/Rules/UseCmdletCorrectly.md | 2 +- docs/Rules/UseCompatibleCmdlets.md | 2 +- docs/Rules/UseCompatibleCommands.md | 2 +- docs/Rules/UseCompatibleSyntax.md | 2 +- docs/Rules/UseCompatibleTypes.md | 2 +- docs/Rules/UseConsistentIndentation.md | 2 +- docs/Rules/UseConsistentWhitespace.md | 2 +- docs/Rules/UseCorrectCasing.md | 2 +- docs/Rules/UseDeclaredVarsMoreThanAssignments.md | 2 +- docs/Rules/UseLiteralInitializerForHashtable.md | 2 +- docs/Rules/UseOutputTypeCorrectly.md | 2 +- docs/Rules/UsePSCredentialType.md | 2 +- docs/Rules/UseProcessBlockForPipelineCommand.md | 2 +- docs/Rules/UseShouldProcessForStateChangingFunctions.md | 2 +- docs/Rules/UseSingularNouns.md | 2 +- docs/Rules/UseSupportsShouldProcess.md | 2 +- docs/Rules/UseToExportFieldsInManifest.md | 2 +- docs/Rules/UseUTF8EncodingForHelpFile.md | 2 +- docs/Rules/UseUsingScopeModifierInNewRunspaces.md | 2 +- 78 files changed, 83 insertions(+), 83 deletions(-) diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index a22df13ee..860700b0b 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -1,10 +1,10 @@  - 1.21.0 + 1.22.0 net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer - 1.21.0 + 1.22.0 Engine Microsoft.Windows.PowerShell.ScriptAnalyzer 9.0 diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index f3692b483..feae6fba4 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -11,7 +11,7 @@ Author = 'Microsoft Corporation' RootModule = 'PSScriptAnalyzer.psm1' # Version number of this module. -ModuleVersion = '1.21.0' +ModuleVersion = '1.22.0' # ID used to uniquely identify this module GUID = 'd6245802-193d-4068-a631-8863a4342a18' diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index 20dc8d226..20240cc7c 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -1,9 +1,9 @@ - + - 1.21.0 + 1.22.0 netstandard2.0;net462 - 1.21.0 + 1.22.0 diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 9cd5a19bc..a9497824d 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -1,10 +1,10 @@ - 1.21.0 + 1.22.0 net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules - 1.21.0 + 1.22.0 Rules Microsoft.Windows.PowerShell.ScriptAnalyzer true diff --git a/docs/Cmdlets/Get-ScriptAnalyzerRule.md b/docs/Cmdlets/Get-ScriptAnalyzerRule.md index 64bd7307f..90366c5b0 100644 --- a/docs/Cmdlets/Get-ScriptAnalyzerRule.md +++ b/docs/Cmdlets/Get-ScriptAnalyzerRule.md @@ -1,7 +1,7 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/get-scriptanalyzerrule?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/Invoke-Formatter.md b/docs/Cmdlets/Invoke-Formatter.md index 91dc68add..88dd320ae 100644 --- a/docs/Cmdlets/Invoke-Formatter.md +++ b/docs/Cmdlets/Invoke-Formatter.md @@ -1,7 +1,7 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/invoke-formatter?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/Invoke-ScriptAnalyzer.md b/docs/Cmdlets/Invoke-ScriptAnalyzer.md index 4605d1d89..9d0195c86 100644 --- a/docs/Cmdlets/Invoke-ScriptAnalyzer.md +++ b/docs/Cmdlets/Invoke-ScriptAnalyzer.md @@ -1,7 +1,7 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/invoke-scriptanalyzer?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index 1db78a547..b7267e8b5 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -1,10 +1,10 @@ --- Download Help Link: https://aka.ms/ps-modules-help -Help Version: 1.21.0 +Help Version: 1.22.0 Locale: en-US Module Guid: d6245802-193d-4068-a631-8863a4342a18 Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 --- diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index 21d5cfbc7..e41bff7a1 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -1,6 +1,6 @@ --- description: Align assignment statement -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AlignAssignmentStatement diff --git a/docs/Rules/AvoidAssignmentToAutomaticVariable.md b/docs/Rules/AvoidAssignmentToAutomaticVariable.md index 7a978e32a..572e9eb3a 100644 --- a/docs/Rules/AvoidAssignmentToAutomaticVariable.md +++ b/docs/Rules/AvoidAssignmentToAutomaticVariable.md @@ -1,6 +1,6 @@ --- description: Changing automatic variables might have undesired side effects -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidAssignmentToAutomaticVariable diff --git a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md index 71a82605f..ac5fdfc9c 100644 --- a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md +++ b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md @@ -1,6 +1,6 @@ --- description: Avoid Default Value For Mandatory Parameter -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueForMandatoryParameter diff --git a/docs/Rules/AvoidDefaultValueSwitchParameter.md b/docs/Rules/AvoidDefaultValueSwitchParameter.md index f08ac7732..74e76bdc6 100644 --- a/docs/Rules/AvoidDefaultValueSwitchParameter.md +++ b/docs/Rules/AvoidDefaultValueSwitchParameter.md @@ -1,6 +1,6 @@ --- description: Switch Parameters Should Not Default To True -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueSwitchParameter diff --git a/docs/Rules/AvoidExclaimOperator.md b/docs/Rules/AvoidExclaimOperator.md index 5f858feca..5ef833fc0 100644 --- a/docs/Rules/AvoidExclaimOperator.md +++ b/docs/Rules/AvoidExclaimOperator.md @@ -1,6 +1,6 @@ --- description: Avoid exclaim operator -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/14/2023 ms.topic: reference title: AvoidExclaimOperator diff --git a/docs/Rules/AvoidGlobalAliases.md b/docs/Rules/AvoidGlobalAliases.md index 9b9463196..36c581ca6 100644 --- a/docs/Rules/AvoidGlobalAliases.md +++ b/docs/Rules/AvoidGlobalAliases.md @@ -1,6 +1,6 @@ --- description: Avoid global aliases. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalAliases diff --git a/docs/Rules/AvoidGlobalFunctions.md b/docs/Rules/AvoidGlobalFunctions.md index cf4ee8fad..ef267ef8b 100644 --- a/docs/Rules/AvoidGlobalFunctions.md +++ b/docs/Rules/AvoidGlobalFunctions.md @@ -1,6 +1,6 @@ --- description: Avoid global functions and aliases -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalFunctions diff --git a/docs/Rules/AvoidGlobalVars.md b/docs/Rules/AvoidGlobalVars.md index 54da52443..0edcd4753 100644 --- a/docs/Rules/AvoidGlobalVars.md +++ b/docs/Rules/AvoidGlobalVars.md @@ -1,6 +1,6 @@ --- description: No Global Variables -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalVars diff --git a/docs/Rules/AvoidInvokingEmptyMembers.md b/docs/Rules/AvoidInvokingEmptyMembers.md index 88665f1af..d512601ad 100644 --- a/docs/Rules/AvoidInvokingEmptyMembers.md +++ b/docs/Rules/AvoidInvokingEmptyMembers.md @@ -1,6 +1,6 @@ --- description: Avoid Invoking Empty Members -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidInvokingEmptyMembers diff --git a/docs/Rules/AvoidLongLines.md b/docs/Rules/AvoidLongLines.md index eab82ad64..2dd94369a 100644 --- a/docs/Rules/AvoidLongLines.md +++ b/docs/Rules/AvoidLongLines.md @@ -1,6 +1,6 @@ --- description: Avoid long lines -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidLongLines diff --git a/docs/Rules/AvoidMultipleTypeAttributes.md b/docs/Rules/AvoidMultipleTypeAttributes.md index 5e5dbea69..f97d183c0 100644 --- a/docs/Rules/AvoidMultipleTypeAttributes.md +++ b/docs/Rules/AvoidMultipleTypeAttributes.md @@ -1,6 +1,6 @@ --- description: Avoid multiple type specifiers on parameters. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidMultipleTypeAttributes diff --git a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md index 4ba4d8e9c..cee57e0e2 100644 --- a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md +++ b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md @@ -1,6 +1,6 @@ --- description: Avoid using null or empty HelpMessage parameter attribute. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidNullOrEmptyHelpMessageAttribute diff --git a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md index 7ac93454e..cd7f9c3a6 100644 --- a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md +++ b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md @@ -1,6 +1,6 @@ --- description: Avoid overwriting built in cmdlets -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidOverwritingBuiltInCmdlets diff --git a/docs/Rules/AvoidSemicolonsAsLineTerminators.md b/docs/Rules/AvoidSemicolonsAsLineTerminators.md index 0a496678c..f6d0d69b7 100644 --- a/docs/Rules/AvoidSemicolonsAsLineTerminators.md +++ b/docs/Rules/AvoidSemicolonsAsLineTerminators.md @@ -1,6 +1,6 @@ --- description: Avoid semicolons as line terminators -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidSemicolonsAsLineTerminators diff --git a/docs/Rules/AvoidShouldContinueWithoutForce.md b/docs/Rules/AvoidShouldContinueWithoutForce.md index 1ffd4ed7b..dba5e6857 100644 --- a/docs/Rules/AvoidShouldContinueWithoutForce.md +++ b/docs/Rules/AvoidShouldContinueWithoutForce.md @@ -1,6 +1,6 @@ --- description: Avoid Using ShouldContinue Without Boolean Force Parameter -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidShouldContinueWithoutForce diff --git a/docs/Rules/AvoidTrailingWhitespace.md b/docs/Rules/AvoidTrailingWhitespace.md index 2067cee75..9740d429c 100644 --- a/docs/Rules/AvoidTrailingWhitespace.md +++ b/docs/Rules/AvoidTrailingWhitespace.md @@ -1,6 +1,6 @@ --- description: Avoid trailing whitespace -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidTrailingWhitespace diff --git a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md index 52510a5e3..a8fc60dc1 100644 --- a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md +++ b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md @@ -1,6 +1,6 @@ --- description: Avoid using broken hash algorithms -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingBrokenHashAlgorithms diff --git a/docs/Rules/AvoidUsingCmdletAliases.md b/docs/Rules/AvoidUsingCmdletAliases.md index 87c13b33c..25adb3767 100644 --- a/docs/Rules/AvoidUsingCmdletAliases.md +++ b/docs/Rules/AvoidUsingCmdletAliases.md @@ -1,6 +1,6 @@ --- description: Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingCmdletAliases diff --git a/docs/Rules/AvoidUsingComputerNameHardcoded.md b/docs/Rules/AvoidUsingComputerNameHardcoded.md index 6ff30b54a..f15b69c06 100644 --- a/docs/Rules/AvoidUsingComputerNameHardcoded.md +++ b/docs/Rules/AvoidUsingComputerNameHardcoded.md @@ -1,6 +1,6 @@ --- description: Avoid Using ComputerName Hardcoded -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingComputerNameHardcoded diff --git a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md index 3c651a400..fa6a0df0e 100644 --- a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md +++ b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md @@ -1,6 +1,6 @@ --- description: Avoid Using SecureString With Plain Text -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingConvertToSecureStringWithPlainText diff --git a/docs/Rules/AvoidUsingDeprecatedManifestFields.md b/docs/Rules/AvoidUsingDeprecatedManifestFields.md index bac27c065..32bc4d5b5 100644 --- a/docs/Rules/AvoidUsingDeprecatedManifestFields.md +++ b/docs/Rules/AvoidUsingDeprecatedManifestFields.md @@ -1,6 +1,6 @@ --- description: Avoid Using Deprecated Manifest Fields -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDeprecatedManifestFields diff --git a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md index ab732fc48..002a72aca 100644 --- a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md +++ b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md @@ -1,6 +1,6 @@ --- description: Avoid using double quotes if the string is constant. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDoubleQuotesForConstantString diff --git a/docs/Rules/AvoidUsingEmptyCatchBlock.md b/docs/Rules/AvoidUsingEmptyCatchBlock.md index 0d0749f06..8c944563f 100644 --- a/docs/Rules/AvoidUsingEmptyCatchBlock.md +++ b/docs/Rules/AvoidUsingEmptyCatchBlock.md @@ -1,6 +1,6 @@ --- description: Avoid Using Empty Catch Block -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingEmptyCatchBlock diff --git a/docs/Rules/AvoidUsingInvokeExpression.md b/docs/Rules/AvoidUsingInvokeExpression.md index de35d16a4..570b51ac4 100644 --- a/docs/Rules/AvoidUsingInvokeExpression.md +++ b/docs/Rules/AvoidUsingInvokeExpression.md @@ -1,6 +1,6 @@ --- description: Avoid Using Invoke-Expression -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingInvokeExpression diff --git a/docs/Rules/AvoidUsingPlainTextForPassword.md b/docs/Rules/AvoidUsingPlainTextForPassword.md index 2d53fbbbe..3d2b9f6e0 100644 --- a/docs/Rules/AvoidUsingPlainTextForPassword.md +++ b/docs/Rules/AvoidUsingPlainTextForPassword.md @@ -1,6 +1,6 @@ --- description: Avoid Using Plain Text For Password Parameter -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingPlainTextForPassword diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index 5d1706a4b..0078ffd70 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -1,6 +1,6 @@ --- description: Avoid Using Positional Parameters -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingPositionalParameters diff --git a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md index f7c634ff2..b37bbfd42 100644 --- a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md +++ b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md @@ -1,6 +1,6 @@ --- description: Avoid Using Username and Password Parameters -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingUsernameAndPasswordParams diff --git a/docs/Rules/AvoidUsingWMICmdlet.md b/docs/Rules/AvoidUsingWMICmdlet.md index f1e633fe7..47888b361 100644 --- a/docs/Rules/AvoidUsingWMICmdlet.md +++ b/docs/Rules/AvoidUsingWMICmdlet.md @@ -1,6 +1,6 @@ --- description: Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWMICmdlet diff --git a/docs/Rules/AvoidUsingWriteHost.md b/docs/Rules/AvoidUsingWriteHost.md index 31396af81..930202d4f 100644 --- a/docs/Rules/AvoidUsingWriteHost.md +++ b/docs/Rules/AvoidUsingWriteHost.md @@ -1,6 +1,6 @@ --- description: Avoid Using Write-Host -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWriteHost diff --git a/docs/Rules/DSCDscExamplesPresent.md b/docs/Rules/DSCDscExamplesPresent.md index a4ab2c792..aaf84d185 100644 --- a/docs/Rules/DSCDscExamplesPresent.md +++ b/docs/Rules/DSCDscExamplesPresent.md @@ -1,6 +1,6 @@ --- description: DSC examples are present -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCDscExamplesPresent diff --git a/docs/Rules/DSCDscTestsPresent.md b/docs/Rules/DSCDscTestsPresent.md index f96ecbc77..22eca027f 100644 --- a/docs/Rules/DSCDscTestsPresent.md +++ b/docs/Rules/DSCDscTestsPresent.md @@ -1,6 +1,6 @@ --- description: Dsc tests are present -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCDscTestsPresent diff --git a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md index 2b0a4e058..882a492ca 100644 --- a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md +++ b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md @@ -1,6 +1,6 @@ --- description: Return Correct Types For DSC Functions -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCReturnCorrectTypesForDSCFunctions diff --git a/docs/Rules/DSCStandardDSCFunctionsInResource.md b/docs/Rules/DSCStandardDSCFunctionsInResource.md index 9fbc2c672..6280e412c 100644 --- a/docs/Rules/DSCStandardDSCFunctionsInResource.md +++ b/docs/Rules/DSCStandardDSCFunctionsInResource.md @@ -1,6 +1,6 @@ --- description: Use Standard Get/Set/Test TargetResource functions in DSC Resource -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCStandardDSCFunctionsInResource diff --git a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md index 65a612712..2c6f04709 100644 --- a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md @@ -1,6 +1,6 @@ --- description: Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalMandatoryParametersForDSC diff --git a/docs/Rules/DSCUseIdenticalParametersForDSC.md b/docs/Rules/DSCUseIdenticalParametersForDSC.md index ea72db0ba..2497f41ad 100644 --- a/docs/Rules/DSCUseIdenticalParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalParametersForDSC.md @@ -1,6 +1,6 @@ --- description: Use Identical Parameters For DSC Test and Set Functions -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalParametersForDSC diff --git a/docs/Rules/DSCUseVerboseMessageInDSCResource.md b/docs/Rules/DSCUseVerboseMessageInDSCResource.md index 0af52de06..49ef63ed4 100644 --- a/docs/Rules/DSCUseVerboseMessageInDSCResource.md +++ b/docs/Rules/DSCUseVerboseMessageInDSCResource.md @@ -1,6 +1,6 @@ --- description: Use verbose message in DSC resource -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseVerboseMessageInDSCResource diff --git a/docs/Rules/MisleadingBacktick.md b/docs/Rules/MisleadingBacktick.md index 02ec622dc..3194e706f 100644 --- a/docs/Rules/MisleadingBacktick.md +++ b/docs/Rules/MisleadingBacktick.md @@ -1,6 +1,6 @@ --- description: Misleading Backtick -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: MisleadingBacktick diff --git a/docs/Rules/MissingModuleManifestField.md b/docs/Rules/MissingModuleManifestField.md index a508e7191..efcf70af1 100644 --- a/docs/Rules/MissingModuleManifestField.md +++ b/docs/Rules/MissingModuleManifestField.md @@ -1,6 +1,6 @@ --- description: Module Manifest Fields -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: MissingModuleManifestField diff --git a/docs/Rules/PlaceCloseBrace.md b/docs/Rules/PlaceCloseBrace.md index 11eefc9d8..d181eebd9 100644 --- a/docs/Rules/PlaceCloseBrace.md +++ b/docs/Rules/PlaceCloseBrace.md @@ -1,6 +1,6 @@ --- description: Place close braces -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PlaceCloseBrace diff --git a/docs/Rules/PlaceOpenBrace.md b/docs/Rules/PlaceOpenBrace.md index b3e48e0ce..bc3e4b5e2 100644 --- a/docs/Rules/PlaceOpenBrace.md +++ b/docs/Rules/PlaceOpenBrace.md @@ -1,6 +1,6 @@ --- description: Place open braces consistently -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PlaceOpenBrace diff --git a/docs/Rules/PossibleIncorrectComparisonWithNull.md b/docs/Rules/PossibleIncorrectComparisonWithNull.md index 1c9ea8d24..d3c62321b 100644 --- a/docs/Rules/PossibleIncorrectComparisonWithNull.md +++ b/docs/Rules/PossibleIncorrectComparisonWithNull.md @@ -1,6 +1,6 @@ --- description: Null Comparison -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectComparisonWithNull diff --git a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md index 7981512ff..8d046e302 100644 --- a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md @@ -1,6 +1,6 @@ --- description: Equal sign is not an assignment operator. Did you mean the equality operator \'-eq\'? -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfAssignmentOperator diff --git a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md index 759252cd3..cfd00c05b 100644 --- a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md @@ -1,6 +1,6 @@ --- description: \'>\' is not a comparison operator. Use \'-gt\' (greater than) or \'-ge\' (greater or equal). -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfRedirectionOperator diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index e8a45b152..c6e373f36 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -1,6 +1,6 @@ --- description: Basic Comment Help -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ProvideCommentHelp diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 42c2003ad..ccfaa71ea 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,6 +1,6 @@ --- description: List of PSScriptAnalyzer rules -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: List of PSScriptAnalyzer rules diff --git a/docs/Rules/ReservedCmdletChar.md b/docs/Rules/ReservedCmdletChar.md index 2fc57ca3e..e4195d42b 100644 --- a/docs/Rules/ReservedCmdletChar.md +++ b/docs/Rules/ReservedCmdletChar.md @@ -1,6 +1,6 @@ --- description: Reserved Cmdlet Chars -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ReservedCmdletChar diff --git a/docs/Rules/ReservedParams.md b/docs/Rules/ReservedParams.md index 2a254683f..2d38c1af2 100644 --- a/docs/Rules/ReservedParams.md +++ b/docs/Rules/ReservedParams.md @@ -1,6 +1,6 @@ --- description: Reserved Parameters -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ReservedParams diff --git a/docs/Rules/ReviewUnusedParameter.md b/docs/Rules/ReviewUnusedParameter.md index 2408c5535..c955760d6 100644 --- a/docs/Rules/ReviewUnusedParameter.md +++ b/docs/Rules/ReviewUnusedParameter.md @@ -1,6 +1,6 @@ --- description: ReviewUnusedParameter -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ReviewUnusedParameter diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index d35ca8d40..6746c3621 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -1,6 +1,6 @@ --- description: Should Process -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ShouldProcess diff --git a/docs/Rules/UseApprovedVerbs.md b/docs/Rules/UseApprovedVerbs.md index 2f51bfdb4..f4d685585 100644 --- a/docs/Rules/UseApprovedVerbs.md +++ b/docs/Rules/UseApprovedVerbs.md @@ -1,6 +1,6 @@ --- description: Cmdlet Verbs -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseApprovedVerbs diff --git a/docs/Rules/UseBOMForUnicodeEncodedFile.md b/docs/Rules/UseBOMForUnicodeEncodedFile.md index 118158773..2d174f99e 100644 --- a/docs/Rules/UseBOMForUnicodeEncodedFile.md +++ b/docs/Rules/UseBOMForUnicodeEncodedFile.md @@ -1,6 +1,6 @@ --- description: Use BOM encoding for non-ASCII files -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseBOMForUnicodeEncodedFile diff --git a/docs/Rules/UseCmdletCorrectly.md b/docs/Rules/UseCmdletCorrectly.md index 205c6a971..39de79c5c 100644 --- a/docs/Rules/UseCmdletCorrectly.md +++ b/docs/Rules/UseCmdletCorrectly.md @@ -1,6 +1,6 @@ --- description: Use Cmdlet Correctly -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCmdletCorrectly diff --git a/docs/Rules/UseCompatibleCmdlets.md b/docs/Rules/UseCompatibleCmdlets.md index 93be36a34..a340a6869 100644 --- a/docs/Rules/UseCompatibleCmdlets.md +++ b/docs/Rules/UseCompatibleCmdlets.md @@ -1,6 +1,6 @@ --- description: Use compatible cmdlets -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCmdlets diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index 1fb47eae7..6e0714f2f 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -1,6 +1,6 @@ --- description: Use compatible commands -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCommands diff --git a/docs/Rules/UseCompatibleSyntax.md b/docs/Rules/UseCompatibleSyntax.md index 71974b62a..ebea10c22 100644 --- a/docs/Rules/UseCompatibleSyntax.md +++ b/docs/Rules/UseCompatibleSyntax.md @@ -1,6 +1,6 @@ --- description: Use compatible syntax -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleSyntax diff --git a/docs/Rules/UseCompatibleTypes.md b/docs/Rules/UseCompatibleTypes.md index 8917ef5eb..72927d140 100644 --- a/docs/Rules/UseCompatibleTypes.md +++ b/docs/Rules/UseCompatibleTypes.md @@ -1,6 +1,6 @@ --- description: Use compatible types -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleTypes diff --git a/docs/Rules/UseConsistentIndentation.md b/docs/Rules/UseConsistentIndentation.md index f9b5d2d81..fa331a74f 100644 --- a/docs/Rules/UseConsistentIndentation.md +++ b/docs/Rules/UseConsistentIndentation.md @@ -1,6 +1,6 @@ --- description: Use consistent indentation -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseConsistentIndentation diff --git a/docs/Rules/UseConsistentWhitespace.md b/docs/Rules/UseConsistentWhitespace.md index 76119293d..1841ed0f5 100644 --- a/docs/Rules/UseConsistentWhitespace.md +++ b/docs/Rules/UseConsistentWhitespace.md @@ -1,6 +1,6 @@ --- description: Use whitespaces -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseConsistentWhitespace diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 7fa3cc5b3..c39c08875 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -1,6 +1,6 @@ --- description: Use exact casing of cmdlet/function/parameter name. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCorrectCasing diff --git a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md index 080fbb671..482105db4 100644 --- a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md +++ b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md @@ -1,6 +1,6 @@ --- description: Extra Variables -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseDeclaredVarsMoreThanAssignments diff --git a/docs/Rules/UseLiteralInitializerForHashtable.md b/docs/Rules/UseLiteralInitializerForHashtable.md index 31462c743..bb180d832 100644 --- a/docs/Rules/UseLiteralInitializerForHashtable.md +++ b/docs/Rules/UseLiteralInitializerForHashtable.md @@ -1,6 +1,6 @@ --- description: Create hashtables with literal initializers -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseLiteralInitializerForHashtable diff --git a/docs/Rules/UseOutputTypeCorrectly.md b/docs/Rules/UseOutputTypeCorrectly.md index 4ce9994da..01895c17d 100644 --- a/docs/Rules/UseOutputTypeCorrectly.md +++ b/docs/Rules/UseOutputTypeCorrectly.md @@ -1,6 +1,6 @@ --- description: Use OutputType Correctly -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseOutputTypeCorrectly diff --git a/docs/Rules/UsePSCredentialType.md b/docs/Rules/UsePSCredentialType.md index 79e1a194b..c3c2dc7f1 100644 --- a/docs/Rules/UsePSCredentialType.md +++ b/docs/Rules/UsePSCredentialType.md @@ -1,6 +1,6 @@ --- description: Use PSCredential type. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UsePSCredentialType diff --git a/docs/Rules/UseProcessBlockForPipelineCommand.md b/docs/Rules/UseProcessBlockForPipelineCommand.md index bcf11a8b4..d74da5a71 100644 --- a/docs/Rules/UseProcessBlockForPipelineCommand.md +++ b/docs/Rules/UseProcessBlockForPipelineCommand.md @@ -1,6 +1,6 @@ --- description: Use process block for command that accepts input from pipeline. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseProcessBlockForPipelineCommand diff --git a/docs/Rules/UseShouldProcessForStateChangingFunctions.md b/docs/Rules/UseShouldProcessForStateChangingFunctions.md index 45017dba8..5c63d150d 100644 --- a/docs/Rules/UseShouldProcessForStateChangingFunctions.md +++ b/docs/Rules/UseShouldProcessForStateChangingFunctions.md @@ -1,6 +1,6 @@ --- description: Use ShouldProcess For State Changing Functions -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseShouldProcessForStateChangingFunctions diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index 387d48c2d..6d1379664 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,6 +1,6 @@ --- description: Cmdlet Singular Noun -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseSingularNouns diff --git a/docs/Rules/UseSupportsShouldProcess.md b/docs/Rules/UseSupportsShouldProcess.md index 9d894b85b..40767e060 100644 --- a/docs/Rules/UseSupportsShouldProcess.md +++ b/docs/Rules/UseSupportsShouldProcess.md @@ -1,6 +1,6 @@ --- description: Use SupportsShouldProcess -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseSupportsShouldProcess diff --git a/docs/Rules/UseToExportFieldsInManifest.md b/docs/Rules/UseToExportFieldsInManifest.md index dcbf890b7..5a4c87c73 100644 --- a/docs/Rules/UseToExportFieldsInManifest.md +++ b/docs/Rules/UseToExportFieldsInManifest.md @@ -1,6 +1,6 @@ --- description: Use the *ToExport module manifest fields. -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseToExportFieldsInManifest diff --git a/docs/Rules/UseUTF8EncodingForHelpFile.md b/docs/Rules/UseUTF8EncodingForHelpFile.md index 56b6f0010..8f7ab429c 100644 --- a/docs/Rules/UseUTF8EncodingForHelpFile.md +++ b/docs/Rules/UseUTF8EncodingForHelpFile.md @@ -1,6 +1,6 @@ --- description: Use UTF8 Encoding For Help File -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseUTF8EncodingForHelpFile diff --git a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md index 88769d80b..86a76561c 100644 --- a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md +++ b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md @@ -1,6 +1,6 @@ --- description: Use 'Using:' scope modifier in RunSpace ScriptBlocks -ms.custom: PSSA v1.21.0 +ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseUsingScopeModifierInNewRunspaces From f15cdbf5fd3e271eb280f60a796fdc6a3224feb6 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 13 Feb 2024 18:16:40 +0000 Subject: [PATCH 029/130] Remove Appveyor badge (#1962) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 97c66848d..c61522134 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://dev.azure.com/powershell/psscriptanalyzer/_apis/build/status/psscriptanalyzer-ci?branchName=master)](https://dev.azure.com/powershell/psscriptanalyzer/_build/latest?definitionId=80&branchName=master) -[![Build status](https://ci.appveyor.com/api/projects/status/h5mot3vqtvxw5d7l/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/psscriptanalyzer/branch/master) [![Join the chat at https://gitter.im/PowerShell/PSScriptAnalyzer](https://badges.gitter.im/PowerShell/PSScriptAnalyzer.svg)](https://gitter.im/PowerShell/PSScriptAnalyzer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Table of Contents From 6cb66c14c6cff69cc88f4a3f45676b79d652338b Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Tue, 13 Feb 2024 18:20:35 +0000 Subject: [PATCH 030/130] Do not hard code common parameters in module help test any more (#1963) --- Tests/Engine/ModuleHelp.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Engine/ModuleHelp.Tests.ps1 b/Tests/Engine/ModuleHelp.Tests.ps1 index 85e93fe3e..ac40dcf94 100644 --- a/Tests/Engine/ModuleHelp.Tests.ps1 +++ b/Tests/Engine/ModuleHelp.Tests.ps1 @@ -175,7 +175,7 @@ Describe 'Cmdlet parameter help' { ) BEGIN { - $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'ProgressAction', 'Verbose', 'WarningAction', 'WarningVariable' + $Common = [System.Management.Automation.PSCmdlet]::CommonParameters $parameters = @() } PROCESS { From c06e0052d14902bb94f5eb082a9553db559b1bfc Mon Sep 17 00:00:00 2001 From: Friedrich Weinmann Date: Tue, 13 Feb 2024 19:23:10 +0100 Subject: [PATCH 031/130] PSReviewUnusedParameter: Add CommandsToTraverse option (#1921) * Added command traversal option Explicitly included Where-Object and ForEach-Object scriptblocks to also be searched for variable use * Command traversal check no longer case sensitive * Extended tests for selective command traversal * Rename setting to CommandsToTraverse * Added docs for new configuration: CommandsToTraverse --- Rules/ReviewUnusedParameter.cs | 91 ++++++++++++++++++++- Tests/Rules/ReviewUnusedParameter.tests.ps1 | 26 +++++- docs/Rules/ReviewUnusedParameter.md | 18 ++++ 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/Rules/ReviewUnusedParameter.cs b/Rules/ReviewUnusedParameter.cs index ffaaa1334..f13584fed 100644 --- a/Rules/ReviewUnusedParameter.cs +++ b/Rules/ReviewUnusedParameter.cs @@ -21,8 +21,60 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif public class ReviewUnusedParameter : IScriptRule { + private readonly string TraverseArgName = "CommandsToTraverse"; + public List TraverseCommands { get; private set; } + + /// + /// Configure the rule. + /// + /// Sets the list of commands to traverse of this rule + /// + private void SetProperties() + { + TraverseCommands = new List() { "Where-Object", "ForEach-Object" }; + + Dictionary ruleArgs = Helper.Instance.GetRuleArguments(GetName()); + if (ruleArgs == null) + { + return; + } + + if (!ruleArgs.TryGetValue(TraverseArgName, out object obj)) + { + return; + } + IEnumerable commands = obj as IEnumerable; + if (commands == null) + { + // try with enumerable objects + var enumerableObjs = obj as IEnumerable; + if (enumerableObjs == null) + { + return; + } + foreach (var x in enumerableObjs) + { + var y = x as string; + if (y == null) + { + return; + } + else + { + TraverseCommands.Add(y); + } + } + } + else + { + TraverseCommands.AddRange(commands); + } + } + public IEnumerable AnalyzeScript(Ast ast, string fileName) { + SetProperties(); + if (ast == null) { throw new ArgumentNullException(Strings.NullAstErrorMessage); @@ -46,10 +98,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) IEnumerable parameterAsts = scriptBlockAst.FindAll(oneAst => oneAst is ParameterAst, false); // list all variables - IDictionary variableCount = scriptBlockAst.FindAll(oneAst => oneAst is VariableExpressionAst, false) - .Select(variableExpressionAst => ((VariableExpressionAst)variableExpressionAst).VariablePath.UserPath) - .GroupBy(variableName => variableName, StringComparer.OrdinalIgnoreCase) - .ToDictionary(variableName => variableName.Key, variableName => variableName.Count(), StringComparer.OrdinalIgnoreCase); + IDictionary variableCount = GetVariableCount(scriptBlockAst); foreach (ParameterAst parameterAst in parameterAsts) { @@ -164,5 +213,39 @@ public string GetSourceName() { return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); } + + /// + /// Returns a dictionary including all variables in the scriptblock and their count + /// + /// The scriptblock ast to scan + /// Previously generated data. New findings are added to any existing dictionary if present + /// a dictionary including all variables in the scriptblock and their count + IDictionary GetVariableCount(ScriptBlockAst ast, Dictionary data = null) + { + Dictionary content = data; + if (null == data) + content = new Dictionary(StringComparer.OrdinalIgnoreCase); + IDictionary result = ast.FindAll(oneAst => oneAst is VariableExpressionAst, false) + .Select(variableExpressionAst => ((VariableExpressionAst)variableExpressionAst).VariablePath.UserPath) + .GroupBy(variableName => variableName, StringComparer.OrdinalIgnoreCase) + .ToDictionary(variableName => variableName.Key, variableName => variableName.Count(), StringComparer.OrdinalIgnoreCase); + + foreach (string key in result.Keys) + { + if (content.ContainsKey(key)) + content[key] = content[key] + result[key]; + else + content[key] = result[key]; + } + + IEnumerable foundScriptBlocks = ast.FindAll(oneAst => oneAst is ScriptBlockExpressionAst, false) + .Where(oneAst => oneAst?.Parent is CommandAst && ((CommandAst)oneAst.Parent).CommandElements[0] is StringConstantExpressionAst && TraverseCommands.Contains(((StringConstantExpressionAst)((CommandAst)oneAst.Parent).CommandElements[0]).Value, StringComparer.OrdinalIgnoreCase)) + .Select(oneAst => ((ScriptBlockExpressionAst)oneAst).ScriptBlock); + foreach (Ast astItem in foundScriptBlocks) + if (astItem != ast) + GetVariableCount((ScriptBlockAst)astItem, content); + + return content; + } } } diff --git a/Tests/Rules/ReviewUnusedParameter.tests.ps1 b/Tests/Rules/ReviewUnusedParameter.tests.ps1 index 0249f9743..59d8b160d 100644 --- a/Tests/Rules/ReviewUnusedParameter.tests.ps1 +++ b/Tests/Rules/ReviewUnusedParameter.tests.ps1 @@ -32,6 +32,12 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 1 } + It "doesn't traverse scriptblock scope for a random command" { + $ScriptDefinition = '{ param ($Param1) 1..3 | Invoke-Parallel { $Param1 }}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + It "violations have correct rule and severity" { $ScriptDefinition = 'function BadFunc1 { param ($Param1, $Param2) $Param1}' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName @@ -81,6 +87,24 @@ Describe "ReviewUnusedParameter" { $ScriptDefinition = 'function foo { param ($Param1, $param2) $param1; $Param2}' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName $Violations.Count | Should -Be 0 + } + + It "does traverse scriptblock scope for Foreach-Object" { + $ScriptDefinition = '{ param ($Param1) 1..3 | ForEach-Object { $Param1 }}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "does traverse scriptblock scope for commands added to the traversal list" { + $ScriptDefinition = '{ param ($Param1) Invoke-PSFProtectedCommand { $Param1 } }' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName -Settings @{ + Rules = @{ + PSReviewUnusedParameter = @{ + CommandsToTraverse = @('Invoke-PSFProtectedCommand') + } + } + } + $Violations.Count | Should -Be 0 } } -} +} \ No newline at end of file diff --git a/docs/Rules/ReviewUnusedParameter.md b/docs/Rules/ReviewUnusedParameter.md index c955760d6..1673bb5a3 100644 --- a/docs/Rules/ReviewUnusedParameter.md +++ b/docs/Rules/ReviewUnusedParameter.md @@ -14,6 +14,24 @@ title: ReviewUnusedParameter This rule identifies parameters declared in a script, scriptblock, or function scope that have not been used in that scope. +## Configuration settings + +|Configuration key|Meaning|Accepted values|Mandatory|Example| +|---|---|---|---|---| +|CommandsToTraverse|By default, this command will not consider child scopes other than scriptblocks provided to Where-Object or ForEach-Object. This setting allows you to add additional commands that accept scriptblocks that this rule should traverse into.|string[]: list of commands whose scriptblock to traverse.|`@('Invoke-PSFProtectedCommand')`| + +```powershell +@{ + Rules = @{ + ReviewUnusedParameter = @{ + CommandsToTraverse = @( + 'Invoke-PSFProtectedCommand' + ) + } + } +} +``` + ## How Consider removing the unused parameter. From c085ee3499b3e2150549180147aa82177cb12581 Mon Sep 17 00:00:00 2001 From: Michael Van Leeuwen Date: Tue, 13 Feb 2024 10:26:59 -0800 Subject: [PATCH 032/130] Add AvoidUsingAllowUnencryptedAuthentication (#1857) * Add AvoidUsingAllowUnencryptedAuthentication rule * Add AvoidUsingAllowUnencryptedAuthentication docs and tests * Update docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md Co-authored-by: Christoph Bergmeister * Fix code review suggestions * Fix md code styling * bump rule count in tests again * Update docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Christoph Bergmeister --- ...voidUsingAllowUnencryptedAuthentication.cs | 117 ++++++++++++++++++ Rules/Strings.resx | 12 ++ Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 2 +- ...ngAllowUnencryptedAuthentication.tests.ps1 | 38 ++++++ ...voidUsingAllowUnencryptedAuthentication.md | 35 ++++++ docs/Rules/README.md | 1 + 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 Rules/AvoidUsingAllowUnencryptedAuthentication.cs create mode 100644 Tests/Rules/AvoidUsingAllowUnencryptedAuthentication.tests.ps1 create mode 100644 docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md diff --git a/Rules/AvoidUsingAllowUnencryptedAuthentication.cs b/Rules/AvoidUsingAllowUnencryptedAuthentication.cs new file mode 100644 index 000000000..955de8113 --- /dev/null +++ b/Rules/AvoidUsingAllowUnencryptedAuthentication.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// AvoidUsingAllowUnencryptedAuthentication: Avoid sending credentials and secrets over unencrypted connections. + /// +#if !CORECLR +[Export(typeof(IScriptRule))] +#endif + public class AvoidUsingAllowUnencryptedAuthentication : AvoidParameterGeneric + { + /// + /// Condition on the cmdlet that must be satisfied for the error to be raised + /// + /// + /// + public override bool CommandCondition(CommandAst CmdAst) + { + return true; + } + + /// + /// Condition on the parameter that must be satisfied for the error to be raised. + /// + /// + /// + /// + public override bool ParameterCondition(CommandAst CmdAst, CommandElementAst CeAst) + { + return CeAst is CommandParameterAst && String.Equals((CeAst as CommandParameterAst).ParameterName, "AllowUnencryptedAuthentication", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Retrieves the error message + /// + /// + /// + /// + public override string GetError(string fileName, CommandAst cmdAst) + { + return String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingAllowUnencryptedAuthenticationError); + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public override string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUsingAllowUnencryptedAuthenticationName); + } + + /// + /// GetCommonName: Retrieves the common name of this rule. + /// + /// The common name of this rule + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingAllowUnencryptedAuthenticationCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingAllowUnencryptedAuthenticationDescription); + } + + /// + /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning or information. + /// + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// DiagnosticSeverity: Retrieves the severity of the rule of type DiagnosticSeverity: error, warning or information. + /// + /// + public override DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// GetSourceName: Retrieves the module/assembly name the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + } +} diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 479ca1f7a..ff75828cf 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1206,4 +1206,16 @@ Replace ! with -not + + Avoid AllowUnencryptedAuthentication Switch + + + Avoid sending credentials and secrets over unencrypted connections. + + + The insecure AllowUsingUnencryptedAuthentication switch was used. This should be avoided except for compatability with legacy systems. + + + AvoidUsingAllowUnencryptedAuthentication + \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 960a2fcd5..93824060a 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -63,7 +63,7 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 69 + $expectedNumRules = 70 if ($PSVersionTable.PSVersion.Major -le 4) { # for PSv3 PSAvoidGlobalAliases is not shipped because diff --git a/Tests/Rules/AvoidUsingAllowUnencryptedAuthentication.tests.ps1 b/Tests/Rules/AvoidUsingAllowUnencryptedAuthentication.tests.ps1 new file mode 100644 index 000000000..ca89b280c --- /dev/null +++ b/Tests/Rules/AvoidUsingAllowUnencryptedAuthentication.tests.ps1 @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $settings = @{ + IncludeRules = @('PSAvoidUsingAllowUnencryptedAuthentication') + Rules = @{ + PSAvoidUsingAllowUnencryptedAuthentication = @{ + Enable = $true + } + } + } +} + +Describe "AvoidUsingAllowUnencryptedAuthentication" { + Context "When there are violations" { + It "detects unencrypted authentication violations" { + (Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-WebRequest foo -AllowUnencryptedAuthentication' -Settings $settings).Count | Should -Be 1 + (Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-RestMethod foo -AllowUnencryptedAuthentication' -Settings $settings).Count | Should -Be 1 + (Invoke-ScriptAnalyzer -ScriptDefinition 'iwr foo -AllowUnencryptedAuthentication' -Settings $settings).Count | Should -Be 1 + } + + It "detects arbitrary cmdlets" { + (Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-CustomWebRequest foo -AllowUnencryptedAuthentication' -Settings $settings).Count | Should -Be 1 + } + + } + + Context "When there are no violations" { + It "does not flag safe usage" { + (Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-WebRequest foo' -Settings $settings).Count | Should -Be 0 + } + + It "does not flag cases with unrelated parameters" { + (Invoke-ScriptAnalyzer -ScriptDefinition 'Invoke-WebRequest foo -Method Get' -Settings $settings).Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md new file mode 100644 index 000000000..c30b69844 --- /dev/null +++ b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md @@ -0,0 +1,35 @@ +--- +description: Avoid sending credentials and secrets over unencrypted connections +ms.custom: PSSA v1.22.0 +ms.date: 11/06/2022 +ms.topic: reference +title: AvoidUsingAllowUnencryptedAuthentication +--- +# AvoidUsingAllowUnencryptedAuthentication + +**Severity Level: Warning** + +## Description + +Avoid using the `AllowUnencryptedAuthentication` switch on `Invoke-WebRequest`, `Invoke-RestMethod`, and other webrequest cmdlets, which sends credentials and secrets over unencrypted connections. +This should be avoided except for compatability with legacy systems. + +For more details, see the documentation warning [here](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/invoke-webrequest#-allowunencryptedauthentication). + +## How + +Avoid using the `AllowUnencryptedAuthentication` switch. + +## Example 1 + +### Wrong + +```powershell +Invoke-WebRequest foo -AllowUnencryptedAuthentication +``` + +### Correct + +```powershell +Invoke-WebRequest foo +``` \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index ccfaa71ea..ded305e89 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -27,6 +27,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidSemicolonsAsLineTerminators](./AvoidSemicolonsAsLineTerminators.md) | Warning | No | | | [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | | | [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | | +| [AvoidUsingAllowUnencryptedAuthentication](./AvoidUsingAllowUnencryptedAuthentication.md) | Warning | Yes | | | [AvoidUsingBrokenHashAlgorithms](./AvoidUsingBrokenHashAlgorithms.md) | Warning | Yes | | | [AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | Yes2 | | [AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | Yes | | From b4365ad7a6057a8766340aa92d3a5665d49e49ed Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 13 Mar 2024 10:06:49 -0700 Subject: [PATCH 033/130] Adding OneBranch pipeline YAML config file for OSS_Microsoft_PSSA-Official (#1981) * Adding OneBranch pipeline YAML config file for OSS_Microsoft_PSSA-Official * Updates to signing and TSA * Fix typo * Fix typo 2 * Fix typo 3 * Update CR feedback * Revert back to old signing * Fix source direcctory for credscan --------- Co-authored-by: OneBranch Resources --- .config/tsaoptions.json | 10 ++ .pipelines/OSS_Microsoft_PSSA-Official.yml | 160 +++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 .config/tsaoptions.json create mode 100644 .pipelines/OSS_Microsoft_PSSA-Official.yml diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json new file mode 100644 index 000000000..75070cbfd --- /dev/null +++ b/.config/tsaoptions.json @@ -0,0 +1,10 @@ +{ + "instanceUrl": "https://msazure.visualstudio.com", + "projectName": "One", + "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core", + "notificationAliases": [ + "jimtru@microsoft.com", + "slee@microsoft.com" + ], + "codebaseName": "PSSA_202403" +} diff --git a/.pipelines/OSS_Microsoft_PSSA-Official.yml b/.pipelines/OSS_Microsoft_PSSA-Official.yml new file mode 100644 index 000000000..7500f4050 --- /dev/null +++ b/.pipelines/OSS_Microsoft_PSSA-Official.yml @@ -0,0 +1,160 @@ +# This Yaml Document has been converted by ESAI Yaml Pipeline Conversion Tool. +# Please make sure to check all the converted content, it is your team's responsibility to make sure that the pipeline is still valid and functions as expected. +# This pipeline will be extended to the OneBranch template +name: PSSA-Release-$(Build.BuildId) +trigger: none +pr: + branches: + include: + - master + - release* +variables: +- name: DOTNET_CLI_TELEMETRY_OPTOUT + value: 1 +- name: POWERSHELL_TELEMETRY_OPTOUT + value: 1 +- name: WindowsContainerImage + value: onebranch.azurecr.io/windows/ltsc2019/vse2022:latest +resources: + repositories: + - repository: ComplianceRepo + type: github + endpoint: ComplianceGHRepo + name: PowerShell/compliance + ref: master + - repository: onebranchTemplates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main +extends: + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates + parameters: + customTags: 'ES365AIMigrationTooling' + globalSdl: + sbom: + enabled: false + codeql: + compiled: + enabled: true + asyncSdl: # https://aka.ms/obpipelines/asyncsdl + enabled: true + forStages: [Build] + credscan: + enabled: true + scanFolder: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA + binskim: + enabled: true + apiscan: + enabled: false + + stages: + - stage: Build + displayName: Build + jobs: + - job: Build_Job + displayName: Build Microsoft.PowerShell.ScriptAnalyzer + variables: + - group: ESRP + - name: ob_outputDirectory + value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' + - name: repoRoot + value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA + - name: ob_sdl_tsa_configFile + value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA\.config\tsaoptions.json + pool: + type: windows + steps: + - checkout: self + - checkout: ComplianceRepo + + - pwsh: | + if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { + Get-ChildItem $(Build.SourcesDirectory) -recurse -ErrorAction SilentlyContinue + throw "tsaoptions.json does not exist under $(Build.SourcesDirectory)/OSS_Microsoft_PSSA/.config" + } + displayName: Test if tsaoptions.json exists + + #- pwsh: | + # New-Item -ItemType Directory -Path $(Build.SourcesDirectory)/.config -Force -Verbose + # Copy-Item '$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/.config/tsaoptions.json' '$(Build.SourcesDirectory)/.config/tsaoptions.json' -Force -Verbose + # displayName: Copy tsaoptions + + - pwsh: | + Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" + try { ./build.ps1 -Configuration Release -All } catch { throw $_ } + displayName: Execute build + + - pwsh: | + $signSrcPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/out" + # Set signing src path variable + $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + $signOutStep1 = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Step1" + $null = New-Item -ItemType Directory -Path $signOutStep1 + # Set signing out path variable + $vstsCommandString = "vso[task.setvariable variable=signOutStep1]${signOutStep1}" + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + $signOutPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/signed" + $null = New-Item -ItemType Directory -Path $signOutPath + # Set signing out path variable + $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + # Set path variable for guardian codesign validation + $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + # Get version and create a variable + $moduleData = Import-PowerShellDataFile "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Engine/PSScriptAnalyzer.psd1" + $moduleVersion = $moduleData.ModuleVersion + $vstsCommandString = "vso[task.setvariable variable=moduleVersion]${moduleVersion}" + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + displayName: Setup variables for signing + + - template: EsrpSign.yml@ComplianceRepo + parameters: + buildOutputPath: $(signSrcPath) + signOutputPath: $(signOutStep1) + certificateId: "CP-230012" + useMinimatch: true + pattern: | + **\*.psd1 + **\*.psm1 + **\*.ps1xml + **\Microsoft*.dll + + - template: EsrpSign.yml@ComplianceRepo + parameters: + buildOutputPath: $(signOutStep1) + signOutputPath: $(signOutPath) + certificateId: "CP-231522" + useMinimatch: true + pattern: | + **/Pluralize*.dll + **/Newtonsoft*.dll + + - template: Sbom.yml@ComplianceRepo + parameters: + BuildDropPath: $(signOutPath) + Build_Repository_Uri: 'https://github.com/powershell/PSScriptAnalyzer' + + - pwsh: | + Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" + ./build -BuildNupkg -CopyManifest -signed + displayName: Create nupkg for publishing + + - task: CopyFiles@2 + displayName: "Copy Files for 'publish build directory' publish task" + inputs: + SourceFolder: "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" + Contents: '**' + TargetFolder: $(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT/build + + - task: CopyFiles@2 + displayName: "Copy Files for 'Publish module nupkg' publish task" + inputs: + Contents: "$(signOutPath)/PSScriptAnalyzer.$(moduleVersion).nupkg" + TargetFolder: $(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT/nupkg From 03b655cc80599229d7b89e7e01e486daa9ced20c Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Wed, 13 Mar 2024 12:58:40 -0500 Subject: [PATCH 034/130] Update format and grammar of AvoidUsingAllowUnencryptedAuthentication (#1974) * Update format and grammar of AvoidUsingAllowUnencryptedAuthentication * Syncing docs changes to all rules * Update rules to match docs and improve wording --- docs/Rules/AvoidExclaimOperator.md | 13 ++++++++----- ...voidUsingAllowUnencryptedAuthentication.md | 13 +++++++------ docs/Rules/AvoidUsingPositionalParameters.md | 2 +- docs/Rules/README.md | 2 +- docs/Rules/ReservedParams.md | 10 ++++++---- .../UseDeclaredVarsMoreThanAssignments.md | 19 +++++++++++++++---- docs/Rules/UseSingularNouns.md | 2 +- 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/Rules/AvoidExclaimOperator.md b/docs/Rules/AvoidExclaimOperator.md index 5ef833fc0..0eb4ff625 100644 --- a/docs/Rules/AvoidExclaimOperator.md +++ b/docs/Rules/AvoidExclaimOperator.md @@ -1,7 +1,7 @@ --- description: Avoid exclaim operator ms.custom: PSSA v1.22.0 -ms.date: 06/14/2023 +ms.date: 02/13/2024 ms.topic: reference title: AvoidExclaimOperator --- @@ -10,20 +10,23 @@ title: AvoidExclaimOperator ## Description -The negation operator `!` should not be used for readability purposes. Use `-not` instead. +Avoid using the negation operator (`!`). Use `-not` for improved readability. -**Note**: This rule is not enabled by default. The user needs to enable it through settings. +> [!NOTE] +> This rule is not enabled by default. The user needs to enable it through settings. ## How to Fix ## Example + ### Wrong: -```PowerShell + +```powershell $MyVar = !$true ``` ### Correct: -```PowerShell +```powershell $MyVar = -not $true ``` diff --git a/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md index c30b69844..e8ba28167 100644 --- a/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md +++ b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md @@ -1,7 +1,7 @@ --- description: Avoid sending credentials and secrets over unencrypted connections ms.custom: PSSA v1.22.0 -ms.date: 11/06/2022 +ms.date: 02/28/2024 ms.topic: reference title: AvoidUsingAllowUnencryptedAuthentication --- @@ -11,14 +11,15 @@ title: AvoidUsingAllowUnencryptedAuthentication ## Description -Avoid using the `AllowUnencryptedAuthentication` switch on `Invoke-WebRequest`, `Invoke-RestMethod`, and other webrequest cmdlets, which sends credentials and secrets over unencrypted connections. -This should be avoided except for compatability with legacy systems. +Avoid using the **AllowUnencryptedAuthentication** parameter of `Invoke-WebRequest` and +`Invoke-RestMethod`. When using this parameter, the cmdlets send credentials and secrets over +unencrypted connections. This should be avoided except for compatibility with legacy systems. -For more details, see the documentation warning [here](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/invoke-webrequest#-allowunencryptedauthentication). +For more details, see [Invoke-RestMethod](xref:Microsoft.PowerShell.Utility.Invoke-RestMethod). ## How -Avoid using the `AllowUnencryptedAuthentication` switch. +Avoid using the **AllowUnencryptedAuthentication** parameter. ## Example 1 @@ -32,4 +33,4 @@ Invoke-WebRequest foo -AllowUnencryptedAuthentication ```powershell Invoke-WebRequest foo -``` \ No newline at end of file +``` diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index 0078ffd70..d968b9f6e 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -1,7 +1,7 @@ --- description: Avoid Using Positional Parameters ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 02/13/2024 ms.topic: reference title: AvoidUsingPositionalParameters --- diff --git a/docs/Rules/README.md b/docs/Rules/README.md index ded305e89..b5761acaf 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,7 +1,7 @@ --- description: List of PSScriptAnalyzer rules ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 02/13/2024 ms.topic: reference title: List of PSScriptAnalyzer rules --- diff --git a/docs/Rules/ReservedParams.md b/docs/Rules/ReservedParams.md index 2d38c1af2..6e5c36a18 100644 --- a/docs/Rules/ReservedParams.md +++ b/docs/Rules/ReservedParams.md @@ -1,7 +1,7 @@ --- description: Reserved Parameters ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 03/06/2024 ms.topic: reference title: ReservedParams --- @@ -11,9 +11,9 @@ title: ReservedParams ## Description -You cannot use [reserved common parameters][01] in an advanced function. - -[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_commonparameters +You can't redefine [common parameters][01] in an advanced function. Using the `CmdletBinding` or +`Parameter` attributes creates an advanced function. The common parameters are are automatically +available in advanced functions, so you can't redefine them. ## How @@ -48,3 +48,5 @@ function Test ) } ``` + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_commonparameters diff --git a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md index 482105db4..0fb7796c2 100644 --- a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md +++ b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md @@ -1,7 +1,7 @@ --- description: Extra Variables ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 03/06/2024 ms.topic: reference title: UseDeclaredVarsMoreThanAssignments --- @@ -44,10 +44,13 @@ function Test } ``` -### Special case +### Special cases -The following example triggers the **PSUseDeclaredVarsMoreThanAssignments** warning because `$bar` -is not used within the scriptblock where it was defined. +The following examples trigger the **PSUseDeclaredVarsMoreThanAssignments** warning. This behavior +is a limitation of the rule. There is no way to avoid these false positive warnings. + +In this case, the warning is triggered because `$bar` is not used within the scriptblock where it +was defined. ```powershell $foo | ForEach-Object { @@ -60,3 +63,11 @@ if($bar){ Write-Host 'Collection contained a false case.' } ``` + +In the next example, the warning is triggered because `$errResult` isn't recognized as being used in +the `Write-Host` command. + +```powershell +$errResult = $null +Write-Host 'Ugh:' -ErrorVariable errResult +``` diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index 6d1379664..e8d91677b 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,7 +1,7 @@ --- description: Cmdlet Singular Noun ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 02/13/2024 ms.topic: reference title: UseSingularNouns --- From 62a14c5d978a32164f388658e40fd32b6a3eb2c0 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Thu, 14 Mar 2024 12:17:01 -0700 Subject: [PATCH 035/130] Move to OneBranch Signing and SBOM generation (#1982) * Updates to signing * fix files to sign * sbom changes * format changes * Make packaging wait on build * clarify task display names * Make sure signing is setup * move other tasks to restore phase * move checkout to restore phase * update container * move signing to it's own job * add flag required for newest container * clarify copy files task displaynames * reuse existing variable * removed unneeded code * Use correct path is signing job * sign the nupkg * fix syntax * Revert "clarify copy files task displaynames" This reverts commit 1bdc12331318fa37bc6041ab9b4d7d222e72ee98. * Disable code sign validation on first job * search for where OBP put dotnet.exe * Move SDK search to it's own job * delete SDK search * make nupkg signing a todo * fix nupkg publishing * try disabling codeql to get signing working per docs * don't setup signing where not needed * combine sign and build job * set version for SBOM * make comments more accurate * Apply suggestions from code review * fix variable name * address pr comments --------- Co-authored-by: Aditya Patwardhan --- .pipelines/OSS_Microsoft_PSSA-Official.yml | 228 +++++++++++++-------- 1 file changed, 143 insertions(+), 85 deletions(-) diff --git a/.pipelines/OSS_Microsoft_PSSA-Official.yml b/.pipelines/OSS_Microsoft_PSSA-Official.yml index 7500f4050..e8e6cc89f 100644 --- a/.pipelines/OSS_Microsoft_PSSA-Official.yml +++ b/.pipelines/OSS_Microsoft_PSSA-Official.yml @@ -9,19 +9,14 @@ pr: - master - release* variables: -- name: DOTNET_CLI_TELEMETRY_OPTOUT - value: 1 -- name: POWERSHELL_TELEMETRY_OPTOUT - value: 1 -- name: WindowsContainerImage - value: onebranch.azurecr.io/windows/ltsc2019/vse2022:latest + - name: DOTNET_CLI_TELEMETRY_OPTOUT + value: 1 + - name: POWERSHELL_TELEMETRY_OPTOUT + value: 1 + - name: WindowsContainerImage + value: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest resources: repositories: - - repository: ComplianceRepo - type: github - endpoint: ComplianceGHRepo - name: PowerShell/compliance - ref: master - repository: onebranchTemplates type: git name: OneBranch.Pipelines/GovernedTemplates @@ -29,10 +24,14 @@ resources: extends: template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: + featureFlags: + WindowsHostVersion: '1ESWindows2022' customTags: 'ES365AIMigrationTooling' globalSdl: + disableLegacyManifest: true sbom: - enabled: false + enabled: true + packageName: Microsoft.PowerShell.ScriptAnalyzer codeql: compiled: enabled: true @@ -48,113 +47,172 @@ extends: enabled: false stages: - - stage: Build - displayName: Build + - stage: stagebuild + displayName: Build and Package Microsoft.PowerShell.ScriptAnalyzer jobs: - - job: Build_Job - displayName: Build Microsoft.PowerShell.ScriptAnalyzer + - job: jobbuild + displayName: Build Microsoft.PowerShell.ScriptAnalyzer Files variables: - - group: ESRP - name: ob_outputDirectory value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' - name: repoRoot value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA - name: ob_sdl_tsa_configFile value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA\.config\tsaoptions.json + - name: signSrcPath + value: $(repoRoot)/out + - name: ob_sdl_sbom_enabled + value: true + - name: ob_signing_setup_enabled + value: true + #CodeQL tasks added manually to workaround signing failures + - name: ob_sdl_codeql_compiled_enabled + value: false + pool: type: windows steps: - checkout: self - - checkout: ComplianceRepo + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - pwsh: | if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { Get-ChildItem $(Build.SourcesDirectory) -recurse -ErrorAction SilentlyContinue - throw "tsaoptions.json does not exist under $(Build.SourcesDirectory)/OSS_Microsoft_PSSA/.config" + throw "tsaoptions.json does not exist under $(repoRoot)/.config" } displayName: Test if tsaoptions.json exists + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + + - task: UseDotNet@2 + displayName: 'Install .NET dependencies' + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + inputs: + packageType: 'sdk' + useGlobalJson: true + # this is to ensure that we are installing the dotnet at the same location as container by default install the dotnet sdks + installationPath: 'C:\Program Files\dotnet\' + workingDirectory: $(repoRoot) - #- pwsh: | - # New-Item -ItemType Directory -Path $(Build.SourcesDirectory)/.config -Force -Verbose - # Copy-Item '$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/.config/tsaoptions.json' '$(Build.SourcesDirectory)/.config/tsaoptions.json' -Force -Verbose - # displayName: Copy tsaoptions + - task: CodeQL3000Init@0 # Add CodeQL Init task right before your 'Build' step. + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + inputs: + Enabled: true + AnalyzeInPipeline: true + Language: csharp + # this is installing .NET - pwsh: | - Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" + Set-Location "$(repoRoot)" try { ./build.ps1 -Configuration Release -All } catch { throw $_ } displayName: Execute build + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + + - task: CodeQL3000Finalize@0 # Add CodeQL Finalize task right after your 'Build' step. + condition: always() + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + + - task: onebranch.pipeline.signing@1 + displayName: Sign 1st party files + inputs: + command: 'sign' + signing_profile: external_distribution + files_to_sign: '**\*.psd1;**\*.psm1;**\*.ps1xml;**\Microsoft*.dll' + search_root: $(signSrcPath) + + - task: onebranch.pipeline.signing@1 + displayName: Sign 3rd Party files + inputs: + command: 'sign' + signing_profile: 135020002 + files_to_sign: '**/Pluralize*.dll;**/Newtonsoft*.dll' + search_root: $(signSrcPath) + + - task: CopyFiles@2 + displayName: "Copy signed files to ob_outputDirectory - '$(ob_outputDirectory)'" + inputs: + SourceFolder: "$(signSrcPath)" + Contents: '**' + TargetFolder: $(ob_outputDirectory) - pwsh: | - $signSrcPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/out" - # Set signing src path variable - $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - $signOutStep1 = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Step1" - $null = New-Item -ItemType Directory -Path $signOutStep1 - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutStep1]${signOutStep1}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - $signOutPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/signed" - $null = New-Item -ItemType Directory -Path $signOutPath - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - # Set path variable for guardian codesign validation - $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - # Get version and create a variable - $moduleData = Import-PowerShellDataFile "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Engine/PSScriptAnalyzer.psd1" + $moduleData = Import-PowerShellDataFile "$(repoRoot)/Engine/PSScriptAnalyzer.psd1" $moduleVersion = $moduleData.ModuleVersion - $vstsCommandString = "vso[task.setvariable variable=moduleVersion]${moduleVersion}" + $vstsCommandString = "vso[task.setvariable variable=ob_sdl_sbom_packageversion]${moduleVersion}" + Write-Host "sending $vstsCommandString" Write-Host "##$vstsCommandString" - displayName: Setup variables for signing - - - template: EsrpSign.yml@ComplianceRepo - parameters: - buildOutputPath: $(signSrcPath) - signOutputPath: $(signOutStep1) - certificateId: "CP-230012" - useMinimatch: true - pattern: | - **\*.psd1 - **\*.psm1 - **\*.ps1xml - **\Microsoft*.dll - - - template: EsrpSign.yml@ComplianceRepo - parameters: - buildOutputPath: $(signOutStep1) - signOutputPath: $(signOutPath) - certificateId: "CP-231522" - useMinimatch: true - pattern: | - **/Pluralize*.dll - **/Newtonsoft*.dll - - - template: Sbom.yml@ComplianceRepo - parameters: - BuildDropPath: $(signOutPath) - Build_Repository_Uri: 'https://github.com/powershell/PSScriptAnalyzer' + displayName: Setup SBOM Package Version + + - job: nupkg + dependsOn: jobbuild + displayName: Package Microsoft.PowerShell.ScriptAnalyzer + variables: + - name: ob_outputDirectory + value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' + - name: repoRoot + value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA + - name: ob_sdl_tsa_configFile + value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA\.config\tsaoptions.json + # Disable because SBOM was already built in the previous job + - name: ob_sdl_sbom_enabled + value: false + - name: signOutPath + value: $(repoRoot)/signed + - name: ob_signing_setup_enabled + value: true + # This job is not compiling code, so disable codeQL + - name: ob_sdl_codeql_compiled_enabled + value: false + + pool: + type: windows + steps: + - checkout: self + + - pwsh: | + if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { + Get-ChildItem $(Build.SourcesDirectory) -recurse -ErrorAction SilentlyContinue + throw "tsaoptions.json does not exist under $(repoRoot)/.config" + } + displayName: Test if tsaoptions.json exists + + - task: DownloadPipelineArtifact@2 + displayName: 'Download build files' + inputs: + targetPath: $(signOutPath) + artifact: drop_stagebuild_jobbuild - pwsh: | - Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" + Set-Location "$(signOutPath)" + dir -dir -recurse * + displayName: Capture artifacts structure + + - pwsh: | + Set-Location "$(repoRoot)" ./build -BuildNupkg -CopyManifest -signed displayName: Create nupkg for publishing - - task: CopyFiles@2 - displayName: "Copy Files for 'publish build directory' publish task" + - task: onebranch.pipeline.signing@1 + displayName: Sign nupkg inputs: - SourceFolder: "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" - Contents: '**' - TargetFolder: $(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT/build + command: 'sign' + signing_profile: external_distribution + files_to_sign: '**\*.nupkg' + search_root: $(signOutPath) + + - pwsh: | + Set-Location "$(repoRoot)" + dir -file -recurse *.nupkg + displayName: Find Nupkg - task: CopyFiles@2 - displayName: "Copy Files for 'Publish module nupkg' publish task" + displayName: "Copy nupkg to ob_outputDirectory - '$(ob_outputDirectory)'" inputs: - Contents: "$(signOutPath)/PSScriptAnalyzer.$(moduleVersion).nupkg" - TargetFolder: $(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT/nupkg + Contents: "$(repoRoot)/**/PSScriptAnalyzer.*.nupkg" + TargetFolder: $(ob_outputDirectory) From a0365a56068f711ee1ec08fae11a85fcc1542f8c Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Tue, 26 Mar 2024 11:19:54 -0500 Subject: [PATCH 036/130] Sync rule docs changes (#1985) --- docs/Rules/AvoidExclaimOperator.md | 12 ++++---- docs/Rules/ProvideCommentHelp.md | 45 ++++++++++++++++------------- docs/Rules/ReviewUnusedParameter.md | 10 +++---- docs/Rules/ShouldProcess.md | 10 +++++-- docs/Rules/UseApprovedVerbs.md | 12 ++++---- docs/Rules/UseCompatibleCommands.md | 2 +- docs/Rules/UseSingularNouns.md | 16 +++++----- 7 files changed, 60 insertions(+), 47 deletions(-) diff --git a/docs/Rules/AvoidExclaimOperator.md b/docs/Rules/AvoidExclaimOperator.md index 0eb4ff625..bafbc9d0e 100644 --- a/docs/Rules/AvoidExclaimOperator.md +++ b/docs/Rules/AvoidExclaimOperator.md @@ -1,11 +1,12 @@ --- description: Avoid exclaim operator ms.custom: PSSA v1.22.0 -ms.date: 02/13/2024 +ms.date: 03/26/2024 ms.topic: reference title: AvoidExclaimOperator --- # AvoidExclaimOperator + **Severity Level: Warning** ## Description @@ -19,13 +20,14 @@ Avoid using the negation operator (`!`). Use `-not` for improved readability. ## Example -### Wrong: +### Wrong ```powershell $MyVar = !$true ``` -### Correct: +### Correct + ```powershell $MyVar = -not $true ``` @@ -42,6 +44,6 @@ Rules = @{ ### Parameters -#### Enable: bool (Default value is `$false`) +- `Enable`: **bool** (Default value is `$false`) -Enable or disable the rule during ScriptAnalyzer invocation. \ No newline at end of file + Enable or disable the rule during ScriptAnalyzer invocation. diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index c6e373f36..51ef66df9 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -17,9 +17,9 @@ presence of comment based help and not on the validity or format. For assistance on comment based help, use the command `Get-Help about_comment_based_help` or the following articles: -- [Writing Comment-based Help](https://learn.microsoft.com/powershell/scripting/developer/help/writing-comment-based-help-topics) -- [Writing Help for PowerShell Cmdlets](https://learn.microsoft.com/powershell/scripting/developer/help/writing-help-for-windows-powershell-cmdlets) -- [Create XML-based help using PlatyPS](https://learn.microsoft.com/powershell/utility-modules/platyps/create-help-using-platyps) +- [Writing Comment-based Help][01] +- [Writing Help for PowerShell Cmdlets][02] +- [Create XML-based help using PlatyPS][03] ## Configuration @@ -37,34 +37,35 @@ Rules = @{ ### Parameters -#### Enable: bool (Default valus is `$true`) +- `Enable`: **bool** (Default valus is `$true`) -Enable or disable the rule during ScriptAnalyzer invocation. + Enable or disable the rule during ScriptAnalyzer invocation. -#### ExportedOnly: bool (Default value is `$true`) +- `ExportedOnly`: **bool** (Default value is `$true`) -If enabled, throw violation only on functions/cmdlets that are exported using the -`Export-ModuleMember` cmdlet. + If enabled, throw violation only on functions/cmdlets that are exported using the + `Export-ModuleMember` cmdlet. -#### BlockComment: bool (Default value is `$true`) +- `BlockComment`: **bool** (Default value is `$true`) -If enabled, returns comment help in block comment style, i.e., `<#...#>`. Otherwise returns comment -help in line comment style, i.e., each comment line starts with `#`. + If enabled, returns comment help in block comment style (`<#...#>`). Otherwise returns + comment help in line comment style where each comment line starts with `#`. -#### VSCodeSnippetCorrection: bool (Default value is `$false`) +- `VSCodeSnippetCorrection`: **bool** (Default value is `$false`) -If enabled, returns comment help in vscode snippet format. + If enabled, returns comment help in vscode snippet format. -#### Placement: string (Default value is `before`) +- `Placement`: **string** (Default value is `before`) -Represents the position of comment help with respect to the function definition. + Represents the position of comment help with respect to the function definition. -Possible values are: `before`, `begin` and `end`. If any invalid value is given, the property -defaults to `before`. + Possible values are: -`before` means the help is placed before the function definition. `begin` means the help is placed -at the beginning of the function definition body. `end` means the help is places the end of the -function definition body. + - `before`: means the help is placed before the function definition + - `begin` means the help is placed at the beginning of the function definition body + - `end` means the help is places the end of the function definition body + + If any invalid value is given, the property defaults to `before`. ## Example @@ -118,3 +119,7 @@ function Get-File } ``` + +[01]: https://learn.microsoft.com/powershell/scripting/developer/help/writing-comment-based-help-topics +[02]: https://learn.microsoft.com/powershell/scripting/developer/help/writing-help-for-windows-powershell-cmdlets +[03]: https://learn.microsoft.com/powershell/utility-modules/platyps/create-help-using-platyps diff --git a/docs/Rules/ReviewUnusedParameter.md b/docs/Rules/ReviewUnusedParameter.md index 1673bb5a3..6a4447785 100644 --- a/docs/Rules/ReviewUnusedParameter.md +++ b/docs/Rules/ReviewUnusedParameter.md @@ -1,7 +1,7 @@ --- description: ReviewUnusedParameter ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 03/26/2024 ms.topic: reference title: ReviewUnusedParameter --- @@ -16,14 +16,14 @@ been used in that scope. ## Configuration settings -|Configuration key|Meaning|Accepted values|Mandatory|Example| -|---|---|---|---|---| -|CommandsToTraverse|By default, this command will not consider child scopes other than scriptblocks provided to Where-Object or ForEach-Object. This setting allows you to add additional commands that accept scriptblocks that this rule should traverse into.|string[]: list of commands whose scriptblock to traverse.|`@('Invoke-PSFProtectedCommand')`| +By default, this rule doesn't consider child scopes other than scriptblocks provided to +`Where-Object` or `ForEach-Object`. The `CommandsToTraverse` setting is an string array allows you +to add additional commands that accept scriptblocks that this rule should examine. ```powershell @{ Rules = @{ - ReviewUnusedParameter = @{ + PSReviewUnusedParameter = @{ CommandsToTraverse = @( 'Invoke-PSFProtectedCommand' ) diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index 6746c3621..38da8320e 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -18,9 +18,9 @@ but makes no calls to `ShouldProcess` or it calls `ShouldProcess` but does not d For more information, see the following articles: -- [about_Functions_Advanced_Methods](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_advanced_methods) -- [about_Functions_CmdletBindingAttribute](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_Functions_CmdletBindingAttribute) -- [Everything you wanted to know about ShouldProcess](https://learn.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-shouldprocess) +- [about_Functions_Advanced_Methods][01] +- [about_Functions_CmdletBindingAttribute][02] +- [Everything you wanted to know about ShouldProcess][03] ## How @@ -73,3 +73,7 @@ function Set-File } } ``` + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_advanced_methods +[02]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_Functions_CmdletBindingAttribute +[03]: https://learn.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-shouldprocess diff --git a/docs/Rules/UseApprovedVerbs.md b/docs/Rules/UseApprovedVerbs.md index f4d685585..858979574 100644 --- a/docs/Rules/UseApprovedVerbs.md +++ b/docs/Rules/UseApprovedVerbs.md @@ -1,7 +1,7 @@ --- description: Cmdlet Verbs ms.custom: PSSA v1.22.0 -ms.date: 06/28/2023 +ms.date: 03/26/2024 ms.topic: reference title: UseApprovedVerbs --- @@ -15,10 +15,9 @@ All cmdlets must used approved verbs. Approved verbs can be found by running the command `Get-Verb`. -Additional documentation on approved verbs can be found at -[Approved Verbs for PowerShell Commands](https://learn.microsoft.com/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands). -Some unapproved verbs are documented on the approved verbs page and point to approved alternatives. -Try searching for the verb you used to find its approved form. For example, searching for `Read`, +For a more information about approved verbs, see [Approved Verbs for PowerShell Commands][01]. Some +unapproved verbs are documented on the approved verbs page and point to approved alternatives. Try +searching for the verb you used to find its approved form. For example, searching for `Read`, `Open`, or `Search` leads you to `Get`. ## How @@ -44,3 +43,6 @@ function Update-Item ... } ``` + + +[01]: /powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands \ No newline at end of file diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index 6e0714f2f..dc8c06b97 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -102,7 +102,7 @@ An example configuration might look like: ```powershell @{ Rules = @{ - PSUseCompatibleCommmands = @{ + PSUseCompatibleCommands = @{ Enable = $true TargetProfiles = @( 'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core' diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index e8d91677b..cde3b7028 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,7 +1,7 @@ --- description: Cmdlet Singular Noun ms.custom: PSSA v1.22.0 -ms.date: 02/13/2024 +ms.date: 03/26/2024 ms.topic: reference title: UseSingularNouns --- @@ -11,9 +11,8 @@ title: UseSingularNouns ## Description -PowerShell team best practices state cmdlets should use singular nouns and not plurals. - -Suppression allows to suppress just specific function names, for example +PowerShell team best practices state cmdlets should use singular nouns and not plurals. Suppression +allows you to suppress the rule for specific function names. For example: ``` function Get-Elements { @@ -35,13 +34,14 @@ Rules = @{ ### Parameters -#### `UseSingularNouns: string[]` (Default value is `{'Data', 'Windows'}`) +- `UseSingularNouns`: `string[]` (Default value is `{'Data', 'Windows'}`) -Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are excluded by default + Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are + excluded by default. -#### Enable: `bool` (Default value is `$true`) +- `Enable`: `bool` (Default value is `$true`) -Enable or disable the rule during ScriptAnalyzer invocation. + Enable or disable the rule during ScriptAnalyzer invocation. ## How From a754b950467aa9e78a1eba1a3423bbd055ed8772 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Wed, 27 Mar 2024 11:12:57 -0500 Subject: [PATCH 037/130] Sync docs changes from MicrosoftDocs/PowerShell-Docs-Modules#213 (#1987) --- docs/Rules/README.md | 6 +++--- docs/Rules/UseSingularNouns.md | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/Rules/README.md b/docs/Rules/README.md index b5761acaf..4f0d24e68 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,7 +1,7 @@ --- description: List of PSScriptAnalyzer rules ms.custom: PSSA v1.22.0 -ms.date: 02/13/2024 +ms.date: 03/27/2024 ms.topic: reference title: List of PSScriptAnalyzer rules --- @@ -58,7 +58,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [ProvideCommentHelp](./ProvideCommentHelp.md) | Information | Yes | Yes | | [ReservedCmdletChar](./ReservedCmdletChar.md) | Error | Yes | | | [ReservedParams](./ReservedParams.md) | Error | Yes | | -| [ReviewUnusedParameter](./ReviewUnusedParameter.md) | Warning | Yes | | +| [ReviewUnusedParameter](./ReviewUnusedParameter.md) | Warning | Yes | Yes2 | | [ShouldProcess](./ShouldProcess.md) | Warning | Yes | | | [UseApprovedVerbs](./UseApprovedVerbs.md) | Warning | Yes | | | [UseBOMForUnicodeEncodedFile](./UseBOMForUnicodeEncodedFile.md) | Warning | Yes | | @@ -84,5 +84,5 @@ The PSScriptAnalyzer contains the following rule definitions. - 1 Rule is not available on all PowerShell versions, editions, or OS platforms. See the rule's documentation for details. -- 2 The rule a configurable property, but the rule can't be disabled like other +- 2 The rule has a configurable property, but the rule can't be disabled like other configurable rules. diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index cde3b7028..b7bba39eb 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,7 +1,7 @@ --- description: Cmdlet Singular Noun ms.custom: PSSA v1.22.0 -ms.date: 03/26/2024 +ms.date: 03/27/2024 ms.topic: reference title: UseSingularNouns --- @@ -25,24 +25,24 @@ function Get-Elements { ```powershell Rules = @{ - UseSingularNouns = @{ - NounAllowList = 'Data', 'Windows', 'Foos' + PSUseSingularNouns = @{ Enable = $true + NounAllowList = 'Data', 'Windows', 'Foos' } } ``` ### Parameters -- `UseSingularNouns`: `string[]` (Default value is `{'Data', 'Windows'}`) - - Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are - excluded by default. - - `Enable`: `bool` (Default value is `$true`) Enable or disable the rule during ScriptAnalyzer invocation. +- `NounAllowList`: `string[]` (Default value is `{'Data', 'Windows'}`) + + Commands to be excluded from this rule. `Data` and `Windows` are common false positives and are + excluded by default. + ## How Change plurals to singular. From e1dc126c361398d6ead6556c24c544702b1918cb Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Thu, 25 Apr 2024 06:42:33 -0500 Subject: [PATCH 038/130] Update CHANGELOG for 1.22.0 release (#1990) --- CHANGELOG.MD | 99 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 19ff33a1f..6afc5be8e 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,11 +1,95 @@ # CHANGELOG +## [1.22.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.22.0) - 2024-03-05 + +Minimum required version when using PowerShell 7 is now `7.2.11`. + +## New Rule + +- Add AvoidUsingAllowUnencryptedAuthentication by @MJVL in (#1857) +- Add the AvoidExclaimOperator rule to warn about the use of the ! negation operator. Fixes (#1826) by + @liamjpeters in (#1922) + +## Enhancements + +- Enable suppression of PSAvoidAssignmentToAutomaticVariable for specific variable or parameter by + @fflaten in (#1896) +- Upgrade to use .NET 6 since PowerShell 7.0 is now out out of support by @bergmeister in (#1873) +- Convert UseSingularNouns to configurable rule and add Windows to allowlist by @MJVL in (#1858) +- Add ErrorView to SpecialVars.cs by @ewisniew0 in (#1865) +- Allow suppression of PSUseSingularNouns for specific function by @fflaten in (#1903) +- Adding ToString() methods to [CorrectionExtent] and [DiagnosticRecord] by @StartAutomating in (#1946) +- Add PSNativeCommandUseErrorActionPreference preference variable by @aelij in (#1954) +- AvoidUsingPositionalParameter: Check if command has parameters to avoid having az in default + CommandAllowList by @bergmeister in (#1850) +- PSReviewUnusedParameter: Add CommandsToTraverse option by @FriedrichWeinmann in (#1921) + +## Fixes + +- Prevent NullReferenceException for null analysis type. by @hubuk in (#1949) + +## Build & Test, Documentation and Maintenance + +- UseApprovedVerbs.md: Backport minor change of PR 104 in PowerShell-Docs-Modules by @bergmeister in + (#1849) +- Improve Pester bootstrap logic for CI by @bergmeister in (#1853) +- Bump .NET SDK from 3.1.419 to 3.1.424 by @bergmeister in (#1852) +- AvoidLongLines: Make internal function DiagnosticSeverity private by @bergmeister in (#1851) +- SupportsShouldProcess.md: Fix Typo - MicrosoftDocs backport of PR 121 there by @sdwheeler in (#1869) +- Minor test fix for UseCorrectCasing rule by @kilasuit in (#1885) +- Make Invoke-Formatter test case assertion fail in case of incorrect casing by @alexandear in (#1888) +- Fix `AvoidUsingDoubleQuotesForConstantString` information in overview README by @michaeltlombardi + in (#1883) +- Update dependabot reviewers to remove Rob by @fflaten in (#1897) +- Fix typo in AvoidUsingPlainTextForPassword error message: 'to' being repeated two times by + @ALiwoto in (#1902) +- CI: Use new Ubuntu 22.04 image and remove deprecated Ubuntu 18.04 by @bergmeister in (#1847) +- Change double quotes to single where possible by @sdwheeler in (#1911) +- Backport MicrosoftDocs PR 143 by @sdwheeler in (#1910) +- Fix typos in rules documentation by @sdwheeler in (#1913) +- add demand for compliance job by @TravisEz13 in (#1920) +- FabricBot: Onboarding to GitOps.ResourceManagement because of FabricBot decommissioning by + @microsoft-github-policy-service in (#1925) +- Sync changes from Docs repository by @sdwheeler in (#1929) +- Developer documentation fix and message fix of + PossibleIncorrectUsageOfRedirectionOperatorDescription by @JoelTipke in (#1928) +- Documentation corrections for AvoidUsingPositionalParameters by @ImportTaste in (#1917) +- Update minimum PowerShell Core version to 7.2.11 as 7.0 is now EOL by @bergmeister in (#1872) +- Remove dead code and simplify by @bergmeister in (#1856) +- PSReservedParams - link about_CommonParameters by @petervandivier in (#1908) +- Generate strongly typed resources as part of build by @bergmeister in (#1855) +- Bump Newtonsoft.Json to 13.0.3 by @dependabot in (#1866) +- Use latest .NET 6.0 SDK patch version and update devcontainer to use .NET 6 as well by + @bergmeister in (#1955) +- Bump Microsoft.Management.Infrastructure from 1.0.0 to 3.0.0 for PowerShell 7 only by @dependabot + in (#1947) +- Bump version from 1.21.0 to 1.22.0 by @bergmeister in (#1965) +- Remove Appveyor badge from main README by @bergmeister in (#1962) +- Do not hard code common parameters in module help test any more by @bergmeister in (#1963) + +## New Contributors + +- @fflaten made their first contribution in (#1897) +- @ALiwoto made their first contribution in (#1902) +- @microsoft-github-policy-service made their first contribution in (#1925) +- @JoelTipke made their first contribution in (#1928) +- @ImportTaste made their first contribution in (#1917) +- @liamjpeters made their first contribution in (#1922) +- @petervandivier made their first contribution in (#1908) +- @ewisniew0 made their first contribution in (#1865) +- @StartAutomating made their first contribution in (#1946) +- @aelij made their first contribution in (#1954) +- @FriedrichWeinmann made their first contribution in (#1921) + +**Full Changelog**: + ## [1.21.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.21.0) - 2022-09-27 ### New Rule - Add AvoidMultipleTypeAttributes rule (#1705) (thanks @hankyi95) -- Add the AvoidSemicolonsAsLineTerminators rule to warn about lines ending with a semicolon. Fix (#824) (#1806) (thanks @tempora-mutantur) +- Add the AvoidSemicolonsAsLineTerminators rule to warn about lines ending with a semicolon. Fix + (#824) (#1806) (thanks @tempora-mutantur) - Add AvoidUsingBrokenHashAlgorithms (#1787) (thanks @MJVL) ### Enhancements @@ -13,7 +97,8 @@ - Also return suggestion to use PSCredential for AvoidUsingPlainTextForPassword rule (#1782) (by @bergmeister) - Invoke-Formatter: Accept input from pipeline (#1763) (by @bergmeister) - Make messages of UseCorrectCasing more detailed (#1843) -- Exclude automatic variable FormatEnumerationLimit from analysis by PSAvoidGlobalVars and PSUseDeclaredVarsMoreThanAssignments (#1836) (by @bergmeister) +- Exclude automatic variable FormatEnumerationLimit from analysis by PSAvoidGlobalVars and + PSUseDeclaredVarsMoreThanAssignments (#1836) (by @bergmeister) - PSAvoidUsingPositionalParameters: Do not warn on AZ CLI (#1846) (by @bergmeister) ### Fixes @@ -59,7 +144,7 @@ - Remove Ubuntu 16.04 from test matrix (#1733) (by @rjmholt) - Use PowerShell1ES pool for official build (#1719) (by @JamesWTruher) - Update cmdlet docs for 1.20.0 (#1726) (by @sdwheeler) -- Fixes #1720 - move rule docs and update tests (#1724) (by @sdwheeler) +- Fixes (#1720) - move rule docs and update tests (#1724) (by @sdwheeler) - Update rule docs (#1711) (by @sdwheeler) ## [1.20.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.20.0) - 2021-08-20 @@ -170,7 +255,7 @@ ### Compatibility Rules -- Make CompatibilityCollector able to parse a single version String #1446 (by @bergmeister) +- Make CompatibilityCollector able to parse a single version String (#1446) (by @bergmeister) - Update compatibility profiles for PowerShell 7 (#1429) (by @rjmholt) - Ps7 syntax (#1426) (by @rjmholt) - Fix ps3 syntax check (#1395) (by @rjmholt) @@ -728,10 +813,10 @@ Here are some improvements since the last release. - Add build script to automate building and testing the solution A big **Thank You!** to the following folks for making PSScriptAnalyzer even better: -- [Kieran Jacobsen (@kjacobsen)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=kjacobsen): Fix rule documentation of `PSDSCExamplesPresent` [PR #591](https://github.com/PowerShell/PSScriptAnalyzer/pull/591) -- [Charlie Schmidt (@charlieschmidt)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=charlieschmidt): Suppress External Rules [PR #585](https://github.com/PowerShell/PSScriptAnalyzer/pull/585) +- [Kieran Jacobsen (@kjacobsen)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=kjacobsen): Fix rule documentation of `PSDSCExamplesPresent` [PR #591](#591) +- [Charlie Schmidt (@charlieschmidt)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=charlieschmidt): Suppress External Rules [PR #585](#585) - [June Blender (@juneb)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=juneb): Add tests for module help [058f65e1](https://github.com/PowerShell/PSScriptAnalyzer/commit/058f65e1f6278222378fedf444eecb2e32865b1e) -- [Shayde Nofziger (@Blackbaud-ShaydeNofziger)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=Blackbaud-ShaydeNofziger): Fix rule name typo and comment [PR #560](https://github.com/PowerShell/PSScriptAnalyzer/pull/560) +- [Shayde Nofziger (@Blackbaud-ShaydeNofziger)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=Blackbaud-ShaydeNofziger): Fix rule name typo and comment [PR #560](#560) ## [1.6.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.6.0) - 2016-06-07 From ae898e7662eae1d40611ffae959f680fb5793b9e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:12:34 -0700 Subject: [PATCH 039/130] Update Code of Conduct and Security Policy (#2002) Per OSPO recommendation. --- CODE_OF_CONDUCT.md | 10 ++++++++++ README.md | 13 ++++++++----- SECURITY.md | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 SECURITY.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..686e5e7a0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,10 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns +- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) diff --git a/README.md b/README.md index c61522134..716224c7c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - [Contributions are welcome](#contributions-are-welcome) - [Creating a Release](#creating-a-release) - [Code of Conduct](#code-of-conduct) +- [Security Policy](#security-policy) @@ -227,10 +228,12 @@ New-Release ## Code of Conduct -This project has adopted the -[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more -information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or -comments. +Please see our [Code of Conduct](CODE_OF_CONDUCT.md) before participating in this project. + +[Back to ToC](#table-of-contents) + +## Security Policy + +For any security issues, please see our [Security Policy](SECURITY.md). [Back to ToC](#table-of-contents) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..f941d308b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin) and [PowerShell](https://github.com/PowerShell). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + From b5fec0a5ef642ba07ff31025578725ae0ecb5daf Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 16 Jul 2024 21:06:18 +0100 Subject: [PATCH 040/130] Update default type definition of [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.RuleInfo] (#2011) --- Engine/ScriptAnalyzer.types.ps1xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/ScriptAnalyzer.types.ps1xml b/Engine/ScriptAnalyzer.types.ps1xml index da989657e..03bf3937e 100644 --- a/Engine/ScriptAnalyzer.types.ps1xml +++ b/Engine/ScriptAnalyzer.types.ps1xml @@ -79,7 +79,7 @@ DefaultDisplayPropertySet - Name + RuleName Severity Description SourceName From d1a1bcb16adc51aae5aba911758a4b2fe0d57a87 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 16 Jul 2024 21:16:41 +0100 Subject: [PATCH 041/130] PSUseConsistentWhitespace: Handle redirect operators which are not in stream order (#2001) --- Rules/UseConsistentWhitespace.cs | 11 ++++++++++- Tests/Rules/UseConsistentWhitespace.tests.ps1 | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Rules/UseConsistentWhitespace.cs b/Rules/UseConsistentWhitespace.cs index 13613ae74..a062e5d3f 100644 --- a/Rules/UseConsistentWhitespace.cs +++ b/Rules/UseConsistentWhitespace.cs @@ -396,8 +396,17 @@ private IEnumerable FindParameterViolations(Ast ast) testAst => testAst is CommandAst, true); foreach (CommandAst commandAst in commandAsts) { + /// When finding all the command parameter elements, there is no guarantee that + /// we will read them from the AST in the order they appear in the script (in token + /// order). So we first sort the tokens by their starting line number, followed by + /// their starting column number. List commandParameterAstElements = commandAst.FindAll( - testAst => testAst.Parent == commandAst, searchNestedScriptBlocks: false).ToList(); + testAst => testAst.Parent == commandAst, searchNestedScriptBlocks: false + ).OrderBy( + e => e.Extent.StartLineNumber + ).ThenBy( + e => e.Extent.StartColumnNumber + ).ToList(); for (int i = 0; i < commandParameterAstElements.Count - 1; i++) { IScriptExtent leftExtent = commandParameterAstElements[i].Extent; diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1 index 31442ad77..30d8cce57 100644 --- a/Tests/Rules/UseConsistentWhitespace.tests.ps1 +++ b/Tests/Rules/UseConsistentWhitespace.tests.ps1 @@ -540,6 +540,12 @@ bar -h i ` Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -Be $null } + It "Should not find a violation when redirect operators, spearated by 1 space, are used and not in stream order" { + # Related to Issue #2000 + $def = 'foo 3>&1 1>$null 2>&1' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -Be $null + } + It "Should find 1 violation if there is 1 space too much before a parameter" { $def = 'foo -bar' $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings @@ -578,5 +584,13 @@ bar -h i ` Invoke-Formatter -ScriptDefinition "$def" -Settings $settings | Should -Be "$expected" } + + It "Should fix script when redirects are involved and whitespace is not consistent" { + # Related to Issue #2000 + $def = 'foo 3>&1 1>$null 2>&1' + $expected = 'foo 3>&1 1>$null 2>&1' + Invoke-Formatter -ScriptDefinition $def -Settings $settings | + Should -Be $expected + } } } From e36d82c20b0ed7737e49b53dea540a89cab137cb Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:44:46 -0700 Subject: [PATCH 042/130] Setup GitHub Actions CI (#2018) And CFS. --- .github/workflows/ci-test.yml | 53 +++++++++++++++++++++++++++++++++++ NuGet.Config | 4 +-- build.psm1 | 3 +- tools/installPSResources.ps1 | 13 +++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci-test.yml create mode 100644 tools/installPSResources.ps1 diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 000000000..e3252aae0 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,53 @@ +name: CI Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + ci: + name: pester + strategy: + matrix: + os: [ windows-latest, macos-latest, ubuntu-latest ] + runs-on: ${{ matrix.os }} + env: + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dotnet + uses: actions/setup-dotnet@v4 + with: + cache: true + cache-dependency-path: '**/*.csproj' + + - name: Install PSResources + run: ./tools/installPSResources.ps1 + shell: pwsh + + - name: Build + run: ./build.ps1 -Configuration Release -All + shell: pwsh + + - name: Test + run: ./build.ps1 -Test + shell: pwsh + + - name: Test Windows PowerShell + run: | + Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck + ./build.ps1 -Test + if: matrix.os == 'windows-latest' + shell: powershell + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSScriptAnalyzer-tests-${{ matrix.os }} + path: testResults.xml diff --git a/NuGet.Config b/NuGet.Config index 80f5bd7fc..f003b0fbd 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@ - + - + diff --git a/build.psm1 b/build.psm1 index 86dfbd763..22dc9ee5e 100644 --- a/build.psm1 +++ b/build.psm1 @@ -386,7 +386,6 @@ function Test-ScriptAnalyzer else { $testModulePath = Join-Path "${projectRoot}" -ChildPath out } - $testResultsFile = "'$(Join-Path ${projectRoot} -childPath TestResults.xml)'" $testScripts = "'${projectRoot}\Tests\Build','${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" try { if ( $major -lt 5 ) { @@ -395,7 +394,7 @@ function Test-ScriptAnalyzer $savedModulePath = $env:PSModulePath $env:PSModulePath = "${testModulePath}{0}${env:PSModulePath}" -f [System.IO.Path]::PathSeparator $analyzerPsd1Path = Join-Path -Path $script:destinationDir -ChildPath "$analyzerName.psd1" - $scriptBlock = [scriptblock]::Create("Import-Module '$analyzerPsd1Path'; Invoke-Pester -Path $testScripts") + $scriptBlock = [scriptblock]::Create("Import-Module '$analyzerPsd1Path'; Invoke-Pester -Path $testScripts -CI") if ( $InProcess ) { & $scriptBlock } diff --git a/tools/installPSResources.ps1 b/tools/installPSResources.ps1 new file mode 100644 index 000000000..08532b8fe --- /dev/null +++ b/tools/installPSResources.ps1 @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ErrorActionPreference = 'Stop' + +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | Out-Null +if ($PSVersionTable.PSVersion.Major -lt 6) { + throw "The build script requires PowerShell 7!" +} + +# TODO: Switch to Install-PSResource when CI uses PowerShell 7.4 +Install-Module -Name platyPS -Scope CurrentUser +Install-Module -Name Pester -Scope CurrentUser From f2378fe8fb714eecbf830c4dd53e7fba9d481169 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:25:37 -0700 Subject: [PATCH 043/130] Delete deprecated files * Azure Pipelines * Docker containers * AppVeyor * ESRP --- .azure-pipelines-ci/ci.yaml | 51 ---- .../templates/test-powershell.yaml | 20 -- .azure-pipelines-ci/templates/test-pwsh.yaml | 20 -- .ci/releaseBuild.yml | 214 ----------------- .devcontainer/Dockerfile | 6 - .devcontainer/devcontainer.json | 16 -- .pipelines/OSS_Microsoft_PSSA-Official.yml | 218 ------------------ appveyor.yml | 55 ----- tools/appveyor.psm1 | 174 -------------- tools/docker/ubuntu/Dockerfile | 12 - tools/releaseBuild/AssemblySignConfig.xml | 38 --- tools/releaseBuild/CatalogSignConfig.xml | 6 - tools/releaseBuild/CredScan.Suppressions.json | 17 -- tools/releaseBuild/FileCatalogSigning.xml | 10 - tools/releaseBuild/Image/DockerFile | 28 --- tools/releaseBuild/Image/buildPSSA.ps1 | 4 - tools/releaseBuild/Image/dockerInstall.psm1 | 114 --------- tools/releaseBuild/build.json | 15 -- tools/releaseBuild/signing.xml | 51 ---- tools/releaseBuild/updateSigning.ps1 | 39 ---- tools/releaseBuild/vstsbuild.ps1 | 70 ------ 21 files changed, 1178 deletions(-) delete mode 100644 .azure-pipelines-ci/ci.yaml delete mode 100644 .azure-pipelines-ci/templates/test-powershell.yaml delete mode 100644 .azure-pipelines-ci/templates/test-pwsh.yaml delete mode 100644 .ci/releaseBuild.yml delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .pipelines/OSS_Microsoft_PSSA-Official.yml delete mode 100644 appveyor.yml delete mode 100644 tools/appveyor.psm1 delete mode 100644 tools/docker/ubuntu/Dockerfile delete mode 100644 tools/releaseBuild/AssemblySignConfig.xml delete mode 100644 tools/releaseBuild/CatalogSignConfig.xml delete mode 100644 tools/releaseBuild/CredScan.Suppressions.json delete mode 100644 tools/releaseBuild/FileCatalogSigning.xml delete mode 100644 tools/releaseBuild/Image/DockerFile delete mode 100644 tools/releaseBuild/Image/buildPSSA.ps1 delete mode 100644 tools/releaseBuild/Image/dockerInstall.psm1 delete mode 100644 tools/releaseBuild/build.json delete mode 100644 tools/releaseBuild/signing.xml delete mode 100644 tools/releaseBuild/updateSigning.ps1 delete mode 100644 tools/releaseBuild/vstsbuild.ps1 diff --git a/.azure-pipelines-ci/ci.yaml b/.azure-pipelines-ci/ci.yaml deleted file mode 100644 index 956139389..000000000 --- a/.azure-pipelines-ci/ci.yaml +++ /dev/null @@ -1,51 +0,0 @@ -variables: - # Avoid expensive initialization of dotnet cli, see: https://donovanbrown.com/post/Stop-wasting-time-during-NET-Core-builds - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - -stages: - - stage: Build - jobs: - - job: 'Full_Build' - pool: - vmImage: windows-latest - steps: - - pwsh: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorInstall -SkipPesterInstallation - ./build.ps1 -Configuration 'Release' -All - ./PSCompatibilityCollector/build.ps1 -Configuration 'Release' - displayName: 'Full Build' - - pwsh: | - Write-Host "##vso[artifact.upload containerfolder=out;artifactname=out;]${env:Build_SourcesDirectory}/out" - - stage: Test - jobs: - - job: - strategy: - matrix: - Ubuntu_20_04: - vmImage: ubuntu-20.04 - Ubuntu_22_04: - vmImage: ubuntu-22.04 - mac_Latest: - vmImage: macOS-latest - Windows_Server2019_PowerShell_Core: - vmImage: windows-2019 - Windows_Server2022_PowerShell_Core: - vmImage: windows-2022 - pool: - vmImage: $[ variables['vmImage'] ] - steps: - - template: templates/test-pwsh.yaml - - job: - strategy: - matrix: - Windows_Server2019_PowerShell_5_1: - vmImage: windows-2019 - pwsh: false - Windows_Server2022_PowerShell_5_1: - vmImage: windows-2022 - pwsh: false - pool: - vmImage: $[ variables['vmImage'] ] - steps: - - template: templates/test-powershell.yaml diff --git a/.azure-pipelines-ci/templates/test-powershell.yaml b/.azure-pipelines-ci/templates/test-powershell.yaml deleted file mode 100644 index de3f30b68..000000000 --- a/.azure-pipelines-ci/templates/test-powershell.yaml +++ /dev/null @@ -1,20 +0,0 @@ -steps: -- task: DownloadPipelineArtifact@2 - displayName: 'Download Pipeline Artifact: out Folder' - inputs: - artifactName: out - targetPath: '$(Build.SourcesDirectory)/out' -- task: PowerShell@2 - displayName: 'Test' - retryCountOnTaskFailure: 2 - inputs: - targetType: inline - pwsh: false - script: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorTest -CheckoutPath $env:BUILD_SOURCESDIRECTORY -- task: PublishTestResults@2 - inputs: - testRunner: NUnit - testResultsFiles: 'testResults.xml' - condition: succeededOrFailed() diff --git a/.azure-pipelines-ci/templates/test-pwsh.yaml b/.azure-pipelines-ci/templates/test-pwsh.yaml deleted file mode 100644 index 152458d6c..000000000 --- a/.azure-pipelines-ci/templates/test-pwsh.yaml +++ /dev/null @@ -1,20 +0,0 @@ -steps: -- task: DownloadPipelineArtifact@2 - displayName: 'Download Pipeline Artifact: out Folder' - inputs: - artifactName: out - targetPath: '$(Build.SourcesDirectory)/out' -- task: PowerShell@2 - displayName: 'Test' - retryCountOnTaskFailure: 2 - inputs: - targetType: inline - pwsh: true - script: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorTest -CheckoutPath $env:BUILD_SOURCESDIRECTORY -- task: PublishTestResults@2 - inputs: - testRunner: NUnit - testResultsFiles: 'TestResults.xml' - condition: succeededOrFailed() diff --git a/.ci/releaseBuild.yml b/.ci/releaseBuild.yml deleted file mode 100644 index 57042b77c..000000000 --- a/.ci/releaseBuild.yml +++ /dev/null @@ -1,214 +0,0 @@ -# The name of the build that will be seen in mscodehub -name: PSSA-Release-$(Build.BuildId) -# how is the build triggered -# since this is a release build, no trigger as it's a manual release -trigger: none - -pr: - branches: - include: - - master - - release* - -# variables to set in the build environment -variables: - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - POWERSHELL_TELEMETRY_OPTOUT: 1 - -# since this build relies on templates, we need access to those -# This needs a service connection in the build to work -# the *name* of the service connection must be the same as the endpoint -resources: - repositories: - - repository: ComplianceRepo - type: github - endpoint: ComplianceGHRepo - name: PowerShell/compliance - # this can be any branch of your choosing - ref: master - -# the stages in this build. There are 2 -# the assumption for script analyzer is that test is done as part of -# CI so we needn't do it here -stages: -- stage: Build - displayName: Build - pool: - name: PowerShell1ES # was Package ES CodeHub Lab E - demands: - - ImageOverride -equals PSMMS2019-Secure - jobs: - - job: Build_Job - displayName: Build Microsoft.PowerShell.ScriptAnalyzer - # note the variable reference to ESRP. - # this must be created in Project -> Pipelines -> Library -> VariableGroups - # where it describes the link to the SigningServer - variables: - - group: ESRP - steps: - - checkout: self - - # the steps for building the module go here - - pwsh: | - Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" - try { ./build.ps1 -Configuration Release -All } catch { throw $_ } - displayName: Execute build - - # these are setting vso variables which will be persisted between stages - - pwsh: | - $signSrcPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/out" - # Set signing src path variable - $vstsCommandString = "vso[task.setvariable variable=signSrcPath]${signSrcPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - - $signOutStep1 = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Step1" - $null = New-Item -ItemType Directory -Path $signOutStep1 - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutStep1]${signOutStep1}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - - $signOutPath = "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/signed" - $null = New-Item -ItemType Directory -Path $signOutPath - # Set signing out path variable - $vstsCommandString = "vso[task.setvariable variable=signOutPath]${signOutPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - - # Set path variable for guardian codesign validation - $vstsCommandString = "vso[task.setvariable variable=GDN_CODESIGN_TARGETDIRECTORY]${signOutPath}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - - # Get version and create a variable - $moduleData = Import-PowerShellDataFile "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/Engine/PSScriptAnalyzer.psd1" - $moduleVersion = $moduleData.ModuleVersion - $vstsCommandString = "vso[task.setvariable variable=moduleVersion]${moduleVersion}" - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - - - displayName: Setup variables for signing - - # checkout the Compliance repository so it can be used to do the actual signing - - checkout: ComplianceRepo - - # in script analyzer, we must sign with 2 different certs - # the normal cert for MS created items and the 3rd party cert - # this the MS authored step - # Because this needs 2 certs, we do it in 2 steps. - # the first step signs the binaries and puts them in a staging directory which - # will then be used for the second step. - - template: EsrpSign.yml@ComplianceRepo - parameters: - # the folder which contains the binaries to sign - buildOutputPath: $(signSrcPath) - # the location to put the signed output - signOutputPath: $(signOutStep1) - # the certificate ID to use - certificateId: "CP-230012" - # use minimatch because we need to exclude the NewtonSoft assembly - useMinimatch: true - # the file pattern to use - newtonSoft is excluded - pattern: | - **\*.psd1 - **\*.psm1 - **\*.ps1xml - **\Microsoft*.dll - - # this is the second step of the signing. - # note that the buildOutputPath (where we get the files to sign) - # is the same as the signOutputPath in the previous step - # at the end of this step we will have all the files signed that should be - # signOutPath is the location which contains the files we will use to make the module - - template: EsrpSign.yml@ComplianceRepo - parameters: - # the folder which contains the binaries to sign - buildOutputPath: $(signOutStep1) - # the location to put the signed output - signOutputPath: $(signOutPath) - # the certificate ID to use - # we'll need to change this to the 3rd party cert id - certificateId: "CP-231522" - # use minimatch because we need to exclude the NewtonSoft assembly - useMinimatch: true - # the file pattern to use - only sign newtonsoft and pluralize - pattern: | - **/Pluralize*.dll - **/Newtonsoft*.dll - - # Create the manifest for the module - - template: Sbom.yml@ComplianceRepo - parameters: - BuildDropPath: $(signOutPath) - Build_Repository_Uri: 'https://github.com/powershell/PSScriptAnalyzer' - - # now create the nupkg which we will use to publish the module - # to the powershell gallery (not part of this yaml) - - pwsh: | - Set-Location "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" - ./build -BuildNupkg -CopyManifest -signed - displayName: Create nupkg for publishing - - # finally publish the parts of the build which will be used in the next stages - # if it's not published, the subsequent stages will not be able to access it. - # This is the build directory (it contains all of the dll/pdb files) - - publish: "$(Build.SourcesDirectory)/OSS_Microsoft_PSSA" - artifact: build - displayName: publish build directory - - # export the nupkg only which will be used in the release pipeline - - publish: "$(signOutPath)/PSScriptAnalyzer.$(moduleVersion).nupkg" - artifact: nupkg - displayName: Publish module nupkg - -# Now on to the compliance stage -- stage: compliance - displayName: Compliance - dependsOn: Build - jobs: - - job: Compliance_Job - pool: - name: PowerShell1ES # was Package ES CodeHub Lab E - demands: - - ImageOverride -equals PSMMS2019-Secure - - steps: - - checkout: self - - checkout: ComplianceRepo - - download: current - artifact: build - - # use the templates in the compliance repo - # since script analyzer has modules, we're using the assembly-module-compliance template - # if you don't have assemblies, you should use script-module-compliance template - - template: assembly-module-compliance.yml@ComplianceRepo - parameters: - # component-governance - the path to sources - sourceScanPath: '$(Build.SourcesDirectory)/OSS_Microsoft_PSSA' - # binskim - this isn't recursive, so you need the path to the assemblies - AnalyzeTarget: '$(Pipeline.Workspace)\build\bin\PSV7Release\netcoreapp3.1\Microsoft.Windows.PowerShell.ScriptAnalyzer*.dll' - # credscan - scan the repo for credentials - # you can suppress some files with this. - suppressionsFile: '$(Build.SourcesDirectory)/OSS_Microsoft_PSSA/tools/ReleaseBuild/CredScan.Suppressions.json' - # TermCheck - optionsRulesDBPath: '' - optionsFTPath: '' - # tsa-upload - # the compliance scanning must be uploaded, which you need to request - codeBaseName: 'PSSA_202004' - # selections - APIScan: false # set to false when not using Windows APIs. - -#- template: template/publish.yml -# parameters: -# stageName: AzArtifactsFeed -# environmentName: -# feedCredential: - -#- template: template/publish.yml -# parameters: -# stageName: NuGet -# environmentName: PSMarkdownRenderNuGetApproval -# feedCredential: NugetOrgPush diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index b731d046f..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -FROM mcr.microsoft.com/dotnet/sdk:6.0 - -RUN pwsh --command Install-Module platyPS,Pester -Force diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d48d48c38..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,16 +0,0 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json -{ - "name": "C# (.NET 6.0)", - "dockerFile": "Dockerfile", - "customizations": { - "vscode": { - "settings": { - "terminal.integrated.defaultProfile.linux": "pwsh" - }, - "extensions": [ - "ms-dotnettools.csharp", - "ms-vscode.powershell" - ] - } - } -} diff --git a/.pipelines/OSS_Microsoft_PSSA-Official.yml b/.pipelines/OSS_Microsoft_PSSA-Official.yml deleted file mode 100644 index e8e6cc89f..000000000 --- a/.pipelines/OSS_Microsoft_PSSA-Official.yml +++ /dev/null @@ -1,218 +0,0 @@ -# This Yaml Document has been converted by ESAI Yaml Pipeline Conversion Tool. -# Please make sure to check all the converted content, it is your team's responsibility to make sure that the pipeline is still valid and functions as expected. -# This pipeline will be extended to the OneBranch template -name: PSSA-Release-$(Build.BuildId) -trigger: none -pr: - branches: - include: - - master - - release* -variables: - - name: DOTNET_CLI_TELEMETRY_OPTOUT - value: 1 - - name: POWERSHELL_TELEMETRY_OPTOUT - value: 1 - - name: WindowsContainerImage - value: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest -resources: - repositories: - - repository: onebranchTemplates - type: git - name: OneBranch.Pipelines/GovernedTemplates - ref: refs/heads/main -extends: - template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates - parameters: - featureFlags: - WindowsHostVersion: '1ESWindows2022' - customTags: 'ES365AIMigrationTooling' - globalSdl: - disableLegacyManifest: true - sbom: - enabled: true - packageName: Microsoft.PowerShell.ScriptAnalyzer - codeql: - compiled: - enabled: true - asyncSdl: # https://aka.ms/obpipelines/asyncsdl - enabled: true - forStages: [Build] - credscan: - enabled: true - scanFolder: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA - binskim: - enabled: true - apiscan: - enabled: false - - stages: - - stage: stagebuild - displayName: Build and Package Microsoft.PowerShell.ScriptAnalyzer - jobs: - - job: jobbuild - displayName: Build Microsoft.PowerShell.ScriptAnalyzer Files - variables: - - name: ob_outputDirectory - value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' - - name: repoRoot - value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA - - name: ob_sdl_tsa_configFile - value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA\.config\tsaoptions.json - - name: signSrcPath - value: $(repoRoot)/out - - name: ob_sdl_sbom_enabled - value: true - - name: ob_signing_setup_enabled - value: true - #CodeQL tasks added manually to workaround signing failures - - name: ob_sdl_codeql_compiled_enabled - value: false - - pool: - type: windows - steps: - - checkout: self - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - - - pwsh: | - if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { - Get-ChildItem $(Build.SourcesDirectory) -recurse -ErrorAction SilentlyContinue - throw "tsaoptions.json does not exist under $(repoRoot)/.config" - } - displayName: Test if tsaoptions.json exists - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - - - task: UseDotNet@2 - displayName: 'Install .NET dependencies' - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - inputs: - packageType: 'sdk' - useGlobalJson: true - # this is to ensure that we are installing the dotnet at the same location as container by default install the dotnet sdks - installationPath: 'C:\Program Files\dotnet\' - workingDirectory: $(repoRoot) - - - task: CodeQL3000Init@0 # Add CodeQL Init task right before your 'Build' step. - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - inputs: - Enabled: true - AnalyzeInPipeline: true - Language: csharp - - # this is installing .NET - - pwsh: | - Set-Location "$(repoRoot)" - try { ./build.ps1 -Configuration Release -All } catch { throw $_ } - displayName: Execute build - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - - - task: CodeQL3000Finalize@0 # Add CodeQL Finalize task right after your 'Build' step. - condition: always() - env: - ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - - - task: onebranch.pipeline.signing@1 - displayName: Sign 1st party files - inputs: - command: 'sign' - signing_profile: external_distribution - files_to_sign: '**\*.psd1;**\*.psm1;**\*.ps1xml;**\Microsoft*.dll' - search_root: $(signSrcPath) - - - task: onebranch.pipeline.signing@1 - displayName: Sign 3rd Party files - inputs: - command: 'sign' - signing_profile: 135020002 - files_to_sign: '**/Pluralize*.dll;**/Newtonsoft*.dll' - search_root: $(signSrcPath) - - - task: CopyFiles@2 - displayName: "Copy signed files to ob_outputDirectory - '$(ob_outputDirectory)'" - inputs: - SourceFolder: "$(signSrcPath)" - Contents: '**' - TargetFolder: $(ob_outputDirectory) - - - pwsh: | - $moduleData = Import-PowerShellDataFile "$(repoRoot)/Engine/PSScriptAnalyzer.psd1" - $moduleVersion = $moduleData.ModuleVersion - $vstsCommandString = "vso[task.setvariable variable=ob_sdl_sbom_packageversion]${moduleVersion}" - - Write-Host "sending $vstsCommandString" - Write-Host "##$vstsCommandString" - displayName: Setup SBOM Package Version - - - job: nupkg - dependsOn: jobbuild - displayName: Package Microsoft.PowerShell.ScriptAnalyzer - variables: - - name: ob_outputDirectory - value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' - - name: repoRoot - value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA - - name: ob_sdl_tsa_configFile - value: $(Build.SourcesDirectory)\OSS_Microsoft_PSSA\.config\tsaoptions.json - # Disable because SBOM was already built in the previous job - - name: ob_sdl_sbom_enabled - value: false - - name: signOutPath - value: $(repoRoot)/signed - - name: ob_signing_setup_enabled - value: true - # This job is not compiling code, so disable codeQL - - name: ob_sdl_codeql_compiled_enabled - value: false - - pool: - type: windows - steps: - - checkout: self - - - pwsh: | - if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { - Get-ChildItem $(Build.SourcesDirectory) -recurse -ErrorAction SilentlyContinue - throw "tsaoptions.json does not exist under $(repoRoot)/.config" - } - displayName: Test if tsaoptions.json exists - - - task: DownloadPipelineArtifact@2 - displayName: 'Download build files' - inputs: - targetPath: $(signOutPath) - artifact: drop_stagebuild_jobbuild - - - pwsh: | - Set-Location "$(signOutPath)" - dir -dir -recurse * - displayName: Capture artifacts structure - - - pwsh: | - Set-Location "$(repoRoot)" - ./build -BuildNupkg -CopyManifest -signed - displayName: Create nupkg for publishing - - - task: onebranch.pipeline.signing@1 - displayName: Sign nupkg - inputs: - command: 'sign' - signing_profile: external_distribution - files_to_sign: '**\*.nupkg' - search_root: $(signOutPath) - - - pwsh: | - Set-Location "$(repoRoot)" - dir -file -recurse *.nupkg - displayName: Find Nupkg - - - task: CopyFiles@2 - displayName: "Copy nupkg to ob_outputDirectory - '$(ob_outputDirectory)'" - inputs: - Contents: "$(repoRoot)/**/PSScriptAnalyzer.*.nupkg" - TargetFolder: $(ob_outputDirectory) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4ba8d02c8..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,55 +0,0 @@ -environment: - PSVersion: 5 - BuildConfiguration: Release - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true # For faster CI builds - matrix: - - APPVEYOR_BUILD_WORKER_IMAGE: WMF 4 - PowerShellEdition: WindowsPowerShell - PSVersion: 4 - ## Only the tests for WMF4 remain active in AppVeyor due to Azure DevOps not offering such images ## - # - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - # PowerShellEdition: PowerShellCore - # - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - # PowerShellEdition: WindowsPowerShell - # - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu - # PowerShellEdition: PowerShellCore - -# cache Nuget packages and dotnet CLI cache -cache: - - '%USERPROFILE%\.nuget\packages -> appveyor.yml' - -install: - - ps: if ($env:PowerShellEdition -eq 'WindowsPowerShell') { Import-Module .\tools\appveyor.psm1; Invoke-AppveyorInstall } - - pwsh: if ($env:PowerShellEdition -eq 'PowerShellCore') { Import-Module .\tools\appveyor.psm1; Invoke-AppveyorInstall } - -build_script: - - ps: | - if ( $env:PowerShellEdition -eq 'WindowsPowerShell' ) { - Set-Location $env:APPVEYOR_BUILD_FOLDER - if ( $env:PSVersion -eq "4" ) { # On WMF4: Also build for v3 to check it builds at least since we do not have a WMF3 image - ./build.ps1 -Configuration "$env:BuildConfiguration" -PSVersion 3 - } - ./build.ps1 -Configuration "$env:BuildConfiguration" -PSVersion "$env:PSVersion" - ./PSCompatibilityCollector/build.ps1 -Configuration "$env:BuildConfiguration" -Framework 'net462' - } - - pwsh: | - if ($env:PowerShellEdition -eq 'PowerShellCore') { - Set-Location $env:APPVEYOR_BUILD_FOLDER - ./build.ps1 -Configuration "$env:BuildConfiguration" -PSVersion 7 - ./PSCompatibilityCollector/build.ps1 -Configuration "$env:BuildConfiguration" -Framework 'net6' - } - -test_script: - - ps: | - if ($env:PowerShellEdition -eq 'WindowsPowerShell') { - Invoke-AppveyorTest -CheckoutPath $env:APPVEYOR_BUILD_FOLDER - } - - pwsh: | - if ($env:PowerShellEdition -eq 'PowerShellCore') { - Import-Module .\tools\appveyor.psm1 # Appveyor does not persist pwsh sessions like it does for ps - Invoke-AppveyorTest -CheckoutPath $env:APPVEYOR_BUILD_FOLDER - } - -# Upload the project along with test results as a zip archive -on_finish: - - ps: Import-Module "${env:APPVEYOR_BUILD_FOLDER}\tools\appveyor.psm1"; Invoke-AppveyorFinish diff --git a/tools/appveyor.psm1 b/tools/appveyor.psm1 deleted file mode 100644 index 294e8d096..000000000 --- a/tools/appveyor.psm1 +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -$ErrorActionPreference = 'Stop' - -function Install-Pester { - $requiredPesterVersion = '5.3' - $pester = Get-Module Pester -ListAvailable | Where-Object { $_.Version -ge $requiredPesterVersion } - if ($null -eq $pester) { - if ($null -eq (Get-Module -ListAvailable PowershellGet)) { - # WMF 4 image build - Write-Verbose -Verbose "Installing Pester via nuget" - nuget install Pester -Version $requiredPesterVersion -source https://www.powershellgallery.com/api/v2 -outputDirectory "$env:ProgramFiles\WindowsPowerShell\Modules\." -ExcludeVersion - } - else { - # Visual Studio 2017 build (has already Pester v3, therefore a different installation mechanism is needed to make it also use the new version 4) - Write-Verbose -Verbose "Installing Pester via Install-Module" - $installedPester = Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser -Repository PSGallery -Verbose -PassThru - } - - $pesterVersion = if ($installedPester) { $installedPester.Version } else { $requiredPesterVersion } - - Write-Verbose -Verbose "Installed Pester version $pesterVersion" - } -} - -# Implements the AppVeyor 'install' step and installs the required versions of Pester, platyPS and the .Net Core SDK if needed. -function Invoke-AppVeyorInstall { - param( - # For the multi-stage build in Azure DevOps, Pester is not needed for bootstrapping the build environment - [switch] $SkipPesterInstallation - ) - - $installPowerShellModulesjobs = @() - if (-not $SkipPesterInstallation.IsPresent) { $installPowerShellModulesjobs += Start-Job ${Function:Install-Pester} } - - $installPowerShellModulesjobs += Start-Job { - if ($null -eq (Get-Module -ListAvailable PowershellGet)) { - # WMF 4 image build - Write-Verbose -Verbose "Installing platyPS via nuget" - nuget install platyPS -source https://www.powershellgallery.com/api/v2 -outputDirectory "$Env:ProgramFiles\WindowsPowerShell\Modules\." -ExcludeVersion - } - else { - Write-Verbose -Verbose "Installing platyPS via Install-Module" - Install-Module -Name platyPS -Force -Scope CurrentUser -Repository PSGallery - } - Write-Verbose -Verbose 'Installed platyPS' - } - - # Do not use 'build.ps1 -bootstrap' option for bootstraping the .Net SDK as it does not work well in CI with the AppVeyor Ubuntu image - Write-Verbose -Verbose "Installing required .Net CORE SDK" - # the legacy WMF4 image only has the old preview SDKs of dotnet - $globalDotJson = Get-Content (Join-Path $PSScriptRoot '..\global.json') -Raw | ConvertFrom-Json - $requiredDotNetCoreSDKVersion = $globalDotJson.sdk.version - if ($PSVersionTable.PSVersion.Major -gt 4) { - $requiredDotNetCoreSDKVersionPresent = (dotnet --list-sdks) -match $requiredDotNetCoreSDKVersion - } - else { - # WMF 4 image has old SDK that does not have --list-sdks parameter - $requiredDotNetCoreSDKVersionPresent = (dotnet --version).StartsWith($requiredDotNetCoreSDKVersion) - } - if (-not $requiredDotNetCoreSDKVersionPresent) { - Write-Verbose -Verbose "Installing required .Net CORE SDK $requiredDotNetCoreSDKVersion" - $originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - if ($IsLinux -or $isMacOS) { - Invoke-WebRequest 'https://dot.net/v1/dotnet-install.sh' -OutFile dotnet-install.sh - bash dotnet-install.sh --version $requiredDotNetCoreSDKVersion - [System.Environment]::SetEnvironmentVariable('PATH', "/home/appveyor/.dotnet$([System.IO.Path]::PathSeparator)$PATH") - } - else { - Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -OutFile dotnet-install.ps1 - .\dotnet-install.ps1 -Version $requiredDotNetCoreSDKVersion - } - } - finally { - [Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol - Remove-Item .\dotnet-install.* - } - Write-Verbose -Verbose 'Installed required .Net CORE SDK' - } - - Wait-Job $installPowerShellModulesjobs | Receive-Job - $installPowerShellModulesjobs | ForEach-Object { - if ($_.State -eq 'Failed') { - throw 'Installing PowerShell modules failed, see job logs above' - } - } -} - -# Implements AppVeyor 'test_script' step -function Invoke-AppveyorTest { - Param( - [Parameter(Mandatory)] - [ValidateScript( {Test-Path $_})] - $CheckoutPath - ) - - Install-Pester - - # enforce the language to utf-8 to avoid issues - $env:LANG = "en_US.UTF-8" - Write-Verbose -Verbose ("Running tests on PowerShell version " + $PSVersionTable.PSVersion) - Write-Verbose -Verbose "Language set to '${env:LANG}'" - - # set up env:PSModulePath to the build location, don't copy it to the "normal place" - $analyzerVersion = ([xml](Get-Content "${CheckoutPath}\Engine\Engine.csproj")).SelectSingleNode(".//VersionPrefix")."#text".Trim() - $majorVersion = ([System.Version]$analyzerVersion).Major - $psMajorVersion = $PSVersionTable.PSVersion.Major - - if ( $psMajorVersion -lt 5 ) { - $versionModuleDir = "${CheckoutPath}\out\PSScriptAnalyzer\${analyzerVersion}" - $renameTarget = "${CheckoutPath}\out\PSScriptAnalyzer\PSScriptAnalyzer" - Rename-Item "${versionModuleDir}" "${renameTarget}" - $moduleDir = "${CheckoutPath}\out\PSScriptAnalyzer" - } - else{ - $moduleDir = "${CheckoutPath}\out" - } - - $env:PSModulePath = "${moduleDir}","${env:PSModulePath}" -join [System.IO.Path]::PathSeparator - Write-Verbose -Verbose "module path: ${env:PSModulePath}" - - # Set up testing assets - [string[]] $testScripts = @( - Join-Path $CheckoutPath 'Tests\Engine' - Join-Path $CheckoutPath 'Tests\Rules' - Join-Path $CheckoutPath 'Tests\Documentation' - Join-Path $CheckoutPath 'PSCompatibilityCollector\Tests' - ) - - # Change culture to Turkish to test that PSSA works well with different locales - [System.Threading.Thread]::CurrentThread.CurrentCulture = [cultureinfo]::CreateSpecificCulture('tr-TR') - [System.Threading.Thread]::CurrentThread.CurrentUICulture = [cultureinfo]::CreateSpecificCulture('tr-TR') - - # Run all tests - Import-Module PSScriptAnalyzer - Import-Module Pester - - Write-Verbose -Verbose "Module versions:" - Get-Module PSScriptAnalyzer,Pester,PowershellGet -ErrorAction SilentlyContinue | - ForEach-Object { - Write-Verbose -Verbose "$($_.Name): $($_.Version) [$($_.Path)]" - } - - $configuration = [PesterConfiguration]::Default - $configuration.CodeCoverage.Enabled = $false - $configuration.Output.Verbosity = 'Normal' - $configuration.Run.Exit = $true - $configuration.Run.PassThru = $true - $configuration.Run.Path = $testScripts - $configuration.TestResult.Enabled = $true - Invoke-Pester -Configuration $configuration -} - -# Implements AppVeyor 'on_finish' step -function Invoke-AppveyorFinish { - $uploadUrl = "https://ci.appveyor.com/api/testresults/nunit/${env:APPVEYOR_JOB_ID}" - $testResultsPath = Join-Path $pwd TestResults.xml # default when using the -CI switch in Invoke-Pester - Write-Verbose -Verbose "Uploading test results '$testResultsPath' to '${uploadUrl}'" - $null = (New-Object 'System.Net.WebClient').UploadFile("$uploadUrl" , $testResultsPath) - - $stagingDirectory = (Resolve-Path ..).Path - $zipFile = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" - Add-Type -AssemblyName 'System.IO.Compression.FileSystem' - [System.IO.Compression.ZipFile]::CreateFromDirectory((Join-Path $pwd 'out'), $zipFile) - @( - # add test results as an artifact - (Get-ChildItem testResults.xml) - # You can add other artifacts here - (Get-ChildItem $zipFile) - ) | ForEach-Object { Push-AppveyorArtifact $_.FullName } -} diff --git a/tools/docker/ubuntu/Dockerfile b/tools/docker/ubuntu/Dockerfile deleted file mode 100644 index b3ea385fd..000000000 --- a/tools/docker/ubuntu/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM mcr.microsoft.com/powershell:ubuntu-20.04 - -ENV __InContainer 1 - -RUN apt update -qq && apt install -q -y wget git apt-transport-https -RUN wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb - -RUN apt update -qq && \ - cd / && \ - git clone https://github.com/PowerShell/PSScriptAnalyzer - -RUN pwsh -c 'save-module -name platyps,pester -path $PSHOME/Modules' diff --git a/tools/releaseBuild/AssemblySignConfig.xml b/tools/releaseBuild/AssemblySignConfig.xml deleted file mode 100644 index a3f2e3f1c..000000000 --- a/tools/releaseBuild/AssemblySignConfig.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/releaseBuild/CatalogSignConfig.xml b/tools/releaseBuild/CatalogSignConfig.xml deleted file mode 100644 index a0f7da038..000000000 --- a/tools/releaseBuild/CatalogSignConfig.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/tools/releaseBuild/CredScan.Suppressions.json b/tools/releaseBuild/CredScan.Suppressions.json deleted file mode 100644 index 0a0e5db11..000000000 --- a/tools/releaseBuild/CredScan.Suppressions.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "tool": "Credential Scanner", - "suppressions": [ - { "file": "\\README.md", - "_justification": "The file refers to passwords but has no actual passwords" }, - { "file": "\\Engine\\Settings\\desktop-4.0-windows.json", - "_justification": "The file contains the list of all parameters of a cmdlet but no passwords are actually present." }, - { "file": "\\Engine\\Settings\\desktop-3.0-windows.json", - "_justification": "The file contains the list of all parameters of a cmdlet but no passwords are actually present." }, - { "file": "\\Engine\\Settings\\desktop-5.1.14393.206-windows.json", - "_justification": "The file contains the list of all parameters of a cmdlet but no passwords are actually present." }, - { "file": "\\Tests\\Engine\\RuleSuppression.tests.ps1", - "_justification": "The parameter password is used in function declaration for test but is not called and no password is present." } - ] -} - - diff --git a/tools/releaseBuild/FileCatalogSigning.xml b/tools/releaseBuild/FileCatalogSigning.xml deleted file mode 100644 index e2e2a6323..000000000 --- a/tools/releaseBuild/FileCatalogSigning.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/tools/releaseBuild/Image/DockerFile b/tools/releaseBuild/Image/DockerFile deleted file mode 100644 index 682f7248e..000000000 --- a/tools/releaseBuild/Image/DockerFile +++ /dev/null @@ -1,28 +0,0 @@ -# escape=` -#0.3.6 (no powershell 6) -# FROM microsoft/windowsservercore -FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 -LABEL maintainer='PowerShell Team ' -LABEL description="This Dockerfile for Windows Server Core with git installed via chocolatey." - -SHELL ["C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-command"] -# Install Git, and platyPS -# Git installs to C:\Program Files\Git -# nuget installs to C:\ProgramData\chocolatey\bin\NuGet.exe -COPY dockerInstall.psm1 containerFiles/dockerInstall.psm1 - -RUN Import-Module PackageManagement; ` - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force; ` - Import-Module ./containerFiles/dockerInstall.psm1; ` - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12; ` - Install-ChocolateyPackage -PackageName git -Executable git.exe; ` - Install-ChocolateyPackage -PackageName nuget.commandline -Executable nuget.exe -Cleanup; ` - Install-Module -Force -Name platyPS -Repository PSGallery; ` - Invoke-WebRequest -Uri https://dot.net/v1/dotnet-install.ps1 -OutFile C:/dotnet-install.ps1; ` - C:/dotnet-install.ps1 -Version 3.1.419; ` - Add-Path C:/Users/ContainerAdministrator/AppData/Local/Microsoft/dotnet; - -COPY buildPSSA.ps1 containerFiles/buildPSSA.ps1 - -ENTRYPOINT ["C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-command"] - diff --git a/tools/releaseBuild/Image/buildPSSA.ps1 b/tools/releaseBuild/Image/buildPSSA.ps1 deleted file mode 100644 index bb60eb2b5..000000000 --- a/tools/releaseBuild/Image/buildPSSA.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -push-location C:/PSScriptAnalyzer -import-module C:/PSScriptAnalyzer/Utils/ReleaseMaker.psm1 -New-ReleaseBuild -Copy-Item -Recurse C:/PSScriptAnalyzer/out C:/ diff --git a/tools/releaseBuild/Image/dockerInstall.psm1 b/tools/releaseBuild/Image/dockerInstall.psm1 deleted file mode 100644 index 24d1235d6..000000000 --- a/tools/releaseBuild/Image/dockerInstall.psm1 +++ /dev/null @@ -1,114 +0,0 @@ -function Install-ChocolateyPackage -{ - param( - [Parameter(Mandatory=$true)] - [string] - $PackageName, - - [Parameter(Mandatory=$false)] - [string] - $Executable, - - [string[]] - $ArgumentList, - - [switch] - $Cleanup, - - [int] - $ExecutionTimeout = 2700, - - [string] - $Version - ) - - if(-not(Get-Command -name Choco -ErrorAction SilentlyContinue)) - { - Write-Verbose "Installing Chocolatey provider..." -Verbose - Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | Invoke-Expression - } - - Write-Verbose "Installing $PackageName..." -Verbose - $extraCommand = @() - if($Version) - { - $extraCommand += '--version', $version - } - choco install -y $PackageName --no-progress --execution-timeout=$ExecutionTimeout $ArgumentList $extraCommands - - if($executable) - { - Write-Verbose "Verifing $Executable is in path..." -Verbose - $exeSource = $null - $exeSource = Get-ChildItem -path "$env:ProgramFiles\$Executable" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - if(!$exeSource) - { - Write-Verbose "Falling back to x86 program files..." -Verbose - $exeSource = Get-ChildItem -path "${env:ProgramFiles(x86)}\$Executable" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - } - - # Don't search the chocolatey program data until more official locations have been searched - if(!$exeSource) - { - Write-Verbose "Falling back to chocolatey..." -Verbose - $exeSource = Get-ChildItem -path "$env:ProgramData\chocolatey\$Executable" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - } - - # all obvious locations are exhausted, use brute force and search from the root of the filesystem - if(!$exeSource) - { - Write-Verbose "Falling back to the root of the drive..." -Verbose - $exeSource = Get-ChildItem -path "/$Executable" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName - } - - if(!$exeSource) - { - throw "$Executable not found" - } - - $exePath = Split-Path -Path $exeSource - Add-Path -path $exePath - } - - if($Cleanup.IsPresent) - { - Remove-Folder -Folder "$env:temp\chocolatey" - } -} - -function Add-Path -{ - param - ( - $path - ) - $machinePathString = [System.Environment]::GetEnvironmentVariable('path',[System.EnvironmentVariableTarget]::Machine) - $machinePath = $machinePathString -split ';' - - if($machinePath -inotcontains $path) - { - $newPath = "$machinePathString;$path" - Write-Verbose "Adding $path to path..." -Verbose - [System.Environment]::SetEnvironmentVariable('path',$newPath,[System.EnvironmentVariableTarget]::Machine) - Write-Verbose "Added $path to path." -Verbose - $env:Path += ";$newPath" - } - else - { - Write-Verbose "$path already in path." -Verbose - } -} - -function Remove-Folder -{ - param( - [string] - $Folder - ) - - Write-Verbose "Cleaning up $Folder..." -Verbose - $filter = Join-Path -Path $Folder -ChildPath * - [int]$measuredCleanupMB = (Get-ChildItem $filter -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB - Remove-Item -recurse -force $filter -ErrorAction SilentlyContinue - Write-Verbose "Cleaned up $measuredCleanupMB MB from $Folder" -Verbose -} diff --git a/tools/releaseBuild/build.json b/tools/releaseBuild/build.json deleted file mode 100644 index 8d46f2e33..000000000 --- a/tools/releaseBuild/build.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Windows": { - "Name": "win7-x64", - "RepoDestinationPath": "C:\\PSScriptAnalyzer", - "BuildCommand": "C:\\containerFiles\\buildPSSA.ps1", - "DockerFile": ".\\tools\\releaseBuild\\Image\\DockerFile", - "DockerImageName": "pssa", - "BinaryBucket": "release", - "PublishAsFolder": true, - "AdditionalContextFiles" : [ - ".\\tools\\releaseBuild\\Image\\buildPSSA.ps1", - ".\\tools\\releaseBuild\\Image\\dockerInstall.psm1" - ] - } -} diff --git a/tools/releaseBuild/signing.xml b/tools/releaseBuild/signing.xml deleted file mode 100644 index f2fc1c3ee..000000000 --- a/tools/releaseBuild/signing.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/releaseBuild/updateSigning.ps1 b/tools/releaseBuild/updateSigning.ps1 deleted file mode 100644 index d3973fb44..000000000 --- a/tools/releaseBuild/updateSigning.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -param( - [string] $SigningXmlPath = (Join-Path -Path $PSScriptRoot -ChildPath 'signing.xml') -) -# Script for use in VSTS to update signing.xml - -$ErrorActionPreference = 'Stop' - -# Parse the signing xml -$signingXml = [xml](Get-Content $signingXmlPath) - -# Get any variables to updating 'signType' in the XML -# Define a varabile named `SignType' in VSTS to updating that signing type -# Example: $env:AuthenticodeSignType='newvalue' -# will cause all files with the 'Authenticode' signtype to be updated with the 'newvalue' signtype -$signTypes = @{} -Get-ChildItem -Path env:/*SignType | ForEach-Object -Process { - $signType = $_.Name.ToUpperInvariant().Replace('SIGNTYPE','') - Write-Host "Found SigningType $signType with value $($_.value)" - $signTypes[$signType] = $_.Value -} - -# examine each job in the xml -$signingXml.SignConfigXML.job | ForEach-Object -Process { - # examine each file in the job - $_.file | ForEach-Object -Process { - # if the sign type is one of the variables we found, update it to the new value - $signType = $_.SignType.ToUpperInvariant() - if($signTypes.ContainsKey($signType)) - { - $newSignType = $signTypes[$signType] - Write-Host "Updating $($_.src) to $newSignType" - $_.signType = $newSignType - } - } -} - -$signingXml.Save($signingXmlPath) diff --git a/tools/releaseBuild/vstsbuild.ps1 b/tools/releaseBuild/vstsbuild.ps1 deleted file mode 100644 index 9faeedfa0..000000000 --- a/tools/releaseBuild/vstsbuild.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -[cmdletbinding()] -param ( ) - -Begin -{ - $ErrorActionPreference = 'Stop' - - $gitBinFullPath = (Get-Command -Name git -CommandType Application).Path | Select-Object -First 1 - if ( ! $gitBinFullPath ) - { - throw "Git is missing! Install from 'https://git-scm.com/download/win'" - } - - # clone the release tools - $releaseToolsDirName = "PSRelease" - $releaseToolsLocation = Join-Path -Path $PSScriptRoot -ChildPath PSRelease - if ( Test-Path $releaseToolsLocation ) - { - Remove-Item -Force -Recurse -Path $releaseToolsLocation - } - & $gitBinFullPath clone -b master --quiet https://github.com/PowerShell/${releaseToolsDirName}.git $releaseToolsLocation - Import-Module "$releaseToolsLocation/vstsBuild" -Force - Import-Module "$releaseToolsLocation/dockerBasedBuild" -Force -} - -End { - - $AdditionalFiles = .{ - Join-Path $PSScriptRoot -child "Image/buildPSSA.ps1" - Join-Path $PSScriptRoot -child "Image/dockerInstall.psm1" - } - $buildPackageName = $null - - # defined if building in VSTS - if($env:BUILD_STAGINGDIRECTORY) - { - # Use artifact staging if running in VSTS - $destFolder = $env:BUILD_STAGINGDIRECTORY - } - else - { - # Use temp as destination if not running in VSTS - $destFolder = $env:temp - } - - $resolvedRepoRoot = (Resolve-Path (Join-Path -Path $PSScriptRoot -ChildPath "../../")).Path - - try - { - Write-Verbose "Starting build at $resolvedRepoRoot ..." -Verbose - Clear-VstsTaskState - - $buildArgs = @{ - RepoPath = $resolvedRepoRoot - BuildJsonPath = './tools/releaseBuild/build.json' - Parameters = @{ ReleaseTag = "unused" } # not needed for PSSA - AdditionalFiles = $AdditionalFiles - Name = "win7-x64" - } - Invoke-Build @buildArgs - } - catch - { - Write-VstsError -Error $_ - } - finally{ - Write-VstsTaskState - exit 0 - } -} From 008747c5e4771668ace8d50b7d7e24c1ec8a61a8 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:30:05 -0700 Subject: [PATCH 044/130] Small updates * Slight .NET SDK bump to satisfy Component Governance * Update license with OSPO's preferred format * Add self to TSA notification list * Remove containers VS Code extension suggestion --- .config/tsaoptions.json | 2 +- .vscode/extensions.json | 5 ++--- LICENSE | 12 ++++++------ global.json | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json index 75070cbfd..a0887d850 100644 --- a/.config/tsaoptions.json +++ b/.config/tsaoptions.json @@ -3,7 +3,7 @@ "projectName": "One", "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core", "notificationAliases": [ - "jimtru@microsoft.com", + "andschwa@microsoft.com", "slee@microsoft.com" ], "codebaseName": "PSSA_202403" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 098ed86e6..bf1b08715 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,6 @@ // for the documentation about the extensions.json format "recommendations": [ "ms-vscode.PowerShell", - "ms-dotnettools.csharp", - "ms-vscode-remote.remote-containers" + "ms-dotnettools.csharp" ] -} \ No newline at end of file +} diff --git a/LICENSE b/LICENSE index cec380d8e..48ea6616b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2015 Microsoft Corporation. +Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/global.json b/global.json index 1cec616b3..a12beb82e 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.418" + "version": "6.0.425" } } From 522c8f6d4777a3ee7f5fc447909931ac5eca2a65 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:46:39 -0700 Subject: [PATCH 045/130] Remove `dotnet` bootstrap/installation code We should not be doing this for the developer/user. --- Tests/Build/BuildModule.tests.ps1 | 54 -------- build.ps1 | 20 +-- build.psm1 | 203 +----------------------------- 3 files changed, 5 insertions(+), 272 deletions(-) diff --git a/Tests/Build/BuildModule.tests.ps1 b/Tests/Build/BuildModule.tests.ps1 index 542ac2a52..ebc4e2a6a 100644 --- a/Tests/Build/BuildModule.tests.ps1 +++ b/Tests/Build/BuildModule.tests.ps1 @@ -48,60 +48,6 @@ Describe "Build Module Tests" { } } - Context "Test-DotnetInstallation" { - BeforeAll { - $availableVersions = ConvertTo-PortableVersion -strVersion "2.2.400","2.2.401","2.2.405" - $foundVersion = ConvertTo-PortableVersion -strVersion 2.2.402 - $missingVersion = ConvertTo-PortableVersion -strVersion 2.2.410 - } - - It "Test-DotnetInstallation finds a good version" { - Mock Get-InstalledCLIVersion { return $availableVersions } - Mock Get-GlobalJSonSdkVersion { return $foundVersion } - $result = Test-DotnetInstallation -requestedVersion (Get-GlobalJsonSdkVersion) -installedVersions (Get-InstalledCLIVersion) - Assert-MockCalled "Get-InstalledCLIVersion" -Times 1 - Assert-MockCalled "Get-GlobalJsonSdkVersion" -Times 1 - $result | Should -Be $true - } - - It "Test-DotnetInstallation cannot find a good version should return false" { - Mock Get-InstalledCLIVersion { return $availableVersions } - Mock Get-GlobalJSonSdkVersion { return $missingVersion } - $result = Test-DotnetInstallation -requestedVersion (Get-GlobalJsonSdkVersion) -installedVersions (Get-InstalledCLIVersion) - Assert-MockCalled "Get-InstalledCLIVersion" -Times 1 - Assert-MockCalled "Get-GlobalJsonSdkVersion" -Times 1 - $result | Should -Be $false - } - } - - Context "Receive-DotnetInstallScript" { - - Mock -ModuleName Build Receive-File { new-item -type file TestDrive:/dotnet-install.sh } - It "Downloads the proper non-Windows file" { - try { - push-location TestDrive: - Receive-DotnetInstallScript -platform NonWindows - "TestDrive:/dotnet-install.sh" | Should -Exist - } - finally { - Pop-Location - } - } - - Mock -ModuleName Build Receive-File { new-item -type file TestDrive:/dotnet-install.ps1 } - It "Downloads the proper file Windows file" { - try { - push-location TestDrive: - Receive-DotnetInstallScript -platform "Windows" - "TestDrive:/dotnet-install.ps1" | Should -Exist - } - finally { - Pop-Location - } - } - - } - Context "Test result functions" { BeforeAll { $xmlFile = @' diff --git a/build.ps1 b/build.ps1 index a0e27e798..bbc6c505a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,20 +33,11 @@ param( [Parameter(ParameterSetName='Test')] [switch] $InProcess, - [Parameter(ParameterSetName='Bootstrap')] - [switch] $Bootstrap, - [Parameter(ParameterSetName='BuildAll')] [switch] $Catalog, [Parameter(ParameterSetName='Package')] - [switch] $BuildNupkg, - - [Parameter(ParameterSetName='Package')] - [switch] $CopyManifest, - - [Parameter(ParameterSetName='Package')] - [switch] $Signed + [switch] $BuildNupkg ) @@ -90,15 +81,8 @@ END { } Start-ScriptAnalyzerBuild @buildArgs } - "Bootstrap" { - Install-DotNet - return - } "Package" { - if($CopyManifest) { - Copy-Manifest -signed:$Signed - } - Start-CreatePackage -signed:$Signed + Start-CreatePackage } "Test" { Test-ScriptAnalyzer -InProcess:$InProcess diff --git a/build.psm1 b/build.psm1 index 22dc9ee5e..6889f1b89 100644 --- a/build.psm1 +++ b/build.psm1 @@ -7,8 +7,7 @@ $analyzerName = "PSScriptAnalyzer" function Get-AnalyzerVersion { - $csprojPath = [io.path]::Combine($projectRoot,"Engine","Engine.csproj") - $xml = [xml](Get-Content "${csprojPath}") + [xml]$xml = Get-Content $([io.path]::Combine($projectRoot,"Engine","Engine.csproj")) $xml.SelectSingleNode(".//VersionPrefix")."#text" } @@ -29,61 +28,6 @@ function Publish-File } } -# attempt to get the users module directory -function Get-UserModulePath -{ - if ( $IsCoreCLR -and -not $IsWindows ) - { - $platformType = "System.Management.Automation.Platform" -as [Type] - if ( $platformType ) { - ${platformType}::SelectProductNameForDirectory("USER_MODULES") - } - else { - throw "Could not determine users module path" - } - } - else { - "${HOME}/Documents/WindowsPowerShell/Modules" - } -} - - -function Uninstall-ScriptAnalyzer -{ - [CmdletBinding(SupportsShouldProcess)] - param ( $ModulePath = $(Join-Path -Path (Get-UserModulePath) -ChildPath ${analyzerName}) ) - END { - if ( $PSCmdlet.ShouldProcess("$modulePath") ) { - Remove-Item -Recurse -Path "$ModulePath" -Force - } - } -} - -# install script analyzer, by default into the users module path -function Install-ScriptAnalyzer -{ - [CmdletBinding(SupportsShouldProcess)] - param ( $ModulePath = $(Join-Path -Path (Get-UserModulePath) -ChildPath ${analyzerName}) ) - END { - if ( $PSCmdlet.ShouldProcess("$modulePath") ) { - Copy-Item -Recurse -Path "$script:destinationDir" -Destination "$ModulePath\." -Force - } - } -} - -# if script analyzer is installed, remove it -function Uninstall-ScriptAnalyzer -{ - [CmdletBinding(SupportsShouldProcess)] - param ( $ModulePath = $(Join-Path -Path (Get-UserModulePath) -ChildPath ${analyzerName}) ) - END { - if ((Test-Path $ModulePath) -and (Get-Item $ModulePath).PSIsContainer ) - { - Remove-Item -Force -Recurse $ModulePath - } - } -} - # Clean up the build location function Remove-Build { @@ -157,9 +101,6 @@ function Start-ScriptAnalyzerBuild BEGIN { # don't allow the build to be started unless we have the proper Cli version - # this will not actually install dotnet if it's already present, but it will - # install the proper version - Install-Dotnet if ( -not (Test-SuitableDotnet) ) { $requiredVersion = $script:wantedVersion $foundVersion = Get-InstalledCLIVersion @@ -431,49 +372,6 @@ function Get-TestFailures $results.SelectNodes(".//test-case[@result='Failure']") } -# BOOTSTRAPPING CODE FOR INSTALLING DOTNET -# install dotnet cli tools based on the version mentioned in global.json -function Install-Dotnet -{ - [CmdletBinding(SupportsShouldProcess=$true)] - param ( - [Parameter()][Switch]$Force, - [Parameter()]$version = $( Get-GlobalJsonSdkVersion -Raw ) - ) - - if ( Test-DotnetInstallation -requestedversion $version ) { - if ( $Force ) { - Write-Verbose -Verbose "Installing again" - } - else { - return - } - } - - try { - Push-Location $PSScriptRoot - $installScriptPath = Receive-DotnetInstallScript - $installScriptName = [System.IO.Path]::GetFileName($installScriptPath) - If ( $PSCmdlet.ShouldProcess("$installScriptName for $version")) { - & "${installScriptPath}" -c release -version $version -SkipNonVersionedFiles - } - # this may be the first time that dotnet has been installed, - # set up the executable variable - if ( -not $script:DotnetExe ) { - $script:DotnetExe = Get-DotnetExe - } - } - catch { - throw $_ - } - finally { - if ( Test-Path $installScriptPath ) { - Remove-Item $installScriptPath - } - Pop-Location - } -} - function Get-GlobalJsonSdkVersion { param ( [switch]$Raw ) $json = Get-Content -raw (Join-Path $PSScriptRoot global.json) | ConvertFrom-Json @@ -612,68 +510,6 @@ function Get-InstalledCLIVersion { return (ConvertTo-PortableVersion $installedVersions) } -function Test-DotnetInstallation -{ - param ( - $requestedVersion = $script:wantedVersion, - $installedVersions = $( Get-InstalledCLIVersion ) - ) - return (Test-SuitableDotnet -availableVersions $installedVersions -requiredVersion $requestedVersion ) -} - -function Receive-File { - param ( [Parameter(Mandatory,Position=0)]$uri ) - - # enable Tls12 for the request - # -SslProtocol parameter for Invoke-WebRequest wasn't in PSv3 - $securityProtocol = [System.Net.ServicePointManager]::SecurityProtocol - $tls12 = [System.Net.SecurityProtocolType]::Tls12 - try { - if ( ([System.Net.ServicePointManager]::SecurityProtocol -band $tls12) -eq 0 ) { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor $tls12 - } - $null = Invoke-WebRequest -Uri ${uri} -OutFile "${installScriptName}" - } - finally { - [System.Net.ServicePointManager]::SecurityProtocol = $securityProtocol - } - if ( (Test-Path Variable:IsWindows) -and -not $IsWindows ) { - chmod +x $installScriptName - } - $installScript = Get-Item $installScriptName -ErrorAction Stop - if ( -not $installScript ) { - throw "Download failure of ${uri}" - } - return $installScript -} - -function Receive-DotnetInstallScript -{ - # param '$platform' is a hook to enable forcing download of a specific - # install script, generally it should not be used except in testing. - param ( $platform = "" ) - - # if $platform has been set, it has priority - # if it's not set to Windows or NonWindows, it will be ignored - if ( $platform -eq "Windows" ) { - $installScriptName = "dotnet-install.ps1" - } - elseif ( $platform -eq "NonWindows" ) { - $installScriptName = "dotnet-install.sh" - } - elseif ( ((Test-Path Variable:IsWindows) -and -not $IsWindows) ) { - # if the variable IsWindows exists and it is set to false - $installScriptName = "dotnet-install.sh" - } - else { # the default case - we're running on a Windows system - $installScriptName = "dotnet-install.ps1" - } - $uri = "https://dot.net/v1/${installScriptName}" - - $installScript = Receive-File -Uri $uri - return $installScript.FullName -} - function Get-DotnetExe { param ( $version = $script:wantedVersion ) @@ -730,7 +566,7 @@ try { $script:DotnetExe = Get-DotnetExe } catch { - Write-Warning "Could not find dotnet executable" + Write-Warning "The dotnet CLI was not found, please install it: https://aka.ms/dotnet-cli" } # Copies the built PSCompatibilityCollector module to the output destination for PSSA @@ -780,44 +616,11 @@ function Copy-CrossCompatibilityModule } } -# copy the manifest into the module if is present -function Copy-Manifest -{ - param ( [switch]$signed ) - if ( $signed ) { - $buildRoot = "signed" - } - else { - $buildRoot = "out" - } - $analyzerVersion = Get-AnalyzerVersion - # location where analyzer goes - # debugging - (Get-ChildItem -File -Recurse)|ForEach-Object {Write-Verbose -Verbose -Message $_} - $modBaseDir = [io.path]::Combine($projectRoot,${buildRoot},"${analyzerName}", $analyzerVersion) - # copy the manifest files - Push-Location $buildRoot - if ( Test-Path _manifest ) { - Copy-Item -Recurse -Path _manifest -Destination $modBaseDir -Verbose - } - else { - Write-Warning -Message "_manifest not found in $PWD" - } - Pop-Location -} - # creates the nuget package which can be used for publishing to the gallery function Start-CreatePackage { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', '')] - param ( [switch]$signed ) try { - if ( $signed ) { - $buildRoot = "signed" - } - else { - $buildRoot = "out" - } + $buildRoot = "out" $repoName = [guid]::NewGuid().ToString() $nupkgDir = Join-Path $PSScriptRoot $buildRoot $null = Register-PSRepository -Name $repoName -InstallationPolicy Trusted -SourceLocation $nupkgDir From 9cdb7c748e18809b38a84adc4660647801cd7832 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:13:35 -0700 Subject: [PATCH 046/130] Setup OneBranch and package build --- .github/workflows/ci-test.yml | 11 ++ .pipelines/PSScriptAnalyzer-Official.yml | 174 +++++++++++++++++++++++ tools/installPSResources.ps1 | 16 +-- 3 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 .pipelines/PSScriptAnalyzer-Official.yml diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index e3252aae0..d681191c8 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -34,6 +34,10 @@ jobs: run: ./build.ps1 -Configuration Release -All shell: pwsh + - name: Package + run: ./build.ps1 -BuildNupkg + shell: pwsh + - name: Test run: ./build.ps1 -Test shell: pwsh @@ -45,6 +49,13 @@ jobs: if: matrix.os == 'windows-latest' shell: powershell + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSScriptAnalyzer-package-${{ matrix.os }} + path: out/**/*.nupkg + - name: Upload test results uses: actions/upload-artifact@v4 if: always() diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml new file mode 100644 index 000000000..61736a39d --- /dev/null +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -0,0 +1,174 @@ +################################################################################# +# OneBranch Pipelines # +# This pipeline was created by EasyStart from a sample located at: # +# https://aka.ms/obpipelines/easystart/samples # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +# Retail Tasks: https://aka.ms/obpipelines/tasks # +# Support: https://aka.ms/onebranchsup # +################################################################################# + +trigger: none + +schedules: +- cron: '20 16 * * 4' + displayName: Weekly CodeQL + branches: + include: + - main + always: true + +parameters: +- name: debug + displayName: Enable debug output + type: boolean + default: false + +variables: + system.debug: ${{ parameters.debug }} + BuildConfiguration: Release + WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + # https://aka.ms/obpipelines/templates + template: v2/OneBranch.Official.CrossPlat.yml@templates + parameters: + globalSdl: # https://aka.ms/obpipelines/sdl + asyncSdl: + enabled: true + forStages: [build] + featureFlags: + EnableCDPxPAT: false + WindowsHostVersion: + Version: 2022 + Network: Netlock + stages: + - stage: build + jobs: + - job: main + displayName: Build package + pool: + type: windows + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/out + steps: + - pwsh: | + [xml]$xml = Get-Content Engine/Engine.csproj + $version = $xml.SelectSingleNode(".//VersionPrefix")."#text" + Write-Output "##vso[task.setvariable variable=version;isOutput=true]$version" + name: package + displayName: Get version from project properties + - task: onebranch.pipeline.version@1 + displayName: Set OneBranch version + inputs: + system: Custom + customVersion: $(package.version) + - task: UseDotNet@2 + displayName: Use .NET SDK + inputs: + packageType: sdk + useGlobalJson: true + - pwsh: | + Register-PSRepository -Name CFS -SourceLocation "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/nuget/v2" -InstallationPolicy Trusted + Install-Module -Repository CFS -Name Microsoft.PowerShell.PSResourceGet + ./tools/installPSResources.ps1 -PSRepository CFS + displayName: Install PSResources + - pwsh: ./build.ps1 -Configuration Release -All + displayName: Build + - task: onebranch.pipeline.signing@1 + displayName: Sign 1st-party files + inputs: + command: sign + signing_profile: external_distribution + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + **/Microsoft.*.dll; + **/*.psd1; + **/*.psm1; + **/*.ps1xml; + - task: onebranch.pipeline.signing@1 + displayName: Sign 3rd-party files + inputs: + command: sign + signing_profile: 135020002 + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + **/Newtonsoft.Json.dll; + **/Pluralize.NET.dll; + - pwsh: ./build.ps1 -BuildNupkg + displayName: Package module + - task: onebranch.pipeline.signing@1 + displayName: Sign NuGet package + inputs: + command: sign + signing_profile: external_distribution + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + *.nupkg + - stage: release + dependsOn: build + condition: ne(variables['Build.Reason'], 'Schedule') + variables: + version: $[ stageDependencies.build.main.outputs['package.version'] ] + drop: $(Pipeline.Workspace)/drop_build_main + jobs: + - job: github + displayName: Publish draft to GitHub + pool: + type: windows + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/out + steps: + - download: current + displayName: Download artifacts + - task: GitHubRelease@1 + displayName: Create GitHub release + inputs: + gitHubConnection: GitHub + repositoryName: PowerShell/PSScriptAnalyzer + assets: | + $(drop)/PSScriptAnalyzer.$(version).nupkg + tagSource: userSpecifiedTag + tag: v$(version) + isDraft: true + addChangeLog: false + releaseNotesSource: inline + releaseNotesInline: "" + - job: validation + displayName: Manual validation + pool: + type: agentless + timeoutInMinutes: 1440 + steps: + - task: ManualValidation@0 + displayName: Wait 24 hours for validation + inputs: + notifyUsers: $(Build.RequestedForEmail) + instructions: Please validate the release and then publish it! + timeoutInMinutes: 1440 + - job: publish + dependsOn: validation + displayName: Publish to PowerShell Gallery + pool: + type: windows + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/out + steps: + - download: current + displayName: Download artifacts + - task: NuGetCommand@2 + displayName: Publish module to PowerShell Gallery + inputs: + command: push + packagesToPush: $(drop)/PSScriptAnalyzer.$(version).nupkg + nuGetFeedType: external + publishFeedCredentials: PowerShellGallery diff --git a/tools/installPSResources.ps1 b/tools/installPSResources.ps1 index 08532b8fe..506d93a35 100644 --- a/tools/installPSResources.ps1 +++ b/tools/installPSResources.ps1 @@ -1,13 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +param( + [ValidateSet("PSGallery", "CFS")] + [string]$PSRepository = "PSGallery" +) -$ErrorActionPreference = 'Stop' - -Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | Out-Null -if ($PSVersionTable.PSVersion.Major -lt 6) { - throw "The build script requires PowerShell 7!" +if ($PSRepository -eq "CFS" -and -not (Get-PSResourceRepository -Name CFS -ErrorAction SilentlyContinue)) { + Register-PSResourceRepository -Name CFS -Uri "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/nuget/v3/index.json" } -# TODO: Switch to Install-PSResource when CI uses PowerShell 7.4 -Install-Module -Name platyPS -Scope CurrentUser -Install-Module -Name Pester -Scope CurrentUser +Install-PSResource -Repository $PSRepository -TrustRepository -Name platyPS +Install-PSResource -Repository $PSRepository -TrustRepository -Name Pester From f7304c77fbbc1b237c70b6f731cccd54a457bea2 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:37:10 -0700 Subject: [PATCH 047/130] Fix unit tests for PowerShell 7.4 Which now comes with PSResourceGet --- Tests/Rules/UseCompatibleCommands.Tests.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Rules/UseCompatibleCommands.Tests.ps1 b/Tests/Rules/UseCompatibleCommands.Tests.ps1 index d2a8c5b8e..cb49f3c1f 100644 --- a/Tests/Rules/UseCompatibleCommands.Tests.ps1 +++ b/Tests/Rules/UseCompatibleCommands.Tests.ps1 @@ -401,6 +401,9 @@ Describe 'UseCompatibleCommands' { ) IgnoreCommands = @( 'Install-Module' + 'Publish-Module' + 'Register-PSRepository' + 'Unregister-PSRepository' # Some PowerShell profiles have Pester installed by default # So Pester is legitimately flagged 'Describe' From 4002b3cddf6fbc9b24f355df6c5ef0c40e8cbaad Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:26:30 -0700 Subject: [PATCH 048/130] Bump SMA version (#2028) --- Engine/Engine.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index 860700b0b..ccee3baee 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -70,7 +70,7 @@ - + $(DefineConstants);PSV7;CORECLR From b389f6c8d398b185988c12b26a1b92e5335428e5 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:07:34 -0700 Subject: [PATCH 049/130] Allow release stage only on manual build So pushes can trigger the pipeline automatically. --- .pipelines/PSScriptAnalyzer-Official.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index 61736a39d..212aeaede 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -8,7 +8,8 @@ # Support: https://aka.ms/onebranchsup # ################################################################################# -trigger: none +trigger: +- main schedules: - cron: '20 16 * * 4' @@ -116,7 +117,7 @@ extends: *.nupkg - stage: release dependsOn: build - condition: ne(variables['Build.Reason'], 'Schedule') + condition: eq(variables['Build.Reason'], 'Manual') variables: version: $[ stageDependencies.build.main.outputs['package.version'] ] drop: $(Pipeline.Workspace)/drop_build_main From b8249e45afbe6bd37238d09947376197ca48b628 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:26:54 -0700 Subject: [PATCH 050/130] Manage packages centrally Like what the internal bot was doing elsewhere, but manually. --- Directory.Build.props | 5 +++++ Directory.Packages.props | 17 +++++++++++++++++ Engine/Engine.csproj | 10 +++++----- ...crosoft.PowerShell.CrossCompatibility.csproj | 17 ++++++++--------- Rules/Rules.csproj | 11 +++++------ 5 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..193280892 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..5d723ff16 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index ccee3baee..ad301705a 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -45,7 +45,7 @@ - + - - - - + + + + - - - + + + - + diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index a9497824d..2afc1a20b 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -17,15 +17,14 @@ - - - - - + + + + - + From 6b670b40beabe9cbd6c501390fa809c105573e17 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:27:58 -0700 Subject: [PATCH 051/130] Bump System.Management.Automation since 7.0 is EOL --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d723ff16..e43bd7dfd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + From 15c37ec3a37276338237358559440942b736dbca Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:51:55 -0700 Subject: [PATCH 052/130] Centralize the project's version too Add help document to updateVersion.ps1 --- .pipelines/PSScriptAnalyzer-Official.yml | 4 +- Directory.Build.props | 2 + Engine/Engine.csproj | 4 +- Engine/PSScriptAnalyzer.psd1 | 2 +- ...osoft.PowerShell.CrossCompatibility.csproj | 4 +- Rules/Rules.csproj | 4 +- Utils/ReleaseMaker.psm1 | 198 ------------------ build.psm1 | 14 +- tools/updateVersion.ps1 | 29 +++ 9 files changed, 50 insertions(+), 211 deletions(-) delete mode 100644 Utils/ReleaseMaker.psm1 create mode 100644 tools/updateVersion.ps1 diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index 212aeaede..15bccd7f0 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -63,8 +63,8 @@ extends: ob_outputDirectory: $(Build.SourcesDirectory)/out steps: - pwsh: | - [xml]$xml = Get-Content Engine/Engine.csproj - $version = $xml.SelectSingleNode(".//VersionPrefix")."#text" + [xml]$xml = Get-Content ./Directory.Build.props + $version = $xml.Project.PropertyGroup.ModuleVersion Write-Output "##vso[task.setvariable variable=version;isOutput=true]$version" name: package displayName: Get version from project properties diff --git a/Directory.Build.props b/Directory.Build.props index 193280892..e05d0ecbe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,7 @@ + + 1.22.0 true diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index ad301705a..3025c9a08 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -1,10 +1,10 @@  - 1.22.0 + $(ModuleVersion) net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer - 1.22.0 + $(ModuleVersion) Engine Microsoft.Windows.PowerShell.ScriptAnalyzer 9.0 diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index feae6fba4..5e933ca4a 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -11,7 +11,7 @@ Author = 'Microsoft Corporation' RootModule = 'PSScriptAnalyzer.psm1' # Version number of this module. -ModuleVersion = '1.22.0' +ModuleVersion = '{{ModuleVersion}}' # ID used to uniquely identify this module GUID = 'd6245802-193d-4068-a631-8863a4342a18' diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index a1eeae704..da987fb69 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -1,9 +1,9 @@  - 1.22.0 + $(ModuleVersion) netstandard2.0;net462 - 1.22.0 + $(ModuleVersion) diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 2afc1a20b..8fef9e969 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -1,10 +1,10 @@ - 1.22.0 + $(ModuleVersion) net6;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules - 1.22.0 + $(ModuleVersion) Rules Microsoft.Windows.PowerShell.ScriptAnalyzer true diff --git a/Utils/ReleaseMaker.psm1 b/Utils/ReleaseMaker.psm1 deleted file mode 100644 index ac59dbb32..000000000 --- a/Utils/ReleaseMaker.psm1 +++ /dev/null @@ -1,198 +0,0 @@ -Function Get-SolutionPath -{ - Split-Path $PSScriptRoot -Parent -} - -Function Get-ChangeLogPath -{ - Join-Path (Get-SolutionPath) 'CHANGELOG.MD' -} - -Function Get-EngineProjectPath -{ - Join-Path (Get-SolutionPath) 'Engine' -} - -Function Get-ModuleManifestPath -{ - Join-Path (Get-EngineProjectPath) 'PSScriptAnalyzer.psd1' -} - -Function New-Release -{ - [CmdletBinding()] - param($newVer, $oldVer) - - $isVersionGiven = $true - if ($null -eq $newVer -or $null -eq $oldVer) - { - Write-Warning "Parameters are null. Checking changelog for version..." - $isVersionGiven = $false - } - - $solutionRoot = Get-SolutionPath - $enginePath = Get-EngineProjectPath - $versions = Get-VersionsFromChangeLog - if ($versions.Count -le 2) - { - throw "This edge condition for the number versions less that 2 is not implemented." - } - - if ($isVersionGiven) - { - Function Test-IfNotPresentInChangelog - { - param($extractedVersion, $inputVersion) - if ($extractedVersion -ne $inputVersion) - { - throw ("Version {0} does not exist in changelog. Please update changelog." -f $inputVersion) - } - } - - Test-IfNotPresentInChangelog $versions[0] $newVer - Test-IfNotPresentInChangelog $versions[1] $oldVer - } - else - { - $newVer = $versions[0] - $oldVer = $versions[1] - $caption = "Version Check" - $query = "Is version {0} the next release and version {1} the previous release ?" -f $newVer,$oldVer - [bool] $yesToAll = $false - [bool] $noToAll = $false - - if (!$PSCmdlet.ShouldContinue($query, $caption, $false, [ref] $yesToAll, [ref] $noToAll)) - { - return "Aborting..." - } - } - - # update version - Update-Version $newVer $oldVer $solutionRoot - - # copy release notes from changelog to module manifest - Update-ReleaseNotesInModuleManifest $newVer $oldVer - - # build the module - New-ReleaseBuild -} - -function Get-VersionsFromChangeLog -{ - $moduleManifestPath = Get-ModuleManifestPath - $changelogPath = Get-ChangeLogPath - $matches = [regex]::new("\[(\d+\.\d+\.\d+)\]").Matches((get-content $changelogPath -raw)) - $versions = $matches | ForEach-Object {$_.Groups[1].Value} - $versions -} - -function New-ReleaseBuild -{ - $solutionPath = Get-SolutionPath - Push-Location $solutionPath - try - { - if ( test-path out ) { remove-item out/ -recurse -force } - .\build.ps1 -All -Configuration Release - .\PSCompatibilityCollector\build.ps1 -Clean -Configuration Release - Copy-Item -Recurse .\PSCompatibilityCollector\out\* .\out\ - } - finally - { - Pop-Location - } -} - -function Update-ReleaseNotesInModuleManifest -{ - [CmdletBinding()] - param($newVer, $oldVer) - - $moduleManifestPath = Get-ModuleManifestPath - Write-Verbose ("Module manifest: {0}" -f $moduleManifestPath) - $changelogPath = Get-ChangeLogPath - Write-Verbose ("CHANGELOG: {0}" -f $changelogPath) - $changelogRegexPattern = "##\s\[{0}\].*\n((?:.*\n)+)##\s\[{1}\].*" ` - -f [regex]::Escape($newVer),[regex]::Escape($oldVer) - $changelogRegex = [regex]::new($changelogRegexPattern) - $matches = $changelogRegex.Match((get-content $changelogPath -raw)) - $changelogWithHyperlinks = $matches.Groups[1].Value.Trim() - - Write-Verbose 'CHANGELOG:' - Write-Verbose $changelogWithHyperlinks - - # Remove hyperlinks from changelog to make is suitable for publishing on powershellgallery.com - Write-Verbose "Removing hyperlinks from changelog" - $changelog = Remove-MarkdownHyperlink $changelogWithHyperlinks - Write-Verbose "CHANGELOG without hyperlinks:" - Write-Verbose $changelog - - $releaseNotesPattern = ` - "(?ReleaseNotes\s*=\s*@')(?(?:.*\n)*)(?'@)" - $replacement = "`${releaseNotesBegin}" ` - + [environment]::NewLine ` - + $changelog ` - + [environment]::NewLine ` - + "`${releaseNotesEnd}" - $r = [regex]::new($releaseNotesPattern) - $updatedManifestContent = $r.Replace([System.IO.File]::ReadAllText($moduleManifestPath), $replacement) - Set-ContentUtf8NoBom $moduleManifestPath $updatedManifestContent -} - -function Remove-MarkdownHyperlink -{ - param($markdownContent) - $markdownContent -replace "\[(.*?)\]\(.*?\)",'$1' -} - -function Combine-Path -{ - if ($args.Count -lt 2) - { - throw "give more 1 argument" - } - - $path = Join-Path $args[0] $args[1] - for ($k = 2; $k -lt $args.Count; $k++) - { - $path = Join-Path $path $args[$k] - } - - $path -} - -function Update-Version -{ - param( - [string] $newVer, - [string] $oldVer, - [string] $solutionPath - ) - - $ruleJson = Combine-Path $solutionPath 'Rules' 'Rules.csproj' - $engineJson = Combine-Path $solutionPath 'Engine' 'Engine.csproj' - $pssaManifest = Combine-Path $solutionPath 'Engine' 'PSScriptAnalyzer.psd1' - - Update-PatternInFile $ruleJson '"version": "{0}"' $oldVer $newVer - Update-PatternInFile $ruleJson '"Engine": "{0}"' $oldVer $newVer - Update-PatternInFile $engineJson '"version": "{0}"' $oldVer $newVer - Update-PatternInFile $pssaManifest "ModuleVersion = '{0}'" $oldVer $newVer -} - -function Update-PatternInFile -{ - param ($path, $unformattedPattern, $oldVal, $newVal) - - $content = Get-Content $path - $newcontent = $content -replace ($unformattedPattern -f $oldVal),($unformattedPattern -f $newVal) - Set-ContentUtf8NoBom $path $newcontent -} - -function Set-ContentUtf8NoBom { - param($path, $content) - $utfNoBom = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::WriteAllLines($path, $content, $utfNoBom) -} - -Export-ModuleMember -Function New-Release -Export-ModuleMember -Function New-ReleaseBuild diff --git a/build.psm1 b/build.psm1 index 6889f1b89..604c5d17e 100644 --- a/build.psm1 +++ b/build.psm1 @@ -7,8 +7,8 @@ $analyzerName = "PSScriptAnalyzer" function Get-AnalyzerVersion { - [xml]$xml = Get-Content $([io.path]::Combine($projectRoot,"Engine","Engine.csproj")) - $xml.SelectSingleNode(".//VersionPrefix")."#text" + [xml]$xml = Get-Content $([io.path]::Combine($projectRoot, "Directory.Build.props")) + $xml.Project.PropertyGroup.ModuleVersion } $analyzerVersion = Get-AnalyzerVersion @@ -159,9 +159,15 @@ function Start-ScriptAnalyzerBuild throw "Not in solution root" } + # "Copy" the module file with the version placeholder replaced + $manifestContent = Get-Content -LiteralPath "$projectRoot\Engine\PSScriptAnalyzer.psd1" -Raw + $newManifestContent = $manifestContent -replace '{{ModuleVersion}}', $analyzerVersion + Set-Content -LiteralPath "$script:destinationDir\PSScriptAnalyzer.psd1" -Encoding utf8 -Value $newManifestContent + $itemsToCopyCommon = @( - "$projectRoot\Engine\PSScriptAnalyzer.psd1", "$projectRoot\Engine\PSScriptAnalyzer.psm1", - "$projectRoot\Engine\ScriptAnalyzer.format.ps1xml", "$projectRoot\Engine\ScriptAnalyzer.types.ps1xml" + "$projectRoot\Engine\PSScriptAnalyzer.psm1", + "$projectRoot\Engine\ScriptAnalyzer.format.ps1xml", + "$projectRoot\Engine\ScriptAnalyzer.types.ps1xml" ) switch ($PSVersion) diff --git a/tools/updateVersion.ps1 b/tools/updateVersion.ps1 new file mode 100644 index 000000000..7e9d1a47d --- /dev/null +++ b/tools/updateVersion.ps1 @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +param( + [Parameter(Mandatory)] + [semver]$Version, + + [Parameter(Mandatory)] + [string]$Changes +) + +git diff --staged --quiet --exit-code +if ($LASTEXITCODE -ne 0) { + throw "There are staged changes in the repository. Please commit or reset them before running this script." +} + +$Path = "Directory.Build.props" +$f = Get-Content -Path $Path +$f = $f -replace '^(?\s+)(.+)(?)$', "`${prefix}${Version}`${suffix}" +$f | Set-Content -Path $Path +git add $Path + +$Path = "docs/Cmdlets/PSScriptAnalyzer.md" +$f = Get-Content -Path $Path +$f = $f -replace '^(?Help Version: )(.+)$', "`${prefix}${Version}" +$f | Set-Content -Path $Path +git add $Path + +git commit --edit --message "v${Version}: $Changes" From 63ae60ddd0f18b6cc5c7b6f96c3e798f92ea9a72 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:40:25 -0700 Subject: [PATCH 053/130] Setup CODEOWNERS --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..983234361 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Default owners +* @andyleejordan @bergmeister + +# Version bumps and documentation updates +Directory.Build.props @sdwheeler @michaeltlombardi +/docs/ @sdwheeler @michaeltlombardi From 70685bedd61c9b70ab8ddf0f5155304582eb6196 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:48:05 -0700 Subject: [PATCH 054/130] Remove ms.custom version from docs --- docs/Cmdlets/Get-ScriptAnalyzerRule.md | 1 - docs/Cmdlets/Invoke-Formatter.md | 1 - docs/Cmdlets/Invoke-ScriptAnalyzer.md | 1 - docs/Cmdlets/PSScriptAnalyzer.md | 1 - docs/Rules/AlignAssignmentStatement.md | 1 - docs/Rules/AvoidAssignmentToAutomaticVariable.md | 1 - docs/Rules/AvoidDefaultValueForMandatoryParameter.md | 1 - docs/Rules/AvoidDefaultValueSwitchParameter.md | 1 - docs/Rules/AvoidExclaimOperator.md | 1 - docs/Rules/AvoidGlobalAliases.md | 1 - docs/Rules/AvoidGlobalFunctions.md | 1 - docs/Rules/AvoidGlobalVars.md | 1 - docs/Rules/AvoidInvokingEmptyMembers.md | 1 - docs/Rules/AvoidLongLines.md | 1 - docs/Rules/AvoidMultipleTypeAttributes.md | 1 - docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md | 1 - docs/Rules/AvoidOverwritingBuiltInCmdlets.md | 1 - docs/Rules/AvoidSemicolonsAsLineTerminators.md | 1 - docs/Rules/AvoidShouldContinueWithoutForce.md | 1 - docs/Rules/AvoidTrailingWhitespace.md | 1 - docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md | 1 - docs/Rules/AvoidUsingBrokenHashAlgorithms.md | 1 - docs/Rules/AvoidUsingCmdletAliases.md | 1 - docs/Rules/AvoidUsingComputerNameHardcoded.md | 1 - docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md | 1 - docs/Rules/AvoidUsingDeprecatedManifestFields.md | 1 - docs/Rules/AvoidUsingDoubleQuotesForConstantString.md | 1 - docs/Rules/AvoidUsingEmptyCatchBlock.md | 1 - docs/Rules/AvoidUsingInvokeExpression.md | 1 - docs/Rules/AvoidUsingPlainTextForPassword.md | 1 - docs/Rules/AvoidUsingPositionalParameters.md | 1 - docs/Rules/AvoidUsingUsernameAndPasswordParams.md | 1 - docs/Rules/AvoidUsingWMICmdlet.md | 1 - docs/Rules/AvoidUsingWriteHost.md | 1 - docs/Rules/DSCDscExamplesPresent.md | 1 - docs/Rules/DSCDscTestsPresent.md | 1 - docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md | 1 - docs/Rules/DSCStandardDSCFunctionsInResource.md | 1 - docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md | 1 - docs/Rules/DSCUseIdenticalParametersForDSC.md | 1 - docs/Rules/DSCUseVerboseMessageInDSCResource.md | 1 - docs/Rules/MisleadingBacktick.md | 1 - docs/Rules/MissingModuleManifestField.md | 1 - docs/Rules/PlaceCloseBrace.md | 1 - docs/Rules/PlaceOpenBrace.md | 1 - docs/Rules/PossibleIncorrectComparisonWithNull.md | 1 - docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md | 1 - docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md | 1 - docs/Rules/ProvideCommentHelp.md | 1 - docs/Rules/README.md | 1 - docs/Rules/ReservedCmdletChar.md | 1 - docs/Rules/ReservedParams.md | 1 - docs/Rules/ReviewUnusedParameter.md | 1 - docs/Rules/ShouldProcess.md | 1 - docs/Rules/UseApprovedVerbs.md | 3 +-- docs/Rules/UseBOMForUnicodeEncodedFile.md | 1 - docs/Rules/UseCmdletCorrectly.md | 1 - docs/Rules/UseCompatibleCmdlets.md | 1 - docs/Rules/UseCompatibleCommands.md | 1 - docs/Rules/UseCompatibleSyntax.md | 1 - docs/Rules/UseCompatibleTypes.md | 1 - docs/Rules/UseConsistentIndentation.md | 1 - docs/Rules/UseConsistentWhitespace.md | 1 - docs/Rules/UseCorrectCasing.md | 1 - docs/Rules/UseDeclaredVarsMoreThanAssignments.md | 1 - docs/Rules/UseLiteralInitializerForHashtable.md | 1 - docs/Rules/UseOutputTypeCorrectly.md | 1 - docs/Rules/UsePSCredentialType.md | 1 - docs/Rules/UseProcessBlockForPipelineCommand.md | 1 - docs/Rules/UseShouldProcessForStateChangingFunctions.md | 1 - docs/Rules/UseSingularNouns.md | 1 - docs/Rules/UseSupportsShouldProcess.md | 1 - docs/Rules/UseToExportFieldsInManifest.md | 1 - docs/Rules/UseUTF8EncodingForHelpFile.md | 1 - docs/Rules/UseUsingScopeModifierInNewRunspaces.md | 1 - 75 files changed, 1 insertion(+), 76 deletions(-) diff --git a/docs/Cmdlets/Get-ScriptAnalyzerRule.md b/docs/Cmdlets/Get-ScriptAnalyzerRule.md index 90366c5b0..a86d7d301 100644 --- a/docs/Cmdlets/Get-ScriptAnalyzerRule.md +++ b/docs/Cmdlets/Get-ScriptAnalyzerRule.md @@ -1,7 +1,6 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/get-scriptanalyzerrule?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/Invoke-Formatter.md b/docs/Cmdlets/Invoke-Formatter.md index 88dd320ae..1ffb766ce 100644 --- a/docs/Cmdlets/Invoke-Formatter.md +++ b/docs/Cmdlets/Invoke-Formatter.md @@ -1,7 +1,6 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/invoke-formatter?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/Invoke-ScriptAnalyzer.md b/docs/Cmdlets/Invoke-ScriptAnalyzer.md index 9d0195c86..4eb1bff5f 100644 --- a/docs/Cmdlets/Invoke-ScriptAnalyzer.md +++ b/docs/Cmdlets/Invoke-ScriptAnalyzer.md @@ -1,7 +1,6 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/invoke-scriptanalyzer?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index b7267e8b5..1434afdb8 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -4,7 +4,6 @@ Help Version: 1.22.0 Locale: en-US Module Guid: d6245802-193d-4068-a631-8863a4342a18 Module Name: PSScriptAnalyzer -ms.custom: PSSA v1.22.0 ms.date: 10/07/2021 --- diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index e41bff7a1..c2709ac1a 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -1,6 +1,5 @@ --- description: Align assignment statement -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AlignAssignmentStatement diff --git a/docs/Rules/AvoidAssignmentToAutomaticVariable.md b/docs/Rules/AvoidAssignmentToAutomaticVariable.md index 572e9eb3a..f8203dc8e 100644 --- a/docs/Rules/AvoidAssignmentToAutomaticVariable.md +++ b/docs/Rules/AvoidAssignmentToAutomaticVariable.md @@ -1,6 +1,5 @@ --- description: Changing automatic variables might have undesired side effects -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidAssignmentToAutomaticVariable diff --git a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md index ac5fdfc9c..19c9aa732 100644 --- a/docs/Rules/AvoidDefaultValueForMandatoryParameter.md +++ b/docs/Rules/AvoidDefaultValueForMandatoryParameter.md @@ -1,6 +1,5 @@ --- description: Avoid Default Value For Mandatory Parameter -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueForMandatoryParameter diff --git a/docs/Rules/AvoidDefaultValueSwitchParameter.md b/docs/Rules/AvoidDefaultValueSwitchParameter.md index 74e76bdc6..7cfbbc212 100644 --- a/docs/Rules/AvoidDefaultValueSwitchParameter.md +++ b/docs/Rules/AvoidDefaultValueSwitchParameter.md @@ -1,6 +1,5 @@ --- description: Switch Parameters Should Not Default To True -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidDefaultValueSwitchParameter diff --git a/docs/Rules/AvoidExclaimOperator.md b/docs/Rules/AvoidExclaimOperator.md index bafbc9d0e..11d078d6b 100644 --- a/docs/Rules/AvoidExclaimOperator.md +++ b/docs/Rules/AvoidExclaimOperator.md @@ -1,6 +1,5 @@ --- description: Avoid exclaim operator -ms.custom: PSSA v1.22.0 ms.date: 03/26/2024 ms.topic: reference title: AvoidExclaimOperator diff --git a/docs/Rules/AvoidGlobalAliases.md b/docs/Rules/AvoidGlobalAliases.md index 36c581ca6..5157ec6f1 100644 --- a/docs/Rules/AvoidGlobalAliases.md +++ b/docs/Rules/AvoidGlobalAliases.md @@ -1,6 +1,5 @@ --- description: Avoid global aliases. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalAliases diff --git a/docs/Rules/AvoidGlobalFunctions.md b/docs/Rules/AvoidGlobalFunctions.md index ef267ef8b..f74b094cb 100644 --- a/docs/Rules/AvoidGlobalFunctions.md +++ b/docs/Rules/AvoidGlobalFunctions.md @@ -1,6 +1,5 @@ --- description: Avoid global functions and aliases -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalFunctions diff --git a/docs/Rules/AvoidGlobalVars.md b/docs/Rules/AvoidGlobalVars.md index 0edcd4753..7fa0f6d2f 100644 --- a/docs/Rules/AvoidGlobalVars.md +++ b/docs/Rules/AvoidGlobalVars.md @@ -1,6 +1,5 @@ --- description: No Global Variables -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidGlobalVars diff --git a/docs/Rules/AvoidInvokingEmptyMembers.md b/docs/Rules/AvoidInvokingEmptyMembers.md index d512601ad..6049e869b 100644 --- a/docs/Rules/AvoidInvokingEmptyMembers.md +++ b/docs/Rules/AvoidInvokingEmptyMembers.md @@ -1,6 +1,5 @@ --- description: Avoid Invoking Empty Members -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidInvokingEmptyMembers diff --git a/docs/Rules/AvoidLongLines.md b/docs/Rules/AvoidLongLines.md index 2dd94369a..cc2603c51 100644 --- a/docs/Rules/AvoidLongLines.md +++ b/docs/Rules/AvoidLongLines.md @@ -1,6 +1,5 @@ --- description: Avoid long lines -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidLongLines diff --git a/docs/Rules/AvoidMultipleTypeAttributes.md b/docs/Rules/AvoidMultipleTypeAttributes.md index f97d183c0..9ccd478c5 100644 --- a/docs/Rules/AvoidMultipleTypeAttributes.md +++ b/docs/Rules/AvoidMultipleTypeAttributes.md @@ -1,6 +1,5 @@ --- description: Avoid multiple type specifiers on parameters. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidMultipleTypeAttributes diff --git a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md index cee57e0e2..386c96050 100644 --- a/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md +++ b/docs/Rules/AvoidNullOrEmptyHelpMessageAttribute.md @@ -1,6 +1,5 @@ --- description: Avoid using null or empty HelpMessage parameter attribute. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidNullOrEmptyHelpMessageAttribute diff --git a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md index cd7f9c3a6..1d94a618a 100644 --- a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md +++ b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md @@ -1,6 +1,5 @@ --- description: Avoid overwriting built in cmdlets -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidOverwritingBuiltInCmdlets diff --git a/docs/Rules/AvoidSemicolonsAsLineTerminators.md b/docs/Rules/AvoidSemicolonsAsLineTerminators.md index f6d0d69b7..4716238ce 100644 --- a/docs/Rules/AvoidSemicolonsAsLineTerminators.md +++ b/docs/Rules/AvoidSemicolonsAsLineTerminators.md @@ -1,6 +1,5 @@ --- description: Avoid semicolons as line terminators -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidSemicolonsAsLineTerminators diff --git a/docs/Rules/AvoidShouldContinueWithoutForce.md b/docs/Rules/AvoidShouldContinueWithoutForce.md index dba5e6857..189989a89 100644 --- a/docs/Rules/AvoidShouldContinueWithoutForce.md +++ b/docs/Rules/AvoidShouldContinueWithoutForce.md @@ -1,6 +1,5 @@ --- description: Avoid Using ShouldContinue Without Boolean Force Parameter -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidShouldContinueWithoutForce diff --git a/docs/Rules/AvoidTrailingWhitespace.md b/docs/Rules/AvoidTrailingWhitespace.md index 9740d429c..416948c3b 100644 --- a/docs/Rules/AvoidTrailingWhitespace.md +++ b/docs/Rules/AvoidTrailingWhitespace.md @@ -1,6 +1,5 @@ --- description: Avoid trailing whitespace -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidTrailingWhitespace diff --git a/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md index e8ba28167..20451e66f 100644 --- a/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md +++ b/docs/Rules/AvoidUsingAllowUnencryptedAuthentication.md @@ -1,6 +1,5 @@ --- description: Avoid sending credentials and secrets over unencrypted connections -ms.custom: PSSA v1.22.0 ms.date: 02/28/2024 ms.topic: reference title: AvoidUsingAllowUnencryptedAuthentication diff --git a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md index a8fc60dc1..32bb464af 100644 --- a/docs/Rules/AvoidUsingBrokenHashAlgorithms.md +++ b/docs/Rules/AvoidUsingBrokenHashAlgorithms.md @@ -1,6 +1,5 @@ --- description: Avoid using broken hash algorithms -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingBrokenHashAlgorithms diff --git a/docs/Rules/AvoidUsingCmdletAliases.md b/docs/Rules/AvoidUsingCmdletAliases.md index 25adb3767..48c914eec 100644 --- a/docs/Rules/AvoidUsingCmdletAliases.md +++ b/docs/Rules/AvoidUsingCmdletAliases.md @@ -1,6 +1,5 @@ --- description: Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingCmdletAliases diff --git a/docs/Rules/AvoidUsingComputerNameHardcoded.md b/docs/Rules/AvoidUsingComputerNameHardcoded.md index f15b69c06..83a60c950 100644 --- a/docs/Rules/AvoidUsingComputerNameHardcoded.md +++ b/docs/Rules/AvoidUsingComputerNameHardcoded.md @@ -1,6 +1,5 @@ --- description: Avoid Using ComputerName Hardcoded -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingComputerNameHardcoded diff --git a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md index fa6a0df0e..5a94d89a3 100644 --- a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md +++ b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md @@ -1,6 +1,5 @@ --- description: Avoid Using SecureString With Plain Text -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingConvertToSecureStringWithPlainText diff --git a/docs/Rules/AvoidUsingDeprecatedManifestFields.md b/docs/Rules/AvoidUsingDeprecatedManifestFields.md index 32bc4d5b5..802304dfc 100644 --- a/docs/Rules/AvoidUsingDeprecatedManifestFields.md +++ b/docs/Rules/AvoidUsingDeprecatedManifestFields.md @@ -1,6 +1,5 @@ --- description: Avoid Using Deprecated Manifest Fields -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDeprecatedManifestFields diff --git a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md index 002a72aca..19c83fad7 100644 --- a/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md +++ b/docs/Rules/AvoidUsingDoubleQuotesForConstantString.md @@ -1,6 +1,5 @@ --- description: Avoid using double quotes if the string is constant. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingDoubleQuotesForConstantString diff --git a/docs/Rules/AvoidUsingEmptyCatchBlock.md b/docs/Rules/AvoidUsingEmptyCatchBlock.md index 8c944563f..198341758 100644 --- a/docs/Rules/AvoidUsingEmptyCatchBlock.md +++ b/docs/Rules/AvoidUsingEmptyCatchBlock.md @@ -1,6 +1,5 @@ --- description: Avoid Using Empty Catch Block -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingEmptyCatchBlock diff --git a/docs/Rules/AvoidUsingInvokeExpression.md b/docs/Rules/AvoidUsingInvokeExpression.md index 570b51ac4..7779008ec 100644 --- a/docs/Rules/AvoidUsingInvokeExpression.md +++ b/docs/Rules/AvoidUsingInvokeExpression.md @@ -1,6 +1,5 @@ --- description: Avoid Using Invoke-Expression -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingInvokeExpression diff --git a/docs/Rules/AvoidUsingPlainTextForPassword.md b/docs/Rules/AvoidUsingPlainTextForPassword.md index 3d2b9f6e0..c25123ec1 100644 --- a/docs/Rules/AvoidUsingPlainTextForPassword.md +++ b/docs/Rules/AvoidUsingPlainTextForPassword.md @@ -1,6 +1,5 @@ --- description: Avoid Using Plain Text For Password Parameter -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingPlainTextForPassword diff --git a/docs/Rules/AvoidUsingPositionalParameters.md b/docs/Rules/AvoidUsingPositionalParameters.md index d968b9f6e..1a1b77d01 100644 --- a/docs/Rules/AvoidUsingPositionalParameters.md +++ b/docs/Rules/AvoidUsingPositionalParameters.md @@ -1,6 +1,5 @@ --- description: Avoid Using Positional Parameters -ms.custom: PSSA v1.22.0 ms.date: 02/13/2024 ms.topic: reference title: AvoidUsingPositionalParameters diff --git a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md index b37bbfd42..32fbac3c7 100644 --- a/docs/Rules/AvoidUsingUsernameAndPasswordParams.md +++ b/docs/Rules/AvoidUsingUsernameAndPasswordParams.md @@ -1,6 +1,5 @@ --- description: Avoid Using Username and Password Parameters -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingUsernameAndPasswordParams diff --git a/docs/Rules/AvoidUsingWMICmdlet.md b/docs/Rules/AvoidUsingWMICmdlet.md index 47888b361..96e716718 100644 --- a/docs/Rules/AvoidUsingWMICmdlet.md +++ b/docs/Rules/AvoidUsingWMICmdlet.md @@ -1,6 +1,5 @@ --- description: Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWMICmdlet diff --git a/docs/Rules/AvoidUsingWriteHost.md b/docs/Rules/AvoidUsingWriteHost.md index 930202d4f..561914168 100644 --- a/docs/Rules/AvoidUsingWriteHost.md +++ b/docs/Rules/AvoidUsingWriteHost.md @@ -1,6 +1,5 @@ --- description: Avoid Using Write-Host -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: AvoidUsingWriteHost diff --git a/docs/Rules/DSCDscExamplesPresent.md b/docs/Rules/DSCDscExamplesPresent.md index aaf84d185..b2b1cf608 100644 --- a/docs/Rules/DSCDscExamplesPresent.md +++ b/docs/Rules/DSCDscExamplesPresent.md @@ -1,6 +1,5 @@ --- description: DSC examples are present -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCDscExamplesPresent diff --git a/docs/Rules/DSCDscTestsPresent.md b/docs/Rules/DSCDscTestsPresent.md index 22eca027f..f8fe31983 100644 --- a/docs/Rules/DSCDscTestsPresent.md +++ b/docs/Rules/DSCDscTestsPresent.md @@ -1,6 +1,5 @@ --- description: Dsc tests are present -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCDscTestsPresent diff --git a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md index 882a492ca..168185280 100644 --- a/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md +++ b/docs/Rules/DSCReturnCorrectTypesForDSCFunctions.md @@ -1,6 +1,5 @@ --- description: Return Correct Types For DSC Functions -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCReturnCorrectTypesForDSCFunctions diff --git a/docs/Rules/DSCStandardDSCFunctionsInResource.md b/docs/Rules/DSCStandardDSCFunctionsInResource.md index 6280e412c..f455d7437 100644 --- a/docs/Rules/DSCStandardDSCFunctionsInResource.md +++ b/docs/Rules/DSCStandardDSCFunctionsInResource.md @@ -1,6 +1,5 @@ --- description: Use Standard Get/Set/Test TargetResource functions in DSC Resource -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCStandardDSCFunctionsInResource diff --git a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md index 2c6f04709..dc8e9e918 100644 --- a/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalMandatoryParametersForDSC.md @@ -1,6 +1,5 @@ --- description: Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalMandatoryParametersForDSC diff --git a/docs/Rules/DSCUseIdenticalParametersForDSC.md b/docs/Rules/DSCUseIdenticalParametersForDSC.md index 2497f41ad..8f8e98e55 100644 --- a/docs/Rules/DSCUseIdenticalParametersForDSC.md +++ b/docs/Rules/DSCUseIdenticalParametersForDSC.md @@ -1,6 +1,5 @@ --- description: Use Identical Parameters For DSC Test and Set Functions -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseIdenticalParametersForDSC diff --git a/docs/Rules/DSCUseVerboseMessageInDSCResource.md b/docs/Rules/DSCUseVerboseMessageInDSCResource.md index 49ef63ed4..bb5abacef 100644 --- a/docs/Rules/DSCUseVerboseMessageInDSCResource.md +++ b/docs/Rules/DSCUseVerboseMessageInDSCResource.md @@ -1,6 +1,5 @@ --- description: Use verbose message in DSC resource -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: DSCUseVerboseMessageInDSCResource diff --git a/docs/Rules/MisleadingBacktick.md b/docs/Rules/MisleadingBacktick.md index 3194e706f..e140b8e9d 100644 --- a/docs/Rules/MisleadingBacktick.md +++ b/docs/Rules/MisleadingBacktick.md @@ -1,6 +1,5 @@ --- description: Misleading Backtick -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: MisleadingBacktick diff --git a/docs/Rules/MissingModuleManifestField.md b/docs/Rules/MissingModuleManifestField.md index efcf70af1..c19d53454 100644 --- a/docs/Rules/MissingModuleManifestField.md +++ b/docs/Rules/MissingModuleManifestField.md @@ -1,6 +1,5 @@ --- description: Module Manifest Fields -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: MissingModuleManifestField diff --git a/docs/Rules/PlaceCloseBrace.md b/docs/Rules/PlaceCloseBrace.md index d181eebd9..6e14acc74 100644 --- a/docs/Rules/PlaceCloseBrace.md +++ b/docs/Rules/PlaceCloseBrace.md @@ -1,6 +1,5 @@ --- description: Place close braces -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PlaceCloseBrace diff --git a/docs/Rules/PlaceOpenBrace.md b/docs/Rules/PlaceOpenBrace.md index bc3e4b5e2..faa6d4c5d 100644 --- a/docs/Rules/PlaceOpenBrace.md +++ b/docs/Rules/PlaceOpenBrace.md @@ -1,6 +1,5 @@ --- description: Place open braces consistently -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PlaceOpenBrace diff --git a/docs/Rules/PossibleIncorrectComparisonWithNull.md b/docs/Rules/PossibleIncorrectComparisonWithNull.md index d3c62321b..28c9c7075 100644 --- a/docs/Rules/PossibleIncorrectComparisonWithNull.md +++ b/docs/Rules/PossibleIncorrectComparisonWithNull.md @@ -1,6 +1,5 @@ --- description: Null Comparison -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectComparisonWithNull diff --git a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md index 8d046e302..bbaf437b8 100644 --- a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md @@ -1,6 +1,5 @@ --- description: Equal sign is not an assignment operator. Did you mean the equality operator \'-eq\'? -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfAssignmentOperator diff --git a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md index cfd00c05b..871d1340f 100644 --- a/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfRedirectionOperator.md @@ -1,6 +1,5 @@ --- description: \'>\' is not a comparison operator. Use \'-gt\' (greater than) or \'-ge\' (greater or equal). -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: PossibleIncorrectUsageOfRedirectionOperator diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index 51ef66df9..8c642419f 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -1,6 +1,5 @@ --- description: Basic Comment Help -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ProvideCommentHelp diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 4f0d24e68..06f27d2da 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,6 +1,5 @@ --- description: List of PSScriptAnalyzer rules -ms.custom: PSSA v1.22.0 ms.date: 03/27/2024 ms.topic: reference title: List of PSScriptAnalyzer rules diff --git a/docs/Rules/ReservedCmdletChar.md b/docs/Rules/ReservedCmdletChar.md index e4195d42b..55acfc707 100644 --- a/docs/Rules/ReservedCmdletChar.md +++ b/docs/Rules/ReservedCmdletChar.md @@ -1,6 +1,5 @@ --- description: Reserved Cmdlet Chars -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ReservedCmdletChar diff --git a/docs/Rules/ReservedParams.md b/docs/Rules/ReservedParams.md index 6e5c36a18..0e433e0e4 100644 --- a/docs/Rules/ReservedParams.md +++ b/docs/Rules/ReservedParams.md @@ -1,6 +1,5 @@ --- description: Reserved Parameters -ms.custom: PSSA v1.22.0 ms.date: 03/06/2024 ms.topic: reference title: ReservedParams diff --git a/docs/Rules/ReviewUnusedParameter.md b/docs/Rules/ReviewUnusedParameter.md index 6a4447785..4732b8aba 100644 --- a/docs/Rules/ReviewUnusedParameter.md +++ b/docs/Rules/ReviewUnusedParameter.md @@ -1,6 +1,5 @@ --- description: ReviewUnusedParameter -ms.custom: PSSA v1.22.0 ms.date: 03/26/2024 ms.topic: reference title: ReviewUnusedParameter diff --git a/docs/Rules/ShouldProcess.md b/docs/Rules/ShouldProcess.md index 38da8320e..40dcedec4 100644 --- a/docs/Rules/ShouldProcess.md +++ b/docs/Rules/ShouldProcess.md @@ -1,6 +1,5 @@ --- description: Should Process -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: ShouldProcess diff --git a/docs/Rules/UseApprovedVerbs.md b/docs/Rules/UseApprovedVerbs.md index 858979574..c1bcaa0c7 100644 --- a/docs/Rules/UseApprovedVerbs.md +++ b/docs/Rules/UseApprovedVerbs.md @@ -1,6 +1,5 @@ --- description: Cmdlet Verbs -ms.custom: PSSA v1.22.0 ms.date: 03/26/2024 ms.topic: reference title: UseApprovedVerbs @@ -45,4 +44,4 @@ function Update-Item ``` -[01]: /powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands \ No newline at end of file +[01]: /powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands diff --git a/docs/Rules/UseBOMForUnicodeEncodedFile.md b/docs/Rules/UseBOMForUnicodeEncodedFile.md index 2d174f99e..6fffaa7fe 100644 --- a/docs/Rules/UseBOMForUnicodeEncodedFile.md +++ b/docs/Rules/UseBOMForUnicodeEncodedFile.md @@ -1,6 +1,5 @@ --- description: Use BOM encoding for non-ASCII files -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseBOMForUnicodeEncodedFile diff --git a/docs/Rules/UseCmdletCorrectly.md b/docs/Rules/UseCmdletCorrectly.md index 39de79c5c..81ec42e4e 100644 --- a/docs/Rules/UseCmdletCorrectly.md +++ b/docs/Rules/UseCmdletCorrectly.md @@ -1,6 +1,5 @@ --- description: Use Cmdlet Correctly -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCmdletCorrectly diff --git a/docs/Rules/UseCompatibleCmdlets.md b/docs/Rules/UseCompatibleCmdlets.md index a340a6869..1fc9520d4 100644 --- a/docs/Rules/UseCompatibleCmdlets.md +++ b/docs/Rules/UseCompatibleCmdlets.md @@ -1,6 +1,5 @@ --- description: Use compatible cmdlets -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCmdlets diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index dc8c06b97..00b768ba3 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -1,6 +1,5 @@ --- description: Use compatible commands -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleCommands diff --git a/docs/Rules/UseCompatibleSyntax.md b/docs/Rules/UseCompatibleSyntax.md index ebea10c22..a2adbbbb6 100644 --- a/docs/Rules/UseCompatibleSyntax.md +++ b/docs/Rules/UseCompatibleSyntax.md @@ -1,6 +1,5 @@ --- description: Use compatible syntax -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleSyntax diff --git a/docs/Rules/UseCompatibleTypes.md b/docs/Rules/UseCompatibleTypes.md index 72927d140..355f35bed 100644 --- a/docs/Rules/UseCompatibleTypes.md +++ b/docs/Rules/UseCompatibleTypes.md @@ -1,6 +1,5 @@ --- description: Use compatible types -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCompatibleTypes diff --git a/docs/Rules/UseConsistentIndentation.md b/docs/Rules/UseConsistentIndentation.md index fa331a74f..f084d5ac4 100644 --- a/docs/Rules/UseConsistentIndentation.md +++ b/docs/Rules/UseConsistentIndentation.md @@ -1,6 +1,5 @@ --- description: Use consistent indentation -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseConsistentIndentation diff --git a/docs/Rules/UseConsistentWhitespace.md b/docs/Rules/UseConsistentWhitespace.md index 1841ed0f5..e36c42783 100644 --- a/docs/Rules/UseConsistentWhitespace.md +++ b/docs/Rules/UseConsistentWhitespace.md @@ -1,6 +1,5 @@ --- description: Use whitespaces -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseConsistentWhitespace diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index c39c08875..f64a7888d 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -1,6 +1,5 @@ --- description: Use exact casing of cmdlet/function/parameter name. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseCorrectCasing diff --git a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md index 0fb7796c2..e316dbf85 100644 --- a/docs/Rules/UseDeclaredVarsMoreThanAssignments.md +++ b/docs/Rules/UseDeclaredVarsMoreThanAssignments.md @@ -1,6 +1,5 @@ --- description: Extra Variables -ms.custom: PSSA v1.22.0 ms.date: 03/06/2024 ms.topic: reference title: UseDeclaredVarsMoreThanAssignments diff --git a/docs/Rules/UseLiteralInitializerForHashtable.md b/docs/Rules/UseLiteralInitializerForHashtable.md index bb180d832..16f9a2f9b 100644 --- a/docs/Rules/UseLiteralInitializerForHashtable.md +++ b/docs/Rules/UseLiteralInitializerForHashtable.md @@ -1,6 +1,5 @@ --- description: Create hashtables with literal initializers -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseLiteralInitializerForHashtable diff --git a/docs/Rules/UseOutputTypeCorrectly.md b/docs/Rules/UseOutputTypeCorrectly.md index 01895c17d..733425fdd 100644 --- a/docs/Rules/UseOutputTypeCorrectly.md +++ b/docs/Rules/UseOutputTypeCorrectly.md @@ -1,6 +1,5 @@ --- description: Use OutputType Correctly -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseOutputTypeCorrectly diff --git a/docs/Rules/UsePSCredentialType.md b/docs/Rules/UsePSCredentialType.md index c3c2dc7f1..c962762ed 100644 --- a/docs/Rules/UsePSCredentialType.md +++ b/docs/Rules/UsePSCredentialType.md @@ -1,6 +1,5 @@ --- description: Use PSCredential type. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UsePSCredentialType diff --git a/docs/Rules/UseProcessBlockForPipelineCommand.md b/docs/Rules/UseProcessBlockForPipelineCommand.md index d74da5a71..2e5630880 100644 --- a/docs/Rules/UseProcessBlockForPipelineCommand.md +++ b/docs/Rules/UseProcessBlockForPipelineCommand.md @@ -1,6 +1,5 @@ --- description: Use process block for command that accepts input from pipeline. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseProcessBlockForPipelineCommand diff --git a/docs/Rules/UseShouldProcessForStateChangingFunctions.md b/docs/Rules/UseShouldProcessForStateChangingFunctions.md index 5c63d150d..f0e102da3 100644 --- a/docs/Rules/UseShouldProcessForStateChangingFunctions.md +++ b/docs/Rules/UseShouldProcessForStateChangingFunctions.md @@ -1,6 +1,5 @@ --- description: Use ShouldProcess For State Changing Functions -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseShouldProcessForStateChangingFunctions diff --git a/docs/Rules/UseSingularNouns.md b/docs/Rules/UseSingularNouns.md index b7bba39eb..3997c069a 100644 --- a/docs/Rules/UseSingularNouns.md +++ b/docs/Rules/UseSingularNouns.md @@ -1,6 +1,5 @@ --- description: Cmdlet Singular Noun -ms.custom: PSSA v1.22.0 ms.date: 03/27/2024 ms.topic: reference title: UseSingularNouns diff --git a/docs/Rules/UseSupportsShouldProcess.md b/docs/Rules/UseSupportsShouldProcess.md index 40767e060..04ca2bfe7 100644 --- a/docs/Rules/UseSupportsShouldProcess.md +++ b/docs/Rules/UseSupportsShouldProcess.md @@ -1,6 +1,5 @@ --- description: Use SupportsShouldProcess -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseSupportsShouldProcess diff --git a/docs/Rules/UseToExportFieldsInManifest.md b/docs/Rules/UseToExportFieldsInManifest.md index 5a4c87c73..5faaf9d1b 100644 --- a/docs/Rules/UseToExportFieldsInManifest.md +++ b/docs/Rules/UseToExportFieldsInManifest.md @@ -1,6 +1,5 @@ --- description: Use the *ToExport module manifest fields. -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseToExportFieldsInManifest diff --git a/docs/Rules/UseUTF8EncodingForHelpFile.md b/docs/Rules/UseUTF8EncodingForHelpFile.md index 8f7ab429c..31c525db6 100644 --- a/docs/Rules/UseUTF8EncodingForHelpFile.md +++ b/docs/Rules/UseUTF8EncodingForHelpFile.md @@ -1,6 +1,5 @@ --- description: Use UTF8 Encoding For Help File -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseUTF8EncodingForHelpFile diff --git a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md index 86a76561c..bde0f667d 100644 --- a/docs/Rules/UseUsingScopeModifierInNewRunspaces.md +++ b/docs/Rules/UseUsingScopeModifierInNewRunspaces.md @@ -1,6 +1,5 @@ --- description: Use 'Using:' scope modifier in RunSpace ScriptBlocks -ms.custom: PSSA v1.22.0 ms.date: 06/28/2023 ms.topic: reference title: UseUsingScopeModifierInNewRunspaces From 384d26baf834ca4350d0c87c21ebc8aa40dda7aa Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:49:18 -0700 Subject: [PATCH 055/130] Update .gitignore --- .gitignore | 231 ++--------------------------------------------------- 1 file changed, 5 insertions(+), 226 deletions(-) diff --git a/.gitignore b/.gitignore index 92c1621bd..fdf91a4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,226 +1,5 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studo 2015 cache/options directory -.vs/ - -# VSCode configuration directory -.vscode/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .Net Core CLI -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding addin-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -##Our project binplace location -PSScriptAnalyzer/ - -# Vim swap files -*.swp - -# Test result file -TestResults.xml - -# PSCompatibilityCollector module -PSCompatibilityCollector/out/ - -# Folder of build module -out - -# Explicitely Include test dir -!/Tests/** +bin/ +obj/ +/module/ +/out/ +testResults.xml From 6b62797acf669a437d2d63f437f1a9912374d8d6 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:45:25 -0700 Subject: [PATCH 056/130] v1.23.0: Update version for new release --- Directory.Build.props | 2 +- docs/Cmdlets/PSScriptAnalyzer.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e05d0ecbe..d2db04cd1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.22.0 + 1.23.0 true diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index 1434afdb8..df190238e 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -1,6 +1,6 @@ --- Download Help Link: https://aka.ms/ps-modules-help -Help Version: 1.22.0 +Help Version: 1.23.0 Locale: en-US Module Guid: d6245802-193d-4068-a631-8863a4342a18 Module Name: PSScriptAnalyzer From d42c155af90c5dfb2cee1594dbc0e5b93695fdbe Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:59:30 -0700 Subject: [PATCH 057/130] Switch from Netlock to KS3 so GitHub publish task works --- .pipelines/PSScriptAnalyzer-Official.yml | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index 15bccd7f0..f9282aedc 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -51,7 +51,7 @@ extends: EnableCDPxPAT: false WindowsHostVersion: Version: 2022 - Network: Netlock + Network: KS3 stages: - stage: build jobs: diff --git a/global.json b/global.json index a12beb82e..6e6e5445c 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.425" + "version": "6.0.427" } } From e4dd831920f7f450865d664070993d1e4144f6d9 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:59:04 -0700 Subject: [PATCH 058/130] Migrate to DeployBox for release stage --- .pipelines/PSScriptAnalyzer-Official.yml | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index f9282aedc..971cdc351 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -52,6 +52,8 @@ extends: WindowsHostVersion: Version: 2022 Network: KS3 + release: + category: NonAzure stages: - stage: build jobs: @@ -119,25 +121,25 @@ extends: dependsOn: build condition: eq(variables['Build.Reason'], 'Manual') variables: + ob_release_environment: Production version: $[ stageDependencies.build.main.outputs['package.version'] ] - drop: $(Pipeline.Workspace)/drop_build_main jobs: - job: github displayName: Publish draft to GitHub pool: - type: windows - variables: - ob_outputDirectory: $(Build.SourcesDirectory)/out + type: release + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_build_main steps: - - download: current - displayName: Download artifacts - task: GitHubRelease@1 displayName: Create GitHub release inputs: gitHubConnection: GitHub repositoryName: PowerShell/PSScriptAnalyzer - assets: | - $(drop)/PSScriptAnalyzer.$(version).nupkg + target: main + assets: $(Pipeline.Workspace)/PSScriptAnalyzer.$(version).nupkg tagSource: userSpecifiedTag tag: v$(version) isDraft: true @@ -147,7 +149,7 @@ extends: - job: validation displayName: Manual validation pool: - type: agentless + type: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 @@ -160,16 +162,16 @@ extends: dependsOn: validation displayName: Publish to PowerShell Gallery pool: - type: windows - variables: - ob_outputDirectory: $(Build.SourcesDirectory)/out + type: release + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_build_main steps: - - download: current - displayName: Download artifacts - task: NuGetCommand@2 displayName: Publish module to PowerShell Gallery inputs: command: push - packagesToPush: $(drop)/PSScriptAnalyzer.$(version).nupkg + packagesToPush: $(Pipeline.Workspace)/PSScriptAnalyzer.$(version).nupkg nuGetFeedType: external publishFeedCredentials: PowerShellGallery From 58af78698737f5d54c621a8007906eb2a64ba2f2 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 10 Oct 2024 07:40:48 +0100 Subject: [PATCH 059/130] Update links in module manifest Update the name of the default branch. --- Engine/PSScriptAnalyzer.psd1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 5e933ca4a..c7289e890 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -83,9 +83,9 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ Tags = 'lint', 'bestpractice' - LicenseUri = 'https://github.com/PowerShell/PSScriptAnalyzer/blob/master/LICENSE' + LicenseUri = 'https://github.com/PowerShell/PSScriptAnalyzer/blob/main/LICENSE' ProjectUri = 'https://github.com/PowerShell/PSScriptAnalyzer' - IconUri = 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/master/logo.png' + IconUri = 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/main/logo.png' ReleaseNotes = '' } } From a744b6cfb6815d8f8fcc1901e617081580751155 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:54:38 -0700 Subject: [PATCH 060/130] Copy more files to module root These should probably be in the package too. --- build.psm1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.psm1 b/build.psm1 index 604c5d17e..6e3ad2edf 100644 --- a/build.psm1 +++ b/build.psm1 @@ -165,6 +165,10 @@ function Start-ScriptAnalyzerBuild Set-Content -LiteralPath "$script:destinationDir\PSScriptAnalyzer.psd1" -Encoding utf8 -Value $newManifestContent $itemsToCopyCommon = @( + "$projectRoot\LICENSE", + "$projectRoot\README.md", + "$projectRoot\SECURITY.md", + "$projectRoot\ThirdPartyNotices.txt", "$projectRoot\Engine\PSScriptAnalyzer.psm1", "$projectRoot\Engine\ScriptAnalyzer.format.ps1xml", "$projectRoot\Engine\ScriptAnalyzer.types.ps1xml" From 9dacfa87f366ae5723da6041b69cb7c436c4db6d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:15:23 -0800 Subject: [PATCH 061/130] Use `RequiredResource` hashtable to specify PowerShell module versions (#2053) --- tools/installPSResources.ps1 | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tools/installPSResources.ps1 b/tools/installPSResources.ps1 index 506d93a35..48ab81bdd 100644 --- a/tools/installPSResources.ps1 +++ b/tools/installPSResources.ps1 @@ -6,8 +6,19 @@ param( ) if ($PSRepository -eq "CFS" -and -not (Get-PSResourceRepository -Name CFS -ErrorAction SilentlyContinue)) { - Register-PSResourceRepository -Name CFS -Uri "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/nuget/v3/index.json" + Register-PSResourceRepository -Name CFS -Uri "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/PowerShellGalleryMirror/nuget/v3/index.json" } -Install-PSResource -Repository $PSRepository -TrustRepository -Name platyPS -Install-PSResource -Repository $PSRepository -TrustRepository -Name Pester +# NOTE: Due to a bug in Install-PSResource with upstream feeds, we have to +# request an exact version. Otherwise, if a newer version is available in the +# upstream feed, it will fail to install any version at all. +Install-PSResource -Verbose -TrustRepository -RequiredResource @{ + platyPS = @{ + version = "0.14.2" + repository = $PSRepository + } + Pester = @{ + version = "5.7.1" + repository = $PSRepository + } +} From 0e4666756d64fcc47258da6ceefdffd8b6688c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:44:24 +0100 Subject: [PATCH 062/130] Set exit code of `Invoke-ScriptAnalyzer -EnableExit` to total number of diagnostics (#2055) --- .../Commands/InvokeScriptAnalyzerCommand.cs | 20 +++++++++------- Engine/ScriptAnalyzer.cs | 7 +++--- Tests/Engine/InvokeScriptAnalyzer.tests.ps1 | 24 +++++++++++++++++-- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 3be9cd7fc..9e640239f 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -34,6 +34,7 @@ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter #region Private variables List processedPaths; + private int totalDiagnosticCount = 0; #endregion // Private variables #region Parameters @@ -412,6 +413,10 @@ protected override void EndProcessing() { ScriptAnalyzer.Instance.CleanUp(); base.EndProcessing(); + + if (EnableExit) { + this.Host.SetShouldExit(totalDiagnosticCount); + } } protected override void StopProcessing() @@ -426,10 +431,12 @@ protected override void StopProcessing() private void ProcessInput() { - WriteToOutput(RunAnalysis()); + var diagnosticRecords = RunAnalysis(); + WriteToOutput(diagnosticRecords); + totalDiagnosticCount += diagnosticRecords.Count; } - private IEnumerable RunAnalysis() + private List RunAnalysis() { if (!IsFileParameterSet()) { @@ -454,7 +461,7 @@ private IEnumerable RunAnalysis() return diagnostics; } - private void WriteToOutput(IEnumerable diagnosticRecords) + private void WriteToOutput(List diagnosticRecords) { foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) { @@ -507,11 +514,6 @@ private void WriteToOutput(IEnumerable diagnosticRecords) } } } - - if (EnableExit.IsPresent) - { - this.Host.SetShouldExit(diagnosticRecords.Count()); - } } private void ProcessPath() @@ -535,4 +537,4 @@ private bool OverrideSwitchParam(bool paramValue, string paramName) #endregion // Private Methods } -} +} \ No newline at end of file diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 1a885eabe..1946ff957 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1488,7 +1488,7 @@ public IEnumerable AnalyzeAndFixPath(string path, FuncParsed tokens of /// Whether variable analysis can be skipped (applicable if rules do not use variable analysis APIs). /// - public IEnumerable AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false) + public List AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false) { scriptAst = null; scriptTokens = null; @@ -1503,7 +1503,7 @@ public IEnumerable AnalyzeScriptDefinition(string scriptDefini catch (Exception e) { this.outputWriter.WriteWarning(e.ToString()); - return null; + return new(); } var relevantParseErrors = RemoveTypeNotFoundParseErrors(errors, out List diagnosticRecords); @@ -1528,7 +1528,8 @@ public IEnumerable AnalyzeScriptDefinition(string scriptDefini } // now, analyze the script definition - return diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty, skipVariableAnalysis)); + diagnosticRecords.AddRange(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, null, skipVariableAnalysis)); + return diagnosticRecords; } /// diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 06b94cb78..049919136 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -561,9 +561,29 @@ Describe "Test -EnableExit Switch" { $pssaPath = (Get-Module PSScriptAnalyzer).Path - & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -EnableExit" + & $pwshExe -NoProfile -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -EnableExit" - $LASTEXITCODE | Should -Be 1 + $LASTEXITCODE | Should -Be 1 + } + + It "Returns exit code equivalent to number of warnings for multiple piped files" { + if ($IsCoreCLR) + { + $pwshExe = (Get-Process -Id $PID).Path + } + else + { + $pwshExe = 'powershell' + } + + $pssaPath = (Get-Module PSScriptAnalyzer).Path + + & $pwshExe -NoProfile { + Import-Module $Args[0] + Get-ChildItem $Args[1] | Invoke-ScriptAnalyzer -EnableExit + } -Args $pssaPath, "$PSScriptRoot\RecursionDirectoryTest" + + $LASTEXITCODE | Should -Be 2 } Describe "-ReportSummary switch" { From 03beb1766c05e67659825a59b8cfbdc188196bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:56:25 +0100 Subject: [PATCH 063/130] Fix incorrect `-ReportSummary` Pester test grouping (#2057) --- Tests/Engine/InvokeScriptAnalyzer.tests.ps1 | 96 ++++++++++----------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 049919136..51b16f210 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -372,6 +372,7 @@ Describe "Test CustomizedRulePath" { BeforeAll { $measureRequired = "CommunityAnalyzerRules\Measure-RequiresModules" } + Context "When used correctly" { It "with the module folder path" { $customizedRulePath = Invoke-ScriptAnalyzer $PSScriptRoot\TestScript.ps1 -CustomizedRulePath $PSScriptRoot\CommunityAnalyzerRules | Where-Object { $_.RuleName -eq $measureRequired } @@ -516,7 +517,6 @@ Describe "Test CustomizedRulePath" { } Describe "Test -Fix Switch" { - BeforeAll { $scriptName = "TestScriptWithFixableWarnings.ps1" $testSource = Join-Path $PSScriptRoot $scriptName @@ -585,65 +585,65 @@ Describe "Test -EnableExit Switch" { $LASTEXITCODE | Should -Be 2 } +} - Describe "-ReportSummary switch" { - BeforeAll { - $pssaPath = (Get-Module PSScriptAnalyzer).Path - - if ($IsCoreCLR) - { - $pwshExe = (Get-Process -Id $PID).Path - } - else - { - $pwshExe = 'powershell' - } +Describe "-ReportSummary switch" { + BeforeAll { + $pssaPath = (Get-Module PSScriptAnalyzer).Path - $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*' + if ($IsCoreCLR) + { + $pwshExe = (Get-Process -Id $PID).Path + } + else + { + $pwshExe = 'powershell' } - It "prints the correct report summary using the -NoReportSummary switch" { - $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary" + $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*' + } - "$result" | Should -BeLike $reportSummaryFor1Warning - } - It "does not print the report summary when not using -NoReportSummary switch" { - $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci" + It "prints the correct report summary using the -NoReportSummary switch" { + $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary" - "$result" | Should -Not -BeLike $reportSummaryFor1Warning - } + "$result" | Should -BeLike $reportSummaryFor1Warning } + It "does not print the report summary when not using -NoReportSummary switch" { + $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci" - # using statements are only supported in v5+ - Describe "Handles parse errors due to unknown types" -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { - BeforeAll { - $script = @' - using namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels - using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions - Import-Module "AzureRm" - class MyClass { [IStorageContext]$StorageContext } # This will result in a parser error due to [IStorageContext] type that comes from the using statement but is not known at parse time + "$result" | Should -Not -BeLike $reportSummaryFor1Warning + } +} + +# using statements are only supported in v5+ +Describe "Handles parse errors due to unknown types" -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { + BeforeAll { + $script = @' + using namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels + using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions + Import-Module "AzureRm" + class MyClass { [IStorageContext]$StorageContext } # This will result in a parser error due to [IStorageContext] type that comes from the using statement but is not known at parse time '@ - } - It "does not throw and detect one expected warning after the parse error has occured when using -ScriptDefintion parameter set" { - $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $script - $warnings.Count | Should -Be 1 - $warnings.RuleName | Should -Be 'TypeNotFound' - } + } + It "does not throw and detect one expected warning after the parse error has occured when using -ScriptDefintion parameter set" { + $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $script + $warnings.Count | Should -Be 1 + $warnings.RuleName | Should -Be 'TypeNotFound' + } - It "does not throw and detect one expected warning after the parse error has occured when using -Path parameter set" { - $testFilePath = "TestDrive:\testfile.ps1" - Set-Content $testFilePath -Value $script - $warnings = Invoke-ScriptAnalyzer -Path $testFilePath - $warnings.Count | Should -Be 1 - $warnings.RuleName | Should -Be 'TypeNotFound' - } + It "does not throw and detect one expected warning after the parse error has occured when using -Path parameter set" { + $testFilePath = "TestDrive:\testfile.ps1" + Set-Content $testFilePath -Value $script + $warnings = Invoke-ScriptAnalyzer -Path $testFilePath + $warnings.Count | Should -Be 1 + $warnings.RuleName | Should -Be 'TypeNotFound' } +} - Describe 'Handles static Singleton (issue 1182)' -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { - It 'Does not throw or return diagnostic record' { - $scriptDefinition = 'class T { static [T]$i }; function foo { [CmdletBinding()] param () $script:T.WriteLog() }' - Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -ErrorAction Stop | Should -BeNullOrEmpty - } +Describe 'Handles static Singleton (issue 1182)' -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { + It 'Does not throw or return diagnostic record' { + $scriptDefinition = 'class T { static [T]$i }; function foo { [CmdletBinding()] param () $script:T.WriteLog() }' + Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -ErrorAction Stop | Should -BeNullOrEmpty } } From aa7a58225d7b23c92cce628759c3aa939bf79de0 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 19 Feb 2025 18:04:54 +0000 Subject: [PATCH 064/130] AvoidAssignmentToAutomaticVariable: Ignore when a Parameter has an Attribute that contains a Variable expression, such as '[ValidateSet($True,$False)]'. (#1988) Co-authored-by: Christoph Bergmeister --- Rules/AvoidAssignmentToAutomaticVariable.cs | 7 ++++++- Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidAssignmentToAutomaticVariable.cs b/Rules/AvoidAssignmentToAutomaticVariable.cs index c188da341..8188ccb70 100644 --- a/Rules/AvoidAssignmentToAutomaticVariable.cs +++ b/Rules/AvoidAssignmentToAutomaticVariable.cs @@ -89,7 +89,12 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { continue; } - + // also check the parent to exclude variableExpressions that appear within attributes, + // such as '[ValidateSet($True,$False)]' where the read-only variables $true,$false appear. + if (variableExpressionAst.Parent is AttributeAst) + { + continue; + } if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName), diff --git a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 index 8130990c2..e9cc5331f 100644 --- a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 +++ b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 @@ -94,6 +94,13 @@ Describe "AvoidAssignmentToAutomaticVariables" { $warnings.Count | Should -Be 0 } + It "Does not flag true or false being used in ValidateSet" { + # All other read-only automatic variables cannot be used in ValidateSet + # they result in a ParseError. $true and $false are permitted however. + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'param([ValidateSet($true,$false)]$MyVar)$MyVar' -ExcludeRule PSReviewUnusedParameter + $warnings.Count | Should -Be 0 + } + It "Does not throw a NullReferenceException when using assigning a .Net property to a .Net property (Bug in 1.17.0 - issue 1007)" { Invoke-ScriptAnalyzer -ScriptDefinition '[foo]::bar = [baz]::qux' -ErrorAction Stop } From 20135fb6d49812a28e701fc1c7c8549c3067af77 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 19 Feb 2025 18:35:29 +0000 Subject: [PATCH 065/130] Rules>PSAlignAssignmentStatement: Treat single kvp hashtables as being on a single line, and not checked for violations. (#1986) Co-authored-by: Christoph Bergmeister --- Rules/AlignAssignmentStatement.cs | 5 ++++ .../Rules/AlignAssignmentStatement.tests.ps1 | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index d8b1623d6..1d79870f2 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -314,6 +314,11 @@ private static List> GetExtents( private bool HasPropertiesOnSeparateLines(IEnumerable> tuples) { + if (tuples.Count() == 1) + { + // If the hashtable has just a single key-value pair, it does not have properties on separate lines + return false; + } var lines = new HashSet(); foreach (var kvp in tuples) { diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 9a94f48ce..7558abf88 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -75,6 +75,33 @@ $x = @{ } Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 } + + It "Should ignore if a hashtable has a single key-value pair on a single line" { + $def = @' +$x = @{ 'key'="value" } +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + + } + + It "Should ignore if a hashtable has a single key-value pair across multiple lines" { + $def = @' +$x = @{ + 'key'="value" +} +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + + } + + It "Should ignore if a hashtable has multiple key-value pairs on a single line" { + $def = @' +$x = @{ 'key'="value"; 'key2'="value2"; 'key3WithLongerName'="value3" } +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + + } + } Context "When assignment statements are in DSC Configuration" { From 004c8e006986f07f76b6a76b7ad2317f8b3d6ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Plantef=C3=A8ve?= Date: Wed, 19 Feb 2025 23:32:59 +0100 Subject: [PATCH 066/130] Trim unnecessary trailing spaces from string resources in Strings.resx (#1972) --- Rules/Strings.resx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Rules/Strings.resx b/Rules/Strings.resx index ff75828cf..fc92f526d 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -202,13 +202,13 @@ One Char - For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. + For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. - The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. + The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. Use PSCredential type. @@ -535,7 +535,7 @@ PSDSC - Use Standard Get/Set/Test TargetResource functions in DSC Resource + Use Standard Get/Set/Test TargetResource functions in DSC Resource DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions. @@ -769,7 +769,7 @@ In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module. - Do not use wildcard or $null in this field. Explicitly specify a list for {0}. + Do not use wildcard or $null in this field. Explicitly specify a list for {0}. UseToExportFieldsInManifest @@ -1129,7 +1129,7 @@ Ensure all parameters are used within the same script, scriptblock, or function where they are declared. - The parameter '{0}' has been declared but not used. + The parameter '{0}' has been declared but not used. ReviewUnusedParameter From 3a911951fd7d8b3891b550918ecf3a9eda0d32b4 Mon Sep 17 00:00:00 2001 From: John Douglas Leitch Date: Thu, 20 Feb 2025 17:28:51 -0500 Subject: [PATCH 067/130] Fixed erroneous PSUseDeclaredVarsMoreThanAssignments for some globals variables (#2013) * Fixed erroneous PSUseDeclaredVarsMoreThanAssignments for some globals. * Added unit tests to cover global bug in UseDeclaredVarsMoreThanAssignments. --------- Co-authored-by: Christoph Bergmeister --- Engine/Helper.cs | 12 ++---- Engine/VariableAnalysis.cs | 2 +- Rules/UseDeclaredVarsMoreThanAssignments.cs | 2 +- ...eDeclaredVarsMoreThanAssignments.tests.ps1 | 42 +++++++++++++++++++ 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Engine/Helper.cs b/Engine/Helper.cs index ded37b011..82948a4fc 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -870,19 +870,13 @@ public bool IsUninitialized(VariableExpressionAst varAst, Ast ast) } /// - /// Returns true if varaible is either a global variable or an environment variable + /// Returns true if variable is either a global variable or an environment variable /// /// - /// /// - public bool IsVariableGlobalOrEnvironment(VariableExpressionAst varAst, Ast ast) + public bool IsVariableGlobalOrEnvironment(VariableExpressionAst varAst) { - if (!VariableAnalysisDictionary.ContainsKey(ast) || VariableAnalysisDictionary[ast] == null) - { - return false; - } - - return VariableAnalysisDictionary[ast].IsGlobalOrEnvironment(varAst); + return VariableAnalysis.IsGlobalOrEnvironment(varAst); } diff --git a/Engine/VariableAnalysis.cs b/Engine/VariableAnalysis.cs index fd66ea2c4..2870d442f 100644 --- a/Engine/VariableAnalysis.cs +++ b/Engine/VariableAnalysis.cs @@ -375,7 +375,7 @@ public bool IsUninitialized(VariableExpressionAst varTarget) /// /// /// - public bool IsGlobalOrEnvironment(VariableExpressionAst varTarget) + public static bool IsGlobalOrEnvironment(VariableExpressionAst varTarget) { if (varTarget != null) { diff --git a/Rules/UseDeclaredVarsMoreThanAssignments.cs b/Rules/UseDeclaredVarsMoreThanAssignments.cs index 5a8440ada..b35caafbc 100644 --- a/Rules/UseDeclaredVarsMoreThanAssignments.cs +++ b/Rules/UseDeclaredVarsMoreThanAssignments.cs @@ -143,7 +143,7 @@ private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scrip if (assignmentVarAst != null) { // Ignore if variable is global or environment variable or scope is drive qualified variable - if (!Helper.Instance.IsVariableGlobalOrEnvironment(assignmentVarAst, scriptBlockAst) + if (!Helper.Instance.IsVariableGlobalOrEnvironment(assignmentVarAst) && !assignmentVarAst.VariablePath.IsScript && assignmentVarAst.VariablePath.DriveName == null) { diff --git a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 index 823334afb..592aecc91 100644 --- a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 +++ b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 @@ -58,6 +58,48 @@ function MyFunc2() { Should -Be 0 } + It "does not flag global variable" { + Invoke-ScriptAnalyzer -ScriptDefinition '$global:x=$null' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "does not flag global variable in block" { + Invoke-ScriptAnalyzer -ScriptDefinition '$global:x=$null;{$global:x=$null}' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "does not flag env variable" { + Invoke-ScriptAnalyzer -ScriptDefinition '$env:x=$null' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "does not flag env variable in block" { + Invoke-ScriptAnalyzer -ScriptDefinition '$env:x=$null;{$env:x=$null}' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "does not flag script variable" { + Invoke-ScriptAnalyzer -ScriptDefinition '$script:x=$null' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "does not flag script variable in block" { + Invoke-ScriptAnalyzer -ScriptDefinition '$script:x=$null;{$script:x=$null}' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + + It "flags private variable" { + Invoke-ScriptAnalyzer -ScriptDefinition '$private:x=$null' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 1 + } + It "flags a variable that is defined twice but never used" { Invoke-ScriptAnalyzer -ScriptDefinition '$myvar=1;$myvar=2' -IncludeRule $violationName | ` Get-Count | ` From 0fecc084374dc10258d4f7af8658a02688a2519a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Thu, 20 Feb 2025 23:51:42 +0100 Subject: [PATCH 068/130] Do not print summary repeatedly for each logger (#2058) --- .../Commands/InvokeScriptAnalyzerCommand.cs | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 9e640239f..38a2ad9fe 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -431,9 +431,7 @@ protected override void StopProcessing() private void ProcessInput() { - var diagnosticRecords = RunAnalysis(); - WriteToOutput(diagnosticRecords); - totalDiagnosticCount += diagnosticRecords.Count; + WriteToOutput(RunAnalysis()); } private List RunAnalysis() @@ -461,56 +459,59 @@ private List RunAnalysis() return diagnostics; } - private void WriteToOutput(List diagnosticRecords) + private void WriteToOutput(IEnumerable diagnosticRecords) { - foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) - { - var errorCount = 0; - var warningCount = 0; - var infoCount = 0; - var parseErrorCount = 0; + var errorCount = 0; + var warningCount = 0; + var infoCount = 0; + var parseErrorCount = 0; - foreach (DiagnosticRecord diagnostic in diagnosticRecords) + foreach (DiagnosticRecord diagnostic in diagnosticRecords) + { + foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) { logger.LogObject(diagnostic, this); - switch (diagnostic.Severity) - { - case DiagnosticSeverity.Information: - infoCount++; - break; - case DiagnosticSeverity.Warning: - warningCount++; - break; - case DiagnosticSeverity.Error: - errorCount++; - break; - case DiagnosticSeverity.ParseError: - parseErrorCount++; - break; - default: - throw new ArgumentOutOfRangeException(nameof(diagnostic.Severity), $"Severity '{diagnostic.Severity}' is unknown"); - } } - if (ReportSummary.IsPresent) + totalDiagnosticCount++; + + switch (diagnostic.Severity) + { + case DiagnosticSeverity.Information: + infoCount++; + break; + case DiagnosticSeverity.Warning: + warningCount++; + break; + case DiagnosticSeverity.Error: + errorCount++; + break; + case DiagnosticSeverity.ParseError: + parseErrorCount++; + break; + default: + throw new ArgumentOutOfRangeException(nameof(diagnostic.Severity), $"Severity '{diagnostic.Severity}' is unknown"); + } + } + + if (ReportSummary.IsPresent) + { + var numberOfRuleViolations = infoCount + warningCount + errorCount; + if (numberOfRuleViolations == 0) + { + Host.UI.WriteLine("0 rule violations found."); + } + else { - var numberOfRuleViolations = infoCount + warningCount + errorCount; - if (numberOfRuleViolations == 0) + var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; + var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; + if (warningCount + errorCount == 0) { - Host.UI.WriteLine("0 rule violations found."); + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); } else { - var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; - var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; - if (warningCount + errorCount == 0) - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); - } - else - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); - } + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); } } } From 5648cf5cb613b6ee0509dab0bc080786aca25732 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 20 Feb 2025 22:57:59 +0000 Subject: [PATCH 069/130] PSReservedParams: Make severity Error instead of Warning (#1989) Co-authored-by: Christoph Bergmeister --- Rules/AvoidReservedParams.cs | 13 +++++++++++-- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Rules/AvoidReservedParams.cs b/Rules/AvoidReservedParams.cs index 7582d4571..4035a9c89 100644 --- a/Rules/AvoidReservedParams.cs +++ b/Rules/AvoidReservedParams.cs @@ -60,7 +60,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (commonParamNames.Contains(paramName, StringComparer.OrdinalIgnoreCase)) { yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.ReservedParamsError, funcAst.Name, paramName), - paramAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + paramAst.Extent, GetName(), GetDiagnosticSeverity(), fileName); } } } @@ -107,7 +107,16 @@ public SourceType GetSourceType() /// public RuleSeverity GetSeverity() { - return RuleSeverity.Warning; + return RuleSeverity.Error; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Error; } /// diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 93824060a..8d61c1c7f 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -154,17 +154,17 @@ Describe "Test RuleExtension" { Describe "TestSeverity" { It "filters rules based on the specified rule severity" { $rules = Get-ScriptAnalyzerRule -Severity Error - $rules.Count | Should -Be 7 + $rules.Count | Should -Be 8 } It "filters rules based on multiple severity inputs"{ $rules = Get-ScriptAnalyzerRule -Severity Error,Information - $rules.Count | Should -Be 18 + $rules.Count | Should -Be 19 } It "takes lower case inputs" { $rules = Get-ScriptAnalyzerRule -Severity error - $rules.Count | Should -Be 7 + $rules.Count | Should -Be 8 } } From dc4ae4bfbf5329c783afcfca300824618bcef12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadas=20Medi=C5=A1auskas?= Date: Thu, 20 Feb 2025 23:25:21 +0000 Subject: [PATCH 070/130] Make Settings type detection more robust (#1967) * Add PSObject unwrapping for all Settings types * Update Engine/Settings.cs --------- Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Engine/Settings.cs | 12 +++++++----- Tests/Engine/Settings.tests.ps1 | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Engine/Settings.cs b/Engine/Settings.cs index a4931978c..b0c424c64 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -497,6 +497,13 @@ private static bool IsBuiltinSettingPreset(object settingPreset) internal static SettingsMode FindSettingsMode(object settings, string path, out object settingsFound) { var settingsMode = SettingsMode.None; + + // if the provided settings argument is wrapped in an expressions then PowerShell resolves it but it will be of type PSObject and we have to operate then on the BaseObject + if (settings is PSObject settingsFoundPSObject) + { + settings = settingsFoundPSObject.BaseObject; + } + settingsFound = settings; if (settingsFound == null) { @@ -532,11 +539,6 @@ internal static SettingsMode FindSettingsMode(object settings, string path, out { settingsMode = SettingsMode.Hashtable; } - // if the provided argument is wrapped in an expressions then PowerShell resolves it but it will be of type PSObject and we have to operate then on the BaseObject - else if (settingsFound is PSObject settingsFoundPSObject) - { - TryResolveSettingForStringType(settingsFoundPSObject.BaseObject, ref settingsMode, ref settingsFound); - } } } diff --git a/Tests/Engine/Settings.tests.ps1 b/Tests/Engine/Settings.tests.ps1 index 2e95bdd04..917b4ed8e 100644 --- a/Tests/Engine/Settings.tests.ps1 +++ b/Tests/Engine/Settings.tests.ps1 @@ -377,4 +377,34 @@ Describe "Settings Class" { @{ Expr = ';)' } ) } + + Context "FindSettingsMode" { + BeforeAll { + $findSettingsMode = ($settingsTypeName -as [type]).GetMethod( + 'FindSettingsMode', + [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Static) + + $outputObject = [System.Object]::new() + } + + It "Should detect hashtable" { + $settings = @{} + $findSettingsMode.Invoke($null, @($settings, $null, [ref]$outputObject)) | Should -Be "Hashtable" + } + + It "Should detect hashtable wrapped by a PSObject" { + $settings = [PSObject]@{} # Force the settings hashtable to be wrapped + $findSettingsMode.Invoke($null, @($settings, $null, [ref]$outputObject)) | Should -Be "Hashtable" + } + + It "Should detect string" { + $settings = "" + $findSettingsMode.Invoke($null, @($settings, $null, [ref]$outputObject)) | Should -Be "File" + } + + It "Should detect string wrapped by a PSObject" { + $settings = [PSObject]"" # Force the settings string to be wrapped + $findSettingsMode.Invoke($null, @($settings, $null, [ref]$outputObject)) | Should -Be "File" + } + } } From d13809cb18cf49c3b054b6183b06e860b7666be2 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 25 Feb 2025 00:01:13 +0000 Subject: [PATCH 071/130] PSUseConsistentIndentation: Check indentation of lines where first token is a LParen not followed by comment or new line (#1995) * Check a line starting with LParen for correct indentation, even if that LParen doesn't impact the indentation level * Fix typo in test utility for the UseConsistentIndentation tests * Add tests to ensure formatting is still checked when LParen is the first token on a line, followed by a non-newline, non-comment --- Rules/UseConsistentIndentation.cs | 3 ++- Tests/Rules/UseConsistentIndentation.tests.ps1 | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index bbb12bd41..41aa4ef4d 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -163,6 +163,7 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do break; case TokenKind.LParen: + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); // When a line starts with a parenthesis and it is not the last non-comment token of that line, // then indentation does not need to be increased. if ((tokenIndex == 0 || tokens[tokenIndex - 1].Kind == TokenKind.NewLine) && @@ -173,7 +174,7 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do break; } lParenSkippedIndentation.Push(false); - AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); + indentationLevel++; break; case TokenKind.Pipe: diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index 1b556baed..0d26ff39d 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -11,7 +11,7 @@ Describe "UseConsistentIndentation" { function Invoke-FormatterAssertion { param( [string] $ScriptDefinition, - [string] $ExcpectedScriptDefinition, + [string] $ExpectedScriptDefinition, [int] $NumberOfExpectedWarnings, [hashtable] $Settings ) @@ -19,9 +19,9 @@ Describe "UseConsistentIndentation" { # Unit test just using this rule only $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings $violations.Count | Should -Be $NumberOfExpectedWarnings -Because $ScriptDefinition - Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | Should -Be $expected -Because $ScriptDefinition + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | Should -Be $ExpectedScriptDefinition -Because $ScriptDefinition # Integration test with all default formatting rules - Invoke-Formatter -ScriptDefinition $scriptDefinition | Should -Be $expected -Because $ScriptDefinition + Invoke-Formatter -ScriptDefinition $scriptDefinition | Should -Be $ExpectedScriptDefinition -Because $ScriptDefinition } } BeforeEach { @@ -177,6 +177,18 @@ function test { '@ Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition | Should -Be $idempotentScriptDefinition } + + It 'Should find violation in script when LParen is first token on a line and is not followed by Newline' { + $ScriptDefinition = @' + (foo) + (bar) +'@ + $ExpectedScriptDefinition = @' +(foo) +(bar) +'@ + Invoke-FormatterAssertion $ScriptDefinition $ExpectedScriptDefinition 2 $settings + } } Context "When a sub-expression is provided" { From 01a3259f9a5ebdbf1aa1ff55e3ce7005f098d993 Mon Sep 17 00:00:00 2001 From: AJ Raymond <100978322+PoshAJ@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:12:02 -0500 Subject: [PATCH 072/130] Add foreach Assignment to AvoidAssignmentToAutomaticVariable (#2021) * Add Evaluation for foreach Assignment * Add Tests for foreach Assignment * Update Tests for Consistency * Remove Unnecessary ExcludeRule * Update Test Description --------- Co-authored-by: PoshAJ <19650958-PoshAJ@users.noreply.gitlab.com> --- Rules/AvoidAssignmentToAutomaticVariable.cs | 25 +++++++++++++++++++ ...oidAssignmentToAutomaticVariable.tests.ps1 | 15 ++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Rules/AvoidAssignmentToAutomaticVariable.cs b/Rules/AvoidAssignmentToAutomaticVariable.cs index 8188ccb70..c1ce88462 100644 --- a/Rules/AvoidAssignmentToAutomaticVariable.cs +++ b/Rules/AvoidAssignmentToAutomaticVariable.cs @@ -79,6 +79,31 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) } } + IEnumerable forEachStatementAsts = ast.FindAll(testAst => testAst is ForEachStatementAst, searchNestedScriptBlocks: true); + foreach (ForEachStatementAst forEachStatementAst in forEachStatementAsts) + { + var variableExpressionAst = forEachStatementAst.Variable; + var variableName = variableExpressionAst.VariablePath.UserPath; + if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) + { + yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName), + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName, variableName); + } + + if (_readOnlyAutomaticVariablesIntroducedInVersion6_0.Contains(variableName, StringComparer.OrdinalIgnoreCase)) + { + var severity = IsPowerShellVersion6OrGreater() ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning; + yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error, variableName), + variableExpressionAst.Extent, GetName(), severity, fileName, variableName); + } + + if (_writableAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase)) + { + yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToWritableAutomaticVariableError, variableName), + variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName, variableName); + } + } + IEnumerable parameterAsts = ast.FindAll(testAst => testAst is ParameterAst, searchNestedScriptBlocks: true); foreach (ParameterAst parameterAst in parameterAsts) { diff --git a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 index e9cc5331f..98f82be41 100644 --- a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 +++ b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 @@ -65,13 +65,22 @@ Describe "AvoidAssignmentToAutomaticVariables" { It "Variable produces warning of Severity " -TestCases $testCases_AutomaticVariables { param ($VariableName, $ExpectedSeverity) - $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "`$${VariableName} = 'foo'" -ExcludeRule PSUseDeclaredVarsMoreThanAssignments + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "`$${VariableName} = 'foo'" -ExcludeRule PSUseDeclaredVarsMoreThanAssignments $warnings.Count | Should -Be 1 $warnings.Severity | Should -Be $ExpectedSeverity $warnings.RuleName | Should -Be $ruleName } - It "Using Variable as parameter name produces warning of Severity error" -TestCases $testCases_AutomaticVariables { + It "Using Variable as foreach assignment produces warning of Severity " -TestCases $testCases_AutomaticVariables { + param ($VariableName, $ExpectedSeverity) + + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "foreach (`$$VariableName in `$foo) {}" + $warnings.Count | Should -Be 1 + $warnings.Severity | Should -Be $ExpectedSeverity + $warnings.RuleName | Should -Be $ruleName + } + + It "Using Variable as parameter name produces warning of Severity " -TestCases $testCases_AutomaticVariables { param ($VariableName, $ExpectedSeverity) [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo{Param(`$$VariableName)}" -ExcludeRule PSReviewUnusedParameter @@ -80,7 +89,7 @@ Describe "AvoidAssignmentToAutomaticVariables" { $warnings.RuleName | Should -Be $ruleName } - It "Using Variable as parameter name in param block produces warning of Severity error" -TestCases $testCases_AutomaticVariables { + It "Using Variable as parameter name in param block produces warning of Severity " -TestCases $testCases_AutomaticVariables { param ($VariableName, $ExpectedSeverity) [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo(`$$VariableName){}" From 56c6ea1277af1851193c3ebe02c0dabb81308b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:16:24 +0100 Subject: [PATCH 073/130] Invoke-ScriptAnalyzer: Stream diagnostics instead of batching (#2062) Before this commit, diagnostics for all analyzed files in this pipeline step were batched and logged at once. With this commit, diagnostics are rendered immediately. --- .../Commands/InvokeScriptAnalyzerCommand.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 38a2ad9fe..18a632874 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -434,29 +434,39 @@ private void ProcessInput() WriteToOutput(RunAnalysis()); } - private List RunAnalysis() + private IEnumerable RunAnalysis() { if (!IsFileParameterSet()) { - return ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _); + foreach (var record in ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _)) + { + yield return record; + } + yield break; } - var diagnostics = new List(); - foreach (string path in this.processedPaths) + foreach (var path in this.processedPaths) { + if (!ShouldProcess(path, $"Analyzing path with Fix={this.fix} and Recurse={this.recurse}")) + { + continue; + } + if (fix) { - ShouldProcess(path, $"Analyzing and fixing path with Recurse={this.recurse}"); - diagnostics.AddRange(ScriptAnalyzer.Instance.AnalyzeAndFixPath(path, this.ShouldProcess, this.recurse)); + foreach (var record in ScriptAnalyzer.Instance.AnalyzeAndFixPath(path, this.ShouldProcess, this.recurse)) + { + yield return record; + } } else { - ShouldProcess(path, $"Analyzing path with Recurse={this.recurse}"); - diagnostics.AddRange(ScriptAnalyzer.Instance.AnalyzePath(path, this.ShouldProcess, this.recurse)); + foreach (var record in ScriptAnalyzer.Instance.AnalyzePath(path, this.ShouldProcess, this.recurse)) + { + yield return record; + } } } - - return diagnostics; } private void WriteToOutput(IEnumerable diagnosticRecords) From 9fa10d4b040531fd70b05cbd779fef695a31a88d Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 25 Feb 2025 00:48:02 +0000 Subject: [PATCH 074/130] PSUseConsistentWhitespace: Correctly fix whitespace between command parameters when parameter value spans multiple lines (#2064) * Added test coverage for the scenarios of parameter values spanning multiple lines * Fix erroneous double-negative in test name * As the correction takes place on the whitespace between two extents, the correction should begin on the last line of the left extent and end on the first line of the right extent --- Rules/UseConsistentWhitespace.cs | 4 +- Tests/Rules/UseConsistentWhitespace.tests.ps1 | 38 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Rules/UseConsistentWhitespace.cs b/Rules/UseConsistentWhitespace.cs index a062e5d3f..de4c0e515 100644 --- a/Rules/UseConsistentWhitespace.cs +++ b/Rules/UseConsistentWhitespace.cs @@ -421,8 +421,8 @@ private IEnumerable FindParameterViolations(Ast ast) { int numberOfRedundantWhiteSpaces = rightExtent.StartColumnNumber - expectedStartColumnNumberOfRightExtent; var correction = new CorrectionExtent( - startLineNumber: leftExtent.StartLineNumber, - endLineNumber: leftExtent.EndLineNumber, + startLineNumber: leftExtent.EndLineNumber, + endLineNumber: rightExtent.StartLineNumber, startColumnNumber: leftExtent.EndColumnNumber + 1, endColumnNumber: leftExtent.EndColumnNumber + 1 + numberOfRedundantWhiteSpaces, text: string.Empty, diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1 index 30d8cce57..362025d0d 100644 --- a/Tests/Rules/UseConsistentWhitespace.tests.ps1 +++ b/Tests/Rules/UseConsistentWhitespace.tests.ps1 @@ -535,7 +535,7 @@ bar -h i ` Invoke-ScriptAnalyzer -ScriptDefinition "$def" -Settings $settings | Should -Be $null } - It "Should not find no violation if there is always 1 space between parameters except when using colon syntax" { + It "Should not find a violation if there is always 1 space between parameters except when using colon syntax" { $def = 'foo -bar $baz @splattedVariable -bat -parameterName:$parameterValue' Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -Be $null } @@ -585,6 +585,42 @@ bar -h i ` Should -Be "$expected" } + It "Should fix script when a parameter value is a script block spanning multiple lines" { + $def = {foo { + bar +} -baz} + + $expected = {foo { + bar +} -baz} + Invoke-Formatter -ScriptDefinition "$def" -Settings $settings | + Should -Be "$expected" + } + + It "Should fix script when a parameter value is a hashtable spanning multiple lines" { + $def = {foo @{ + a = 1 +} -baz} + + $expected = {foo @{ + a = 1 +} -baz} + Invoke-Formatter -ScriptDefinition "$def" -Settings $settings | + Should -Be "$expected" + } + + It "Should fix script when a parameter value is an array spanning multiple lines" { + $def = {foo @( + 1 +) -baz} + + $expected = {foo @( + 1 +) -baz} + Invoke-Formatter -ScriptDefinition "$def" -Settings $settings | + Should -Be "$expected" + } + It "Should fix script when redirects are involved and whitespace is not consistent" { # Related to Issue #2000 $def = 'foo 3>&1 1>$null 2>&1' From 4f0c07aea1660315cb0c2976347c6a22d5d46b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:56:58 +0100 Subject: [PATCH 075/130] Use -NoProfile when invoking pwsh in Pester tests (#2061) --- Tests/Engine/InvokeScriptAnalyzer.tests.ps1 | 2 +- build.psm1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 51b16f210..d37ff5561 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -604,7 +604,7 @@ Describe "-ReportSummary switch" { } It "prints the correct report summary using the -NoReportSummary switch" { - $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary" + $result = & $pwshExe -NoProfile -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary" "$result" | Should -BeLike $reportSummaryFor1Warning } diff --git a/build.psm1 b/build.psm1 index 6e3ad2edf..3dc9c0ec0 100644 --- a/build.psm1 +++ b/build.psm1 @@ -351,7 +351,7 @@ function Test-ScriptAnalyzer } else { $powershell = (Get-Process -id $PID).MainModule.FileName - & ${powershell} -Command $scriptBlock + & ${powershell} -NoProfile -Command $scriptBlock } } finally { From d6eb35e177b57d8865174071851db8f93f236e06 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 25 Feb 2025 01:00:06 +0000 Subject: [PATCH 076/130] PSAvoidTrailingWhitespace: Rule not applied when using formatter + single character lines with trailing whitespace are truncated (#1993) * Formatter: Added PSAvoidTrailingWhitespace to the list of rules considered by the formatter * Rules/AvoidTrailingWhitespace: Fixed issue where lines with a single character, followed by multiple white-spaces were truncated when fixed/formatted * Tests/Rules/AvoidTrailingWhitespace: Added test for usage of Invoke-Formatter with PSAvoidTrailingWhitespace and also checking that single-character lines that have trailing whitespace are not removed --------- Co-authored-by: Christoph Bergmeister --- Engine/Formatter.cs | 1 + Rules/AvoidTrailingWhitespace.cs | 2 +- Tests/Rules/AvoidTrailingWhitespace.tests.ps1 | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index 5a93854c5..a6a25f0fb 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -47,6 +47,7 @@ public static string Format( "PSAvoidUsingDoubleQuotesForConstantString", "PSAvoidSemicolonsAsLineTerminators", "PSAvoidExclaimOperator", + "PSAvoidTrailingWhitespace", }; var text = new EditableText(scriptDefinition); diff --git a/Rules/AvoidTrailingWhitespace.cs b/Rules/AvoidTrailingWhitespace.cs index 47f576d5b..a7567d6e6 100644 --- a/Rules/AvoidTrailingWhitespace.cs +++ b/Rules/AvoidTrailingWhitespace.cs @@ -54,7 +54,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) } int startColumnOfTrailingWhitespace = 1; - for (int i = line.Length - 2; i > 0; i--) + for (int i = line.Length - 2; i >= 0; i--) { if (line[i] != ' ' && line[i] != '\t') { diff --git a/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 b/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 index f130a77c5..e5a45f0d3 100644 --- a/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 +++ b/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 @@ -9,6 +9,9 @@ BeforeAll { $settings = @{ IncludeRules = @($ruleName) + Rules = @{ + $ruleName = @{} + } } } @@ -34,4 +37,26 @@ Describe "AvoidTrailingWhitespace" { $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings Test-CorrectionExtentFromContent $def $violations 1 $Whitespace '' } + + It 'Should be used by Invoke-Formatter, when in settings, replacing trailing ' -TestCases $testCases { + param ( + [string] $Whitespace + ) + # Test also guards against regression where single-character lines, with trailing whitespace + # would be removed entirely. See issues #1757, #1992 + $def = @" +Function Get-Example { + 'Example'$Whitespace +}$Whitespace +"@ + + $expected = @" +Function Get-Example { + 'Example' } +"@ + $formatted = Invoke-Formatter -ScriptDefinition $def -Settings $settings + $formatted | Should -Be $expected + } + +} \ No newline at end of file From d30f10f06e46e5e6d61ac032a8798d06fef7c0a8 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 25 Feb 2025 19:43:04 +0000 Subject: [PATCH 077/130] PSUseConsistentWhitespace: When checking separators, ignore whitespace violations between a separator and a comment (#2065) --- Rules/UseConsistentWhitespace.cs | 1 + Tests/Rules/UseConsistentWhitespace.tests.ps1 | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Rules/UseConsistentWhitespace.cs b/Rules/UseConsistentWhitespace.cs index de4c0e515..e6d4cff99 100644 --- a/Rules/UseConsistentWhitespace.cs +++ b/Rules/UseConsistentWhitespace.cs @@ -451,6 +451,7 @@ private IEnumerable FindSeparatorViolations(TokenOperations to { return node.Next != null && node.Next.Value.Kind != TokenKind.NewLine + && node.Next.Value.Kind != TokenKind.Comment && node.Next.Value.Kind != TokenKind.EndOfInput // semicolon can be followed by end of input && !IsPreviousTokenApartByWhitespace(node.Next); }; diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1 index 362025d0d..03a6fbc35 100644 --- a/Tests/Rules/UseConsistentWhitespace.tests.ps1 +++ b/Tests/Rules/UseConsistentWhitespace.tests.ps1 @@ -514,6 +514,48 @@ if ($true) { Get-Item ` } } + Context "CheckSeparator" { + BeforeAll { + $ruleConfiguration.CheckInnerBrace = $false + $ruleConfiguration.CheckOpenBrace = $false + $ruleConfiguration.CheckOpenParen = $false + $ruleConfiguration.CheckOperator = $false + $ruleConfiguration.CheckPipe = $false + $ruleConfiguration.CheckSeparator = $true + } + + It "Should find a violation if there is no space after a comma" { + $def = '$Array = @(1,2)' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -HaveCount 1 + } + + It "Should not find a violation if there is a space after a comma" { + $def = '$Array = @(1, 2)' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -Be $null + } + + It "Should not find a violation if there is a new-line after a comma" { + $def = @' +$Array = @( + 1, + 2 +) +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -Be $null + } + + It "Should not find a violation if there is a comment after the separator" { + $def = @' +$Array = @( + 'foo', # Comment Line 1 + 'FizzBuzz' # Comment Line 2 +) +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + } + Context "CheckParameter" { BeforeAll { From bcec9f6bc5df9ca823993a5ad8e77b08d0ff2444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:02:17 +0100 Subject: [PATCH 078/130] Invoke-ScriptAnalyzer: Print summary only once per invocation (#2063) --- .../Commands/InvokeScriptAnalyzerCommand.cs | 104 +++++++----------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 18a632874..bcc6be9de 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -34,7 +34,9 @@ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter #region Private variables List processedPaths; - private int totalDiagnosticCount = 0; + // initialize to zero for all severity enum values + private Dictionary diagnosticCounts = + Enum.GetValues(typeof(DiagnosticSeverity)).Cast().ToDictionary(s => s, _ => 0); #endregion // Private variables #region Parameters @@ -414,8 +416,36 @@ protected override void EndProcessing() ScriptAnalyzer.Instance.CleanUp(); base.EndProcessing(); - if (EnableExit) { - this.Host.SetShouldExit(totalDiagnosticCount); + var infoCount = diagnosticCounts[DiagnosticSeverity.Information]; + var warningCount = diagnosticCounts[DiagnosticSeverity.Warning]; + var errorCount = diagnosticCounts[DiagnosticSeverity.Error]; + var parseErrorCount = diagnosticCounts[DiagnosticSeverity.ParseError]; + + if (ReportSummary.IsPresent) + { + var numberOfRuleViolations = infoCount + warningCount + errorCount; + if (numberOfRuleViolations == 0) + { + Host.UI.WriteLine("0 rule violations found."); + } + else + { + var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; + var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; + if (warningCount + errorCount == 0) + { + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); + } + else + { + ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); + } + } + } + + if (EnableExit) + { + this.Host.SetShouldExit(diagnosticCounts.Values.Sum()); } } @@ -431,7 +461,15 @@ protected override void StopProcessing() private void ProcessInput() { - WriteToOutput(RunAnalysis()); + foreach (var diagnostic in RunAnalysis()) + { + diagnosticCounts[diagnostic.Severity]++; + + foreach (var logger in ScriptAnalyzer.Instance.Loggers) + { + logger.LogObject(diagnostic, this); + } + } } private IEnumerable RunAnalysis() @@ -469,64 +507,6 @@ private IEnumerable RunAnalysis() } } - private void WriteToOutput(IEnumerable diagnosticRecords) - { - var errorCount = 0; - var warningCount = 0; - var infoCount = 0; - var parseErrorCount = 0; - - foreach (DiagnosticRecord diagnostic in diagnosticRecords) - { - foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) - { - logger.LogObject(diagnostic, this); - } - - totalDiagnosticCount++; - - switch (diagnostic.Severity) - { - case DiagnosticSeverity.Information: - infoCount++; - break; - case DiagnosticSeverity.Warning: - warningCount++; - break; - case DiagnosticSeverity.Error: - errorCount++; - break; - case DiagnosticSeverity.ParseError: - parseErrorCount++; - break; - default: - throw new ArgumentOutOfRangeException(nameof(diagnostic.Severity), $"Severity '{diagnostic.Severity}' is unknown"); - } - } - - if (ReportSummary.IsPresent) - { - var numberOfRuleViolations = infoCount + warningCount + errorCount; - if (numberOfRuleViolations == 0) - { - Host.UI.WriteLine("0 rule violations found."); - } - else - { - var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; - var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; - if (warningCount + errorCount == 0) - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); - } - else - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); - } - } - } - } - private void ProcessPath() { Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path); From 45a82a1cf05f71ed6f094c7ea9b845db20725189 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:04:40 -0800 Subject: [PATCH 079/130] Upgrade to .NET 8 since .NET 6 is past EOL (#2073) --- .pipelines/PSScriptAnalyzer-Official.yml | 5 +---- Engine/Engine.csproj | 10 +++++----- Rules/Rules.csproj | 6 +++--- build.psm1 | 2 +- global.json | 3 ++- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index 971cdc351..b3f2c2cb4 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -80,10 +80,7 @@ extends: inputs: packageType: sdk useGlobalJson: true - - pwsh: | - Register-PSRepository -Name CFS -SourceLocation "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/nuget/v2" -InstallationPolicy Trusted - Install-Module -Repository CFS -Name Microsoft.PowerShell.PSResourceGet - ./tools/installPSResources.ps1 -PSRepository CFS + - pwsh: ./tools/installPSResources.ps1 -PSRepository CFS displayName: Install PSResources - pwsh: ./build.ps1 -Configuration Release -All displayName: Build diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index 3025c9a08..e96f5c9d9 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -2,7 +2,7 @@ $(ModuleVersion) - net6;net462 + net8;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer $(ModuleVersion) Engine @@ -18,11 +18,11 @@ portable - + $(DefineConstants);CORECLR - + @@ -69,10 +69,10 @@ - + - + $(DefineConstants);PSV7;CORECLR diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 8fef9e969..6e485c4e9 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -2,7 +2,7 @@ $(ModuleVersion) - net6;net462 + net8;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules $(ModuleVersion) Rules @@ -16,7 +16,7 @@ - + @@ -61,7 +61,7 @@ $(DefineConstants);PSV3;PSV4 - + $(DefineConstants);PSV7;CORECLR diff --git a/build.psm1 b/build.psm1 index 3dc9c0ec0..c83fa2664 100644 --- a/build.psm1 +++ b/build.psm1 @@ -144,7 +144,7 @@ function Start-ScriptAnalyzerBuild $framework = 'net462' if ($PSVersion -eq 7) { - $framework = 'net6' + $framework = 'net8' } # build the appropriate assembly diff --git a/global.json b/global.json index 6e6e5445c..37239305b 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "6.0.427" + "version": "8.0.406", + "rollForward": "latestFeature" } } From e93af95df0146564d5b74fa471aaa4268b3a1dac Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:52:24 -0700 Subject: [PATCH 080/130] Add GitHub Actions Ubuntu's dotnet path (#2080) Since they're not setting up PATH correctly now. --- build.psm1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.psm1 b/build.psm1 index c83fa2664..ba839fab1 100644 --- a/build.psm1 +++ b/build.psm1 @@ -563,6 +563,13 @@ function Get-DotnetExe $script:DotnetExe = $dotnetHuntPath return $dotnetHuntPath } + + $dotnetHuntPath = "/usr/share/dotnet/dotnet" + Write-Verbose -Verbose "checking non-Windows $dotnetHuntPath" + if ( test-path $dotnetHuntPath ) { + $script:DotnetExe = $dotnetHuntPath + return $dotnetHuntPath + } } Write-Warning "Could not find dotnet executable" From d3a0f8e7ebc811aca1a9f35b652cc1b2805f5e24 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 13 Mar 2025 16:59:10 +0000 Subject: [PATCH 081/130] Update README.md with recent upgrade to .NET 8 (#2076) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 716224c7c..2ceffeb23 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ To install **PSScriptAnalyzer** from source code: ### Requirements -- [Latest .NET 6.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +- [Latest .NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) * If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows. * Optionally but recommended for development: [Visual Studio 2017/2019](https://www.visualstudio.com/downloads) - [Pester v5 PowerShell module, available on PowerShell Gallery](https://github.com/pester/Pester) From 205aed53fb4b6279a8b8bf725c855d8c9962d1bb Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 13 Mar 2025 16:59:48 +0000 Subject: [PATCH 082/130] Update CHANGELOG.MD with 1.23.0 release notes (#2078) In the past, we've always kept them in sync --- CHANGELOG.MD | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 6afc5be8e..b948c475a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,27 @@ # CHANGELOG +## [1.23.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.23.0) - 2024-10-09 + +## What's Changed +* Adding OneBranch pipeline YAML config file for OSS_Microsoft_PSSA-Official by @adityapatwardhan in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 +* Update format and grammar of AvoidUsingAllowUnencryptedAuthentication by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1974 +* Move to OneBranch Signing and SBOM generation by @TravisEz13 in https://github.com/PowerShell/PSScriptAnalyzer/pull/1982 +* Sync rule docs changes by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1985 +* Sync docs changes from MicrosoftDocs/PowerShell-Docs-Modules#213 by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1987 +* Update CHANGELOG for 1.22.0 release by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1990 +* Update Code of Conduct by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2002 +* Update default type definition of `RuleInfo` by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2011 +* PSUseConsistentWhitespace: Handle redirect operators which are not in stream order by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2001 +* Setup GitHub Actions CI by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2018 +* Setup new OneBranch pipeline by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2027 +* Bump SMA version by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2028 +* Package updates by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2030 +* v1.23.0: Update version for new release by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2032 +* Migrate release pipeline to DeployBox by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2033 + +## New Contributors +* @adityapatwardhan made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 + ## [1.22.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.22.0) - 2024-03-05 Minimum required version when using PowerShell 7 is now `7.2.11`. From 04e27d00affdfd9d2521b62a62f7731d7ce1ab0a Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 13 Mar 2025 17:00:05 +0000 Subject: [PATCH 083/130] Bring back Codespaces (#2077) --- .devcontainer/Dockerfile | 6 ++++++ .devcontainer/devcontainer.json | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..cc17c531c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +FROM mcr.microsoft.com/dotnet/sdk:8.0 + +RUN pwsh --command Install-Module platyPS,Pester -Force \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..dbbc9c7ee --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +// For format details, see https://aka.ms/vscode-remote/devcontainer.json +{ + "name": "C# (.NET 8.0)", + "dockerFile": "Dockerfile", + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "pwsh" + }, + "extensions": [ + "ms-dotnettools.csharp", + "ms-vscode.powershell" + ] + } + } +} \ No newline at end of file From d4fbdc3c1698e0f2dce27e082d86660964d3a2f0 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Thu, 13 Mar 2025 17:02:15 +0000 Subject: [PATCH 084/130] Update SMA version to 7.4.7 (#2075) Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e43bd7dfd..77cb7a272 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + From 1d394eef2696b21c413d145f4f15c14a405fc4d8 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 13 Mar 2025 17:57:52 +0000 Subject: [PATCH 085/130] PSReviewUnusedParameter false positive for ValueFromPipeline (#2072) * Check for ValueFromPipeline * Check whether each scriptblock being analysed has a process block that directly contains variable usage of $_ or $PSItem. Then when we encounted a parameter with ValueFromPipeline set, we consider whether we saw usage within a process block by automatic variable. * Amend tests to consider process block * Update Rules/ReviewUnusedParameter.cs Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> * Update Rules/ReviewUnusedParameter.cs --------- Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Rules/ReviewUnusedParameter.cs | 32 +++++++++++++- Tests/Rules/ReviewUnusedParameter.tests.ps1 | 48 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Rules/ReviewUnusedParameter.cs b/Rules/ReviewUnusedParameter.cs index f13584fed..9b727e7fc 100644 --- a/Rules/ReviewUnusedParameter.cs +++ b/Rules/ReviewUnusedParameter.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Management.Automation.Language; using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions; #if !CORECLR using System.ComponentModel.Composition; #endif @@ -97,11 +98,40 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // find all declared parameters IEnumerable parameterAsts = scriptBlockAst.FindAll(oneAst => oneAst is ParameterAst, false); + // does the scriptblock have a process block where either $PSItem or $_ is referenced + bool hasProcessBlockWithPSItemOrUnderscore = false; + if (scriptBlockAst.ProcessBlock != null) + { + IDictionary processBlockVariableCount = GetVariableCount(scriptBlockAst.ProcessBlock); + processBlockVariableCount.TryGetValue("_", out int underscoreVariableCount); + processBlockVariableCount.TryGetValue("psitem", out int psitemVariableCount); + if (underscoreVariableCount > 0 || psitemVariableCount > 0) + { + hasProcessBlockWithPSItemOrUnderscore = true; + } + } + // list all variables IDictionary variableCount = GetVariableCount(scriptBlockAst); foreach (ParameterAst parameterAst in parameterAsts) { + // Check if the parameter has the ValueFromPipeline attribute + NamedAttributeArgumentAst valueFromPipeline = (NamedAttributeArgumentAst)parameterAst.Find( + valFromPipelineAst => valFromPipelineAst is NamedAttributeArgumentAst namedAttrib && string.Equals( + namedAttrib.ArgumentName, "ValueFromPipeline", + StringComparison.OrdinalIgnoreCase + ), + false + ); + // If the parameter has the ValueFromPipeline attribute and the scriptblock has a process block with + // $_ or $PSItem usage, then the parameter is considered used + if (valueFromPipeline != null && valueFromPipeline.GetValue() && hasProcessBlockWithPSItemOrUnderscore) + + { + continue; + } + // there should be at least two usages of the variable since the parameter declaration counts as one variableCount.TryGetValue(parameterAst.Name.VariablePath.UserPath, out int variableUsageCount); if (variableUsageCount >= 2) @@ -220,7 +250,7 @@ public string GetSourceName() /// The scriptblock ast to scan /// Previously generated data. New findings are added to any existing dictionary if present /// a dictionary including all variables in the scriptblock and their count - IDictionary GetVariableCount(ScriptBlockAst ast, Dictionary data = null) + IDictionary GetVariableCount(Ast ast, Dictionary data = null) { Dictionary content = data; if (null == data) diff --git a/Tests/Rules/ReviewUnusedParameter.tests.ps1 b/Tests/Rules/ReviewUnusedParameter.tests.ps1 index 59d8b160d..9e4202dcf 100644 --- a/Tests/Rules/ReviewUnusedParameter.tests.ps1 +++ b/Tests/Rules/ReviewUnusedParameter.tests.ps1 @@ -20,6 +20,30 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 2 } + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to false and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $false)] $Param1) process {$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to false and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $false)] $Param1) process {$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to true and `$_ usage outside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) $_}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to true and `$PSItem usage outside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) $PSItem}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + It "has 1 violation - scriptblock with 1 unused parameter" { $ScriptDefinition = '{ param ($Param1) }' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName @@ -59,6 +83,30 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 0 } + It "has no violation - function with 1 parameter with ValueFromPipeline explictly set to true and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) process {$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline explictly set to true and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) process {$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline implicitly set to true and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline)] $Param1) process{$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline implicitly set to true and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline)] $Param1) process{$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + It "has no violations when using PSBoundParameters" { $ScriptDefinition = 'function Bound { param ($Param1) Get-Foo @PSBoundParameters }' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName From 8862e51e646a3b408377337be213411c93de92a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Kafka?= <6414091+MatejKafka@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:14 +0100 Subject: [PATCH 086/130] Invoke-ScriptAnalyzer: Include parse errors in reported error count (#2069) Previously, parse error were not reported in the summary. With this commit, the exit code from -EnableExit matches the number of reported issues. Co-authored-by: Christoph Bergmeister --- .../Commands/InvokeScriptAnalyzerCommand.cs | 33 +++++++++---------- Tests/Engine/InvokeScriptAnalyzer.tests.ps1 | 2 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index bcc6be9de..a444327e0 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -416,36 +416,35 @@ protected override void EndProcessing() ScriptAnalyzer.Instance.CleanUp(); base.EndProcessing(); - var infoCount = diagnosticCounts[DiagnosticSeverity.Information]; - var warningCount = diagnosticCounts[DiagnosticSeverity.Warning]; - var errorCount = diagnosticCounts[DiagnosticSeverity.Error]; - var parseErrorCount = diagnosticCounts[DiagnosticSeverity.ParseError]; + var diagnosticCount = diagnosticCounts.Values.Sum(); if (ReportSummary.IsPresent) { - var numberOfRuleViolations = infoCount + warningCount + errorCount; - if (numberOfRuleViolations == 0) + if (diagnosticCount == 0) { Host.UI.WriteLine("0 rule violations found."); } else { - var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; - var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; - if (warningCount + errorCount == 0) - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); - } - else - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); - } + var infoCount = diagnosticCounts[DiagnosticSeverity.Information]; + var warningCount = diagnosticCounts[DiagnosticSeverity.Warning]; + var errorCount = diagnosticCounts[DiagnosticSeverity.Error] + diagnosticCounts[DiagnosticSeverity.ParseError]; + var severeDiagnosticCount = diagnosticCount - infoCount; + + var colorPropertyPrefix = severeDiagnosticCount == 0 ? "Warning" : "Error"; + var pluralS = diagnosticCount > 1 ? "s" : string.Empty; + ConsoleHostHelper.DisplayMessageUsingSystemProperties( + Host, colorPropertyPrefix + "ForegroundColor", colorPropertyPrefix + "BackgroundColor", + $"{diagnosticCount} rule violation{pluralS} found. Severity distribution: " + + $"{DiagnosticSeverity.Error} = {errorCount}, " + + $"{DiagnosticSeverity.Warning} = {warningCount}, " + + $"{DiagnosticSeverity.Information} = {infoCount}"); } } if (EnableExit) { - this.Host.SetShouldExit(diagnosticCounts.Values.Sum()); + this.Host.SetShouldExit(diagnosticCount); } } diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index d37ff5561..b930c9980 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -600,7 +600,7 @@ Describe "-ReportSummary switch" { $pwshExe = 'powershell' } - $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*' + $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*' } It "prints the correct report summary using the -NoReportSummary switch" { From bbf258bfa9c3b5faf02809bd21784a71458fdb1c Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:39:14 -0700 Subject: [PATCH 087/130] Test PowerShell Preview in CI (#2070) Since the Daily no longer exists. --- .github/workflows/ci-test.yml | 29 ++++++++++++++++++++++++----- build.ps1 | 8 +++++++- build.psm1 | 22 ++++++++++++++++++++-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index d681191c8..5469c54a5 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -31,24 +31,43 @@ jobs: shell: pwsh - name: Build - run: ./build.ps1 -Configuration Release -All + run: ./build.ps1 -Configuration Release -All -Verbose shell: pwsh - name: Package - run: ./build.ps1 -BuildNupkg + run: ./build.ps1 -BuildNupkg -Verbose shell: pwsh - name: Test - run: ./build.ps1 -Test + run: ./build.ps1 -Test -Verbose shell: pwsh - name: Test Windows PowerShell + if: matrix.os == 'windows-latest' run: | Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck - ./build.ps1 -Test - if: matrix.os == 'windows-latest' + ./build.ps1 -Test -Verbose shell: powershell + - name: Download PowerShell install script + uses: actions/checkout@v4 + with: + repository: PowerShell/PowerShell + path: pwsh + sparse-checkout: tools/install-powershell.ps1 + sparse-checkout-cone-mode: false + + - name: Install preview + continue-on-error: true + run: ./pwsh/tools/install-powershell.ps1 -Preview -Destination ./preview + shell: pwsh + + - name: Test preview + run: | + $PwshPreview = if ($isWindows) { "./preview/pwsh.exe" } else { "./preview/pwsh" } + ./build.ps1 -Test -WithPowerShell:$PwshPreview -Verbose + shell: pwsh + - name: Upload build artifacts uses: actions/upload-artifact@v4 if: always() diff --git a/build.ps1 b/build.ps1 index bbc6c505a..d6d661bf6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -32,6 +32,7 @@ param( [Parameter(ParameterSetName='Test')] [switch] $InProcess, + [string] $WithPowerShell, [Parameter(ParameterSetName='BuildAll')] [switch] $Catalog, @@ -85,7 +86,12 @@ END { Start-CreatePackage } "Test" { - Test-ScriptAnalyzer -InProcess:$InProcess + $testArgs = @{ + InProcess = $InProcess + WithPowerShell = $WithPowerShell + Verbose = $verboseWanted + } + Test-ScriptAnalyzer @testArgs return } default { diff --git a/build.psm1 b/build.psm1 index ba839fab1..98a87af0d 100644 --- a/build.psm1 +++ b/build.psm1 @@ -308,7 +308,10 @@ function New-Catalog function Test-ScriptAnalyzer { [CmdletBinding()] - param ( [switch] $InProcess ) + param ( + [switch] $InProcess, + [string] $WithPowerShell + ) END { # versions 3 and 4 don't understand versioned module paths, so we need to rename the directory of the version to @@ -347,11 +350,19 @@ function Test-ScriptAnalyzer $analyzerPsd1Path = Join-Path -Path $script:destinationDir -ChildPath "$analyzerName.psd1" $scriptBlock = [scriptblock]::Create("Import-Module '$analyzerPsd1Path'; Invoke-Pester -Path $testScripts -CI") if ( $InProcess ) { + Write-Verbose "Testing with PowerShell $($PSVersionTable.PSVersion)" & $scriptBlock } + elseif ( $WithPowerShell ) { + $pwshVersion = & $WithPowerShell --version + Write-Verbose "Testing with $pwshVersion" + & $WithPowerShell -Command $scriptBlock + } else { $powershell = (Get-Process -id $PID).MainModule.FileName - & ${powershell} -NoProfile -Command $scriptBlock + $pwshVersion = & $powershell --version + Write-Verbose "Testing with $pwshVersion" + & $powershell -NoProfile -Command $scriptBlock } } finally { @@ -555,6 +566,13 @@ function Get-DotnetExe $script:DotnetExe = $dotnetHuntPath return $dotnetHuntPath } + + $dotnetHuntPath = "C:\Program Files\dotnet\dotnet.exe" + Write-Verbose -Verbose "checking Windows $dotnetHuntPath" + if ( test-path $dotnetHuntPath ) { + $script:DotnetExe = $dotnetHuntPath + return $dotnetHuntPath + } } else { $dotnetHuntPath = "$HOME/.dotnet/dotnet" From 37ddc1c99ca0489dcfdc4a33cb47560683443d54 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Thu, 13 Mar 2025 15:22:43 -0400 Subject: [PATCH 088/130] Add UseConsistentCasing (#1704) * Correct casing for keywords and operators * Change which command I use in tests because 'more' was a function in PS5.1 --- Engine/Generic/DiagnosticRecord.cs | 7 +- Rules/Strings.resx | 68 +++---- Rules/UseCorrectCasing.cs | 241 +++++++++++++++---------- Tests/Rules/UseCorrectCasing.tests.ps1 | 73 +++++--- docs/Rules/UseCorrectCasing.md | 21 ++- 5 files changed, 256 insertions(+), 154 deletions(-) diff --git a/Engine/Generic/DiagnosticRecord.cs b/Engine/Generic/DiagnosticRecord.cs index 0673e1391..41eb86a05 100644 --- a/Engine/Generic/DiagnosticRecord.cs +++ b/Engine/Generic/DiagnosticRecord.cs @@ -74,7 +74,7 @@ public string ScriptPath } /// - /// Returns the rule id for this record + /// Returns the rule suppression id for this record /// public string RuleSuppressionID { @@ -88,7 +88,7 @@ public string RuleSuppressionID /// public IEnumerable SuggestedCorrections { - get { return suggestedCorrections; } + get { return suggestedCorrections; } set { suggestedCorrections = value; } } @@ -100,7 +100,7 @@ public IEnumerable SuggestedCorrections public DiagnosticRecord() { } - + /// /// DiagnosticRecord: The constructor for DiagnosticRecord class that takes in suggestedCorrection /// @@ -108,6 +108,7 @@ public DiagnosticRecord() /// The place in the script this diagnostic refers to /// The name of the rule that created this diagnostic /// The severity of this diagnostic + /// The rule suppression ID of this diagnostic /// The full path of the script file being analyzed /// The correction suggested by the rule to replace the extent text public DiagnosticRecord( diff --git a/Rules/Strings.resx b/Rules/Strings.resx index fc92f526d..260214967 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1,17 +1,17 @@  - @@ -1096,7 +1096,7 @@ Use exact casing of cmdlet/function/parameter name. - For better readability and consistency, use the exact casing of the cmdlet/function/parameter. + For better readability and consistency, use consistent casing. Function/Cmdlet '{0}' does not match its exact casing '{1}'. @@ -1104,6 +1104,15 @@ UseCorrectCasing + + Keyword '{0}' does not match the expected case '{1}'. + + + Operator '{0}' does not match the expected case '{1}'. + + + Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'. + Use process block for command that accepts input from pipeline. @@ -1188,9 +1197,6 @@ AvoidUsingBrokenHashAlgorithms - - Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'. - AvoidExclaimOperator diff --git a/Rules/UseCorrectCasing.cs b/Rules/UseCorrectCasing.cs index 9d3abd098..a9fbce198 100644 --- a/Rules/UseCorrectCasing.cs +++ b/Rules/UseCorrectCasing.cs @@ -22,88 +22,122 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif public class UseCorrectCasing : ConfigurableRule { + + /// If true, require the case of all operators to be lowercase. + [ConfigurableRuleProperty(defaultValue: true)] + public bool CheckOperator { get; set; } + + /// If true, require the case of all keywords to be lowercase. + [ConfigurableRuleProperty(defaultValue: true)] + public bool CheckKeyword { get; set; } + + /// If true, require the case of all commands to match their actual casing. + [ConfigurableRuleProperty(defaultValue: true)] + public bool CheckCommands { get; set; } + + private TokenFlags operators = TokenFlags.BinaryOperator | TokenFlags.UnaryOperator; + /// /// AnalyzeScript: Analyze the script to check if cmdlet alias is used. /// public override IEnumerable AnalyzeScript(Ast ast, string fileName) { - if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - - IEnumerable commandAsts = ast.FindAll(testAst => testAst is CommandAst, true); + if (ast is null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - // Iterates all CommandAsts and check the command name. - foreach (CommandAst commandAst in commandAsts) + if (CheckOperator || CheckKeyword) { - string commandName = commandAst.GetCommandName(); - - // Handles the exception caused by commands like, {& $PLINK $args 2> $TempErrorFile}. - // You can also review the remark section in following document, - // MSDN: CommandAst.GetCommandName Method - if (commandName == null) + // Iterate tokens to look for the keywords and operators + for (int i = 0; i < Helper.Instance.Tokens.Length; i++) { - continue; - } + Token token = Helper.Instance.Tokens[i]; - var commandInfo = Helper.Instance.GetCommandInfo(commandName); - if (commandInfo == null || commandInfo.CommandType == CommandTypes.ExternalScript || commandInfo.CommandType == CommandTypes.Application) - { - continue; + if (CheckKeyword && ((token.TokenFlags & TokenFlags.Keyword) != 0)) + { + string correctCase = token.Text.ToLowerInvariant(); + if (!token.Text.Equals(correctCase, StringComparison.Ordinal)) + { + yield return GetDiagnosticRecord(token, fileName, correctCase, Strings.UseCorrectCasingKeywordError); + } + continue; + } + + if (CheckOperator && ((token.TokenFlags & operators) != 0)) + { + string correctCase = token.Text.ToLowerInvariant(); + if (!token.Text.Equals(correctCase, StringComparison.Ordinal)) + { + yield return GetDiagnosticRecord(token, fileName, correctCase, Strings.UseCorrectCasingOperatorError); + } + } } + } - var shortName = commandInfo.Name; - var fullyqualifiedName = $"{commandInfo.ModuleName}\\{shortName}"; - var isFullyQualified = commandName.Equals(fullyqualifiedName, StringComparison.OrdinalIgnoreCase); - var correctlyCasedCommandName = isFullyQualified ? fullyqualifiedName : shortName; + if (CheckCommands) + { + // Iterate command ASTs for command and parameter names + IEnumerable commandAsts = ast.FindAll(testAst => testAst is CommandAst, true); - if (!commandName.Equals(correctlyCasedCommandName, StringComparison.Ordinal)) + // Iterates all CommandAsts and check the command name. + foreach (CommandAst commandAst in commandAsts) { - yield return new DiagnosticRecord( - string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingError, commandName, correctlyCasedCommandName), - GetCommandExtent(commandAst), - GetName(), - DiagnosticSeverity.Warning, - fileName, - commandName, - suggestedCorrections: GetCorrectionExtent(commandAst, correctlyCasedCommandName)); - } + string commandName = commandAst.GetCommandName(); - var commandParameterAsts = commandAst.FindAll( - testAst => testAst is CommandParameterAst, true).Cast(); - Dictionary availableParameters; - try - { - availableParameters = commandInfo.Parameters; - } - // It's a known issue that objects from PowerShell can have a runspace affinity, - // therefore if that happens, we query a fresh object instead of using the cache. - // https://github.com/PowerShell/PowerShell/issues/4003 - catch (InvalidOperationException) - { - commandInfo = Helper.Instance.GetCommandInfo(commandName, bypassCache: true); - availableParameters = commandInfo.Parameters; - } - foreach (var commandParameterAst in commandParameterAsts) - { - var parameterName = commandParameterAst.ParameterName; - if (availableParameters.TryGetValue(parameterName, out ParameterMetadata parameterMetaData)) + // Handles the exception caused by commands like, {& $PLINK $args 2> $TempErrorFile}. + // You can also review the remark section in following document, + // MSDN: CommandAst.GetCommandName Method + if (commandName == null) + { + continue; + } + + var commandInfo = Helper.Instance.GetCommandInfo(commandName); + if (commandInfo == null || commandInfo.CommandType == CommandTypes.ExternalScript || commandInfo.CommandType == CommandTypes.Application) + { + continue; + } + + var shortName = commandInfo.Name; + var fullyqualifiedName = $"{commandInfo.ModuleName}\\{shortName}"; + var isFullyQualified = commandName.Equals(fullyqualifiedName, StringComparison.OrdinalIgnoreCase); + var correctlyCasedCommandName = isFullyQualified ? fullyqualifiedName : shortName; + + if (!commandName.Equals(correctlyCasedCommandName, StringComparison.Ordinal)) + { + yield return GetDiagnosticRecord(commandAst, fileName, correctlyCasedCommandName, Strings.UseCorrectCasingError); + } + + var commandParameterAsts = commandAst.FindAll( + testAst => testAst is CommandParameterAst, true).Cast(); + Dictionary availableParameters; + try + { + availableParameters = commandInfo.Parameters; + } + // It's a known issue that objects from PowerShell can have a runspace affinity, + // therefore if that happens, we query a fresh object instead of using the cache. + // https://github.com/PowerShell/PowerShell/issues/4003 + catch (InvalidOperationException) { - var correctlyCasedParameterName = parameterMetaData.Name; - if (!parameterName.Equals(correctlyCasedParameterName, StringComparison.Ordinal)) + commandInfo = Helper.Instance.GetCommandInfo(commandName, bypassCache: true); + availableParameters = commandInfo.Parameters; + } + foreach (var commandParameterAst in commandParameterAsts) + { + var parameterName = commandParameterAst.ParameterName; + if (availableParameters.TryGetValue(parameterName, out ParameterMetadata parameterMetaData)) { - yield return new DiagnosticRecord( - string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingParameterError, parameterName, commandName, correctlyCasedParameterName), - GetCommandExtent(commandAst), - GetName(), - DiagnosticSeverity.Warning, - fileName, - commandName, - suggestedCorrections: GetCorrectionExtent(commandParameterAst, correctlyCasedParameterName)); + var correctlyCasedParameterName = parameterMetaData.Name; + if (!parameterName.Equals(correctlyCasedParameterName, StringComparison.Ordinal)) + { + yield return GetDiagnosticRecord(commandParameterAst, fileName, correctlyCasedParameterName, Strings.UseCorrectCasingError); + } } } } } } + /// /// For a command like "gci -path c:", returns the extent of "gci" in the command /// @@ -124,44 +158,69 @@ private IScriptExtent GetCommandExtent(CommandAst commandAst) return commandAst.Extent; } - private IEnumerable GetCorrectionExtent(CommandAst commandAst, string correctlyCaseName) + private IEnumerable GetCorrectionExtent(Ast ast, IScriptExtent extent, string correctlyCaseName) { - var description = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCorrectCasingDescription, - correctlyCaseName, - correctlyCaseName); - var cmdExtent = GetCommandExtent(commandAst); var correction = new CorrectionExtent( - cmdExtent.StartLineNumber, - cmdExtent.EndLineNumber, - cmdExtent.StartColumnNumber, - cmdExtent.EndColumnNumber, + extent.StartLineNumber, + extent.EndLineNumber, + // For parameters, add +1 because of the dash before the parameter name + (ast is CommandParameterAst ? extent.StartColumnNumber + 1 : extent.StartColumnNumber), + // and do not use EndColumnNumber property, because sometimes it's all of: -ParameterName:$ParameterValue + (ast is CommandParameterAst ? extent.StartColumnNumber + 1 + ((CommandParameterAst)ast).ParameterName.Length : extent.EndColumnNumber), correctlyCaseName, - commandAst.Extent.File, - description); + extent.File, + GetDescription()); yield return correction; } - private IEnumerable GetCorrectionExtent(CommandParameterAst commandParameterAst, string correctlyCaseName) + private DiagnosticRecord GetDiagnosticRecord(Token token, string fileName, string correction, string message) { - var description = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCorrectCasingDescription, - correctlyCaseName, - correctlyCaseName); - var cmdExtent = commandParameterAst.Extent; - var correction = new CorrectionExtent( - cmdExtent.StartLineNumber, - cmdExtent.EndLineNumber, - // +1 because of the dash before the parameter name - cmdExtent.StartColumnNumber + 1, - // do not use EndColumnNumber property as it would not cover the case where the colon syntax: -ParameterName:$ParameterValue - cmdExtent.StartColumnNumber + 1 + commandParameterAst.ParameterName.Length, - correctlyCaseName, - commandParameterAst.Extent.File, - description); - yield return correction; + var extents = new[] + { + new CorrectionExtent( + token.Extent.StartLineNumber, + token.Extent.EndLineNumber, + token.Extent.StartColumnNumber, + token.Extent.EndColumnNumber, + correction, + token.Extent.File, + GetDescription()) + }; + + return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, message, token.Text, correction), + token.Extent, + GetName(), + DiagnosticSeverity.Information, + fileName, + correction, // return the keyword case as the id, so you can turn this off for specific keywords... + suggestedCorrections: extents); + } + + private DiagnosticRecord GetDiagnosticRecord(Ast ast, string fileName, string correction, string message) + { + var extent = ast is CommandAst ? GetCommandExtent((CommandAst)ast) : ast.Extent; + return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), + extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + correction, + suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); + } + + private DiagnosticRecord GetDiagnosticRecord(CommandParameterAst ast, string fileName, string correction, string message) + { + var extent = ast.Extent; + return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), + extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + correction, + suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); } /// diff --git a/Tests/Rules/UseCorrectCasing.tests.ps1 b/Tests/Rules/UseCorrectCasing.tests.ps1 index e22f5308f..c142dd2da 100644 --- a/Tests/Rules/UseCorrectCasing.tests.ps1 +++ b/Tests/Rules/UseCorrectCasing.tests.ps1 @@ -3,11 +3,11 @@ Describe "UseCorrectCasing" { It "corrects case of simple cmdlet" { - Invoke-Formatter 'get-childitem' | Should -Be 'Get-ChildItem' + Invoke-Formatter 'get-childitem' | Should -BeExactly 'Get-ChildItem' } It "corrects case of fully qualified cmdlet" { - Invoke-Formatter 'Microsoft.PowerShell.management\get-childitem' | Should -Be 'Microsoft.PowerShell.Management\Get-ChildItem' + Invoke-Formatter 'Microsoft.PowerShell.management\get-childitem' | Should -BeExactly 'Microsoft.PowerShell.Management\Get-ChildItem' } It "corrects case of of cmdlet inside interpolated string" { @@ -15,18 +15,18 @@ Describe "UseCorrectCasing" { } It "Corrects alias correctly" { - Invoke-Formatter 'Gci' | Should -Be 'gci' - Invoke-Formatter '?' | Should -Be '?' + Invoke-Formatter 'Gci' | Should -BeExactly 'gci' + Invoke-Formatter '?' | Should -BeExactly '?' } It "Does not corrects applications on the PATH" -Skip:($IsLinux -or $IsMacOS) { - Invoke-Formatter 'Cmd' | Should -Be 'Cmd' - Invoke-Formatter 'MORE' | Should -Be 'MORE' + Invoke-Formatter 'Git' | Should -BeExactly 'Git' + Invoke-Formatter 'SSH' | Should -BeExactly 'SSH' } It "Preserves extension of applications on Windows" -Skip:($IsLinux -or $IsMacOS) { - Invoke-Formatter 'cmd.exe' | Should -Be 'cmd.exe' - Invoke-Formatter 'more.com' | Should -Be 'more.com' + Invoke-Formatter 'cmd.exe' | Should -BeExactly 'cmd.exe' + Invoke-Formatter 'more.com' | Should -BeExactly 'more.com' } It "Preserves full application path" { @@ -36,37 +36,38 @@ Describe "UseCorrectCasing" { else { $applicationPath = "${env:WINDIR}\System32\cmd.exe" } - Invoke-Formatter ". $applicationPath" | Should -Be ". $applicationPath" + Invoke-Formatter ". $applicationPath" | Should -BeExactly ". $applicationPath" } - It "Corrects case of script function" { - function Invoke-DummyFunction { } - Invoke-Formatter 'invoke-dummyFunction' | Should -Be 'Invoke-DummyFunction' + # TODO: Can we make this work? + # There is a limitation in the Helper's CommandCache: it doesn't see commands that are (only temporarily) defined in the current scope + It "Corrects case of script function" -Skip { + function global:Invoke-DummyFunction { } + Invoke-Formatter 'invoke-dummyFunction' | Should -BeExactly 'Invoke-DummyFunction' } It "Preserves script path" { $path = Join-Path $TestDrive "$([guid]::NewGuid()).ps1" New-Item -ItemType File -Path $path $scriptDefinition = ". $path" - Invoke-Formatter $scriptDefinition | Should -Be $scriptDefinition + Invoke-Formatter $scriptDefinition | Should -BeExactly $scriptDefinition } It "Preserves UNC script path" -Skip:($IsLinux -or $IsMacOS) { $uncPath = [System.IO.Path]::Combine("\\$(HOSTNAME.EXE)\C$\", $TestDrive, "$([guid]::NewGuid()).ps1") New-Item -ItemType File -Path $uncPath $scriptDefinition = ". $uncPath" - Invoke-Formatter $scriptDefinition | Should -Be $scriptDefinition + Invoke-Formatter $scriptDefinition | Should -BeExactly $scriptDefinition } It "Corrects parameter casing" { - function Invoke-DummyFunction ($ParameterName) { } - - Invoke-Formatter 'Invoke-DummyFunction -parametername $parameterValue' | - Should -Be 'Invoke-DummyFunction -ParameterName $parameterValue' - Invoke-Formatter 'Invoke-DummyFunction -parametername:$parameterValue' | - Should -Be 'Invoke-DummyFunction -ParameterName:$parameterValue' - Invoke-Formatter 'Invoke-DummyFunction -parametername: $parameterValue' | - Should -Be 'Invoke-DummyFunction -ParameterName: $parameterValue' + # Without messing up the spacing or use of semicolons + Invoke-Formatter 'Get-ChildItem -literalpath $parameterValue' | + Should -BeExactly 'Get-ChildItem -LiteralPath $parameterValue' + Invoke-Formatter 'Get-ChildItem -literalpath:$parameterValue' | + Should -BeExactly 'Get-ChildItem -LiteralPath:$parameterValue' + Invoke-Formatter 'Get-ChildItem -literalpath: $parameterValue' | + Should -BeExactly 'Get-ChildItem -LiteralPath: $parameterValue' } It "Should not throw when using parameter name that does not exist" { @@ -75,11 +76,37 @@ Describe "UseCorrectCasing" { It "Does not throw when correcting certain cmdlets (issue 1516)" { $scriptDefinition = 'Get-Content;Test-Path;Get-ChildItem;Get-Content;Test-Path;Get-ChildItem' - $settings = @{ 'Rules' = @{ 'PSUseCorrectCasing' = @{ 'Enable' = $true } } } + $settings = @{ 'Rules' = @{ 'PSUseCorrectCasing' = @{ 'Enable' = $true; CheckCommands = $true; CheckKeywords = $true; CheckOperators = $true } } } { 1..100 | ForEach-Object { $null = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings -ErrorAction Stop } } | Should -Not -Throw } + + It "Corrects uppercase operators" { + Invoke-Formatter '$ENV:PATH -SPLIT ";"' | + Should -BeExactly '$ENV:PATH -split ";"' + } + + It "Corrects mixed case operators" { + Invoke-Formatter '$ENV:PATH -Split ";" -Join ":"' | + Should -BeExactly '$ENV:PATH -split ";" -join ":"' + } + + It "Corrects unary operators" { + Invoke-Formatter '-Split "Hello World"' | + Should -BeExactly '-split "Hello World"' + } + It "Does not break PlusPlus or MinusMinus" { + Invoke-Formatter '$A++; $B--' | + Should -BeExactly '$A++; $B--' + } + + Context "Inconsistent Keywords" { + It "Corrects keyword case" { + Invoke-Formatter 'ForEach ($x IN $y) { $x }' | + Should -BeExactly 'foreach ($x in $y) { $x }' + } + } } diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index f64a7888d..3d6c3d5a9 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -10,26 +10,35 @@ title: UseCorrectCasing ## Description -This is a style/formatting rule. PowerShell is case insensitive where applicable. The casing of -cmdlet names or parameters does not matter but this rule ensures that the casing matches for -consistency and also because most cmdlets/parameters start with an upper case and using that -improves readability to the human eye. +This is a style/formatting rule. PowerShell is case insensitive wherever possible, +so the casing of cmdlet names, parameters, keywords and operators does not matter. +This rule nonetheless ensures consistent casing for clarity and readability. +Using lowercase keywords helps distinguish them from commands. +Using lowercase operators helps distinguish them from parameters. ## How +Use exact casing for type names. + Use exact casing of the cmdlet and its parameters, e.g. `Invoke-Command { 'foo' } -RunAsAdministrator`. +Use lowercase for language keywords and operators. + ## Example ### Wrong ```powershell -invoke-command { 'foo' } -runasadministrator +ForEach ($file IN get-childitem -recurse) { + $file.Extension -Eq '.txt' +} ``` ### Correct ```powershell -Invoke-Command { 'foo' } -RunAsAdministrator +foreach ($file in Get-ChildItem -Recurse) { + $file.Extension -eq '.txt' +} ``` From 73455bedd8955374c35ebcaa57c3fd40c718cb9e Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Fri, 14 Mar 2025 16:37:35 +0000 Subject: [PATCH 089/130] Change severity of UseCorrectCasing to be Information (#2082) --- Rules/UseCorrectCasing.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules/UseCorrectCasing.cs b/Rules/UseCorrectCasing.cs index a9fbce198..5d1393d45 100644 --- a/Rules/UseCorrectCasing.cs +++ b/Rules/UseCorrectCasing.cs @@ -204,7 +204,7 @@ private DiagnosticRecord GetDiagnosticRecord(Ast ast, string fileName, string co string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), extent, GetName(), - DiagnosticSeverity.Warning, + DiagnosticSeverity.Information, fileName, correction, suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); @@ -217,7 +217,7 @@ private DiagnosticRecord GetDiagnosticRecord(CommandParameterAst ast, string fil string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), extent, GetName(), - DiagnosticSeverity.Warning, + DiagnosticSeverity.Information, fileName, correction, suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); From e84906804b54aec19e0a3bdb549c052aeb2ebc59 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:19:36 -0700 Subject: [PATCH 090/130] Drop v3 and v4 support from build (#2081) Co-authored-by: Christoph Bergmeister --- Directory.Packages.props | 4 +-- Engine/Engine.csproj | 26 +------------------ Engine/PSScriptAnalyzer.psd1 | 4 +-- ...osoft.PowerShell.CrossCompatibility.csproj | 2 +- README.md | 20 +++----------- build.ps1 | 2 +- build.psm1 | 20 +++----------- 7 files changed, 13 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 77cb7a272..50150e6ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,14 +3,12 @@ - - - + diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index e96f5c9d9..63b9a1b9c 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -75,33 +75,9 @@ $(DefineConstants);PSV7;CORECLR - - - - - - - - - + - - - $(DefineConstants);PSV3 - - - - $(DefineConstants);PSV3;PSV4 - - - - $(DefineConstants);PSV3 - - - - $(DefineConstants);PSV3;PSV4 - diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index c7289e890..49fb93227 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -20,13 +20,13 @@ GUID = 'd6245802-193d-4068-a631-8863a4342a18' CompanyName = 'Microsoft Corporation' # Copyright statement for this module -Copyright = '(c) Microsoft Corporation 2016. All rights reserved.' +Copyright = '(c) Microsoft Corporation 2025. All rights reserved.' # Description of the functionality provided by this module Description = 'PSScriptAnalyzer provides script analysis and checks for potential code defects in the scripts by applying a group of built-in or customized rules on the scripts being analyzed.' # Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '3.0' +PowerShellVersion = '5.1' # Name of the Windows PowerShell host required by this module # PowerShellHostName = '' diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index da987fb69..c4667a950 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -19,7 +19,7 @@ - + diff --git a/README.md b/README.md index 2ceffeb23..d038ec756 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,11 @@ To install **PSScriptAnalyzer** from source code: ### Requirements - [Latest .NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) -* If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows. -* Optionally but recommended for development: [Visual Studio 2017/2019](https://www.visualstudio.com/downloads) +- If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows. +- Optional but recommended for development: [Visual Studio 2022](https://www.visualstudio.com/downloads) +- Or [Visual Studio Code](https://code.visualstudio.com/download) - [Pester v5 PowerShell module, available on PowerShell Gallery](https://github.com/pester/Pester) - [PlatyPS PowerShell module, available on PowerShell Gallery](https://github.com/PowerShell/platyPS/releases) -- Optionally but recommended for development: [Visual Studio](https://www.visualstudio.com/downloads) ### Steps @@ -110,18 +110,6 @@ To install **PSScriptAnalyzer** from source code: .\build.ps1 -PSVersion 5 ``` - - Windows PowerShell version 4.0 - - ```powershell - .\build.ps1 -PSVersion 4 - ``` - - - Windows PowerShell version 3.0 - - ```powershell - .\build.ps1 -PSVersion 3 - ``` - - PowerShell 7 ```powershell @@ -134,7 +122,7 @@ To install **PSScriptAnalyzer** from source code: .\build.ps1 -Documentation ``` -- Build all versions (PowerShell v3, v4, v5, and v6) and documentation +- Build all versions (PowerShell v5 and v7) and documentation ```powershell .\build.ps1 -All diff --git a/build.ps1 b/build.ps1 index d6d661bf6..5dade48fe 100644 --- a/build.ps1 +++ b/build.ps1 @@ -7,7 +7,7 @@ param( [switch]$All, [Parameter(ParameterSetName="BuildOne")] - [ValidateSet(3, 4, 5, 7)] + [ValidateSet(5, 7)] [int]$PSVersion = $PSVersionTable.PSVersion.Major, [Parameter(ParameterSetName="BuildOne")] diff --git a/build.psm1 b/build.psm1 index 98a87af0d..5daba36ba 100644 --- a/build.psm1 +++ b/build.psm1 @@ -88,7 +88,7 @@ function Start-ScriptAnalyzerBuild param ( [switch]$All, - [ValidateSet(3, 4, 5, 7)] + [ValidateSet(5, 7)] [int]$PSVersion = $PSVersionTable.PSVersion.Major, [ValidateSet("Debug", "Release")] @@ -124,7 +124,7 @@ function Start-ScriptAnalyzerBuild if ( $All ) { # Build all the versions of the analyzer - foreach ($psVersion in 3, 4, 5, 7) { + foreach ($psVersion in 5, 7) { Write-Verbose -Verbose -Message "Configuration: $Configuration PSVersion: $psVersion" Start-ScriptAnalyzerBuild -Configuration $Configuration -PSVersion $psVersion -Verbose:$verboseWanted } @@ -147,12 +147,6 @@ function Start-ScriptAnalyzerBuild $framework = 'net8' } - # build the appropriate assembly - if ($PSVersion -match "[34]" -and $Framework -ne "net462") - { - throw ("ScriptAnalyzer for PS version '{0}' is not applicable to {1} framework" -f $PSVersion,$Framework) - } - Push-Location -Path $projectRoot if (-not (Test-Path "$projectRoot/global.json")) { @@ -176,14 +170,6 @@ function Start-ScriptAnalyzerBuild switch ($PSVersion) { - 3 - { - $destinationDirBinaries = "$script:destinationDir\PSv3" - } - 4 - { - $destinationDirBinaries = "$script:destinationDir\PSv4" - } 5 { $destinationDirBinaries = "$script:destinationDir" @@ -199,7 +185,7 @@ function Start-ScriptAnalyzerBuild } $buildConfiguration = $Configuration - if ((3, 4, 7) -contains $PSVersion) { + if ($PSVersion -eq 7) { $buildConfiguration = "PSV${PSVersion}${Configuration}" } From ae712f7187235f6fb43d2e28ae31a4009f42f78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadas=20Medi=C5=A1auskas?= Date: Fri, 14 Mar 2025 18:27:43 +0000 Subject: [PATCH 091/130] Add exception message for missing rules (#1968) * Add exception message for missing rules * Apply suggestions from code review --------- Co-authored-by: Christoph Bergmeister --- Engine/ScriptAnalyzer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 1946ff957..f250336b5 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -822,13 +822,13 @@ private void Initialize( // Ensure that rules were actually loaded if (rules == null || rules.Any() == false) { + string errorMessage = string.Format(CultureInfo.CurrentCulture, Strings.RulesNotFound); + this.outputWriter.ThrowTerminatingError( new ErrorRecord( - new Exception(), - string.Format( - CultureInfo.CurrentCulture, - Strings.RulesNotFound), - ErrorCategory.ResourceExists, + new Exception(errorMessage), + errorMessage, + ErrorCategory.ObjectNotFound, this)); } From 28081e5257d2853b0fcd5dd6a4ccfb3fb0e18076 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Mon, 17 Mar 2025 17:10:25 +0000 Subject: [PATCH 092/130] Backport MSDocs changes (#2085) * Backport docs updates from docs repo * Backport msdocs changes for Cmdlets. Where references to files in MSDocs were present (e.g. in UseShouldProcessForStateChangingFunctions), replaced with the live URLs. Did not port back TODO block of AvoidAssignmentToAutomaticVariable.md from https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/241/files#diff-1db3dc2f98da1fa58e24bac28ea9f14f507c78d8299349aa0196f01f9479b6f5 Backported PRs: - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/281/files - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/commit/ff80de65ff5a2615c4e61bddcab7eea5682a05e2 - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/275/files - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/278 - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/235/files - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/276/files - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/273 - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/285 - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/commit/6ca872bc245790e6b326166b3b46855666fc16d1#diff-b3e50d455fe1b464a6fec38ac616cbde4a412766a05a606c1ae55be90ccb63a3 - https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/pull/277 * Update docs/Cmdlets/Invoke-ScriptAnalyzer.md Co-authored-by: Christoph Bergmeister * Apply suggestions from code review Co-authored-by: Christoph Bergmeister --------- Co-authored-by: Sean Wheeler --- docs/Cmdlets/Get-ScriptAnalyzerRule.md | 4 +- docs/Cmdlets/Invoke-ScriptAnalyzer.md | 2 +- docs/Cmdlets/PSScriptAnalyzer.md | 3 + .../AvoidAssignmentToAutomaticVariable.md | 5 ++ .../Rules/AvoidDefaultValueSwitchParameter.md | 30 ++++++++-- docs/Rules/AvoidGlobalFunctions.md | 1 - docs/Rules/AvoidOverwritingBuiltInCmdlets.md | 23 ++++---- docs/Rules/AvoidUsingCmdletAliases.md | 2 +- ...UsingConvertToSecureStringWithPlainText.md | 4 +- docs/Rules/AvoidUsingWriteHost.md | 27 ++++++--- docs/Rules/PlaceOpenBrace.md | 2 +- .../PossibleIncorrectComparisonWithNull.md | 12 ++-- ...sibleIncorrectUsageOfAssignmentOperator.md | 2 +- docs/Rules/ProvideCommentHelp.md | 2 +- docs/Rules/UseBOMForUnicodeEncodedFile.md | 28 ++++++++- docs/Rules/UseCompatibleCmdlets.md | 17 +++--- docs/Rules/UseCompatibleCommands.md | 58 ++++++++++--------- docs/Rules/UseCompatibleTypes.md | 54 +++++++++-------- docs/Rules/UseCorrectCasing.md | 21 ++----- ...eShouldProcessForStateChangingFunctions.md | 22 ++++++- docs/Rules/UseSupportsShouldProcess.md | 4 +- docs/Rules/UseUTF8EncodingForHelpFile.md | 24 +++++++- 22 files changed, 225 insertions(+), 122 deletions(-) diff --git a/docs/Cmdlets/Get-ScriptAnalyzerRule.md b/docs/Cmdlets/Get-ScriptAnalyzerRule.md index a86d7d301..3d815b2c3 100644 --- a/docs/Cmdlets/Get-ScriptAnalyzerRule.md +++ b/docs/Cmdlets/Get-ScriptAnalyzerRule.md @@ -1,7 +1,7 @@ --- external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml Module Name: PSScriptAnalyzer -ms.date: 10/07/2021 +ms.date: 12/12/2024 online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/get-scriptanalyzerrule?view=ps-modules&wt.mc_id=ps-gethelp schema: 2.0.0 --- @@ -92,7 +92,7 @@ one value, but wildcards are supported. To get rules in subdirectories of the pa **RecurseCustomRulePath** parameter. You can create custom rules using a .NET assembly or a PowerShell module, such as the -[Community Analyzer Rules](https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1) +[Community Analyzer Rules](https://github.com/PowerShell/PSScriptAnalyzer/tree/main/Tests/Engine/CommunityAnalyzerRules) in the GitHub repository. ```yaml diff --git a/docs/Cmdlets/Invoke-ScriptAnalyzer.md b/docs/Cmdlets/Invoke-ScriptAnalyzer.md index 4eb1bff5f..b3e72a337 100644 --- a/docs/Cmdlets/Invoke-ScriptAnalyzer.md +++ b/docs/Cmdlets/Invoke-ScriptAnalyzer.md @@ -192,7 +192,7 @@ value of the **Profile** parameter is the path to the Script Analyzer profile. ExcludeRules = '*WriteHost' } -Invoke-ScriptAnalyzer -Path $pshome\Modules\BitLocker -Profile .\ScriptAnalyzerProfile.txt +Invoke-ScriptAnalyzer -Path $pshome\Modules\BitLocker -Settings .\ScriptAnalyzerProfile.txt ``` If you include a conflicting parameter in the `Invoke-ScriptAnalyzer` command, such as diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index df190238e..36ad283c1 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -21,10 +21,13 @@ checks the quality of PowerShell code by running a set of rules. ## PSScriptAnalyzer Cmdlets ### [Get-ScriptAnalyzerRule](Get-ScriptAnalyzerRule.md) + Gets the script analyzer rules on the local computer. ### [Invoke-Formatter](Invoke-Formatter.md) + Formats a script text based on the input settings or default settings. ### [Invoke-ScriptAnalyzer](Invoke-ScriptAnalyzer.md) + Evaluates a script or module based on selected best practice rules diff --git a/docs/Rules/AvoidAssignmentToAutomaticVariable.md b/docs/Rules/AvoidAssignmentToAutomaticVariable.md index f8203dc8e..60d520d07 100644 --- a/docs/Rules/AvoidAssignmentToAutomaticVariable.md +++ b/docs/Rules/AvoidAssignmentToAutomaticVariable.md @@ -16,6 +16,11 @@ only be assigned in certain special cases to achieve a certain effect as a speci To understand more about automatic variables, see `Get-Help about_Automatic_Variables`. + + ## How Use variable names in functions or their parameters that do not conflict with automatic variables. diff --git a/docs/Rules/AvoidDefaultValueSwitchParameter.md b/docs/Rules/AvoidDefaultValueSwitchParameter.md index 7cfbbc212..9cc1ba855 100644 --- a/docs/Rules/AvoidDefaultValueSwitchParameter.md +++ b/docs/Rules/AvoidDefaultValueSwitchParameter.md @@ -1,6 +1,6 @@ --- description: Switch Parameters Should Not Default To True -ms.date: 06/28/2023 +ms.date: 12/05/2024 ms.topic: reference title: AvoidDefaultValueSwitchParameter --- @@ -10,11 +10,19 @@ title: AvoidDefaultValueSwitchParameter ## Description -Switch parameters for commands should default to false. +If your parameter takes only `true` and `false`, define the parameter as type `[Switch]`. PowerShell +treats a switch parameter as `true` when it's used with a command. If the parameter isn't included +with the command, PowerShell considers the parameter to be false. Don't define `[Boolean]` +parameters. + +You shouldn't define a switch parameter with a default value of `$true` because this isn't the +expected behavior of a switch parameter. ## How -Change the default value of the switch parameter to be false. +Change the default value of the switch parameter to be `$false` or don't provide a default value. +Write the logic of the script to assume that the switch parameter default value is `$false` or not +provided. ## Example @@ -48,8 +56,22 @@ function Test-Script $Param1, [switch] - $Switch=$False + $Switch ) + + begin { + # Ensure that the $Switch is set to false if not provided + if (-not $PSBoundParameters.ContainsKey('Switch')) { + $Switch = $false + } + } ... } ``` + +## More information + +- [Strongly Encouraged Development Guidelines][01] + + +[01]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines#parameters-that-take-true-and-false diff --git a/docs/Rules/AvoidGlobalFunctions.md b/docs/Rules/AvoidGlobalFunctions.md index f74b094cb..929466cb6 100644 --- a/docs/Rules/AvoidGlobalFunctions.md +++ b/docs/Rules/AvoidGlobalFunctions.md @@ -13,7 +13,6 @@ title: AvoidGlobalFunctions Globally scoped functions override existing functions within the sessions with matching names. This name collision can cause difficult to debug issues for consumers of modules. - To understand more about scoping, see `Get-Help about_Scopes`. ## How diff --git a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md index 1d94a618a..10e1ad30a 100644 --- a/docs/Rules/AvoidOverwritingBuiltInCmdlets.md +++ b/docs/Rules/AvoidOverwritingBuiltInCmdlets.md @@ -1,6 +1,6 @@ --- description: Avoid overwriting built in cmdlets -ms.date: 06/28/2023 +ms.date: 12/12/2024 ms.topic: reference title: AvoidOverwritingBuiltInCmdlets --- @@ -14,7 +14,7 @@ This rule flags cmdlets that are available in a given edition/version of PowerSh operating system which are overwritten by a function declaration. It works by comparing function declarations against a set of allowlists that ship with PSScriptAnalyzer. These allowlist files are used by other PSScriptAnalyzer rules. More information can be found in the documentation for the -[UseCompatibleCmdlets](./UseCompatibleCmdlets.md) rule. +[UseCompatibleCmdlets][01] rule. ## Configuration @@ -37,14 +37,17 @@ following your settings file. The parameter `PowerShellVersion` is a list of allowlists that ship with PSScriptAnalyzer. -**Note**: The default value for `PowerShellVersion` is `core-6.1.0-windows` if PowerShell 6 or -later is installed, and `desktop-5.1.14393.206-windows` if it is not. +> [!NOTE] +> The default value for `PowerShellVersion` is `core-6.1.0-windows` if PowerShell 6 or +> later is installed, and `desktop-5.1.14393.206-windows` if it's not. Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major and minor versions of PowerShell are supplied. One can also create a custom settings file as well -with the -[New-CommandDataFile.ps1](https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Utils/New-CommandDataFile.ps1) -script and use it by placing the created `JSON` into the `Settings` folder of the `PSScriptAnalyzer` -module installation folder, then the `PowerShellVersion` parameter is just its file name (that can -also be changed if desired). Note that the `core-6.0.2-*` files were removed in PSScriptAnalyzer -1.18 since PowerShell 6.0 reached end of life. +with the [New-CommandDataFile.ps1][02] script and use it by placing the created `JSON` into the +`Settings` folder of the `PSScriptAnalyzer` module installation folder, then the `PowerShellVersion` +parameter is just its filename (that can also be changed if desired). Note that the `core-6.0.2-*` +files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 reached end of life. + + +[01]: ./UseCompatibleCmdlets.md +[02]: https://github.com/PowerShell/PSScriptAnalyzer/blob/main/Utils/New-CommandDataFile.ps1 diff --git a/docs/Rules/AvoidUsingCmdletAliases.md b/docs/Rules/AvoidUsingCmdletAliases.md index 48c914eec..9a33149ad 100644 --- a/docs/Rules/AvoidUsingCmdletAliases.md +++ b/docs/Rules/AvoidUsingCmdletAliases.md @@ -20,7 +20,7 @@ There are also implicit aliases. When PowerShell cannot find the cmdlet name, it Every PowerShell author learns the actual command names, but different authors learn and use different aliases. Aliases can make code difficult to read, understand and impact availability. -Using the full command name makes it eaiser to maintain your scripts in the the future. +Using the full command name makes it easier to maintain your scripts in the the future. Using the full command names also allows for syntax highlighting in sites and applications like GitHub and Visual Studio Code. diff --git a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md index 5a94d89a3..d25fce124 100644 --- a/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md +++ b/docs/Rules/AvoidUsingConvertToSecureStringWithPlainText.md @@ -1,6 +1,6 @@ --- description: Avoid Using SecureString With Plain Text -ms.date: 06/28/2023 +ms.date: 01/28/2025 ms.topic: reference title: AvoidUsingConvertToSecureStringWithPlainText --- @@ -37,6 +37,4 @@ $EncryptedInput = ConvertTo-SecureString -String $UserInput -AsPlainText -Force ```powershell $SecureUserInput = Read-Host 'Please enter your secure code' -AsSecureString -$EncryptedInput = ConvertFrom-SecureString -String $SecureUserInput -$SecureString = ConvertTo-SecureString -String $EncryptedInput ``` diff --git a/docs/Rules/AvoidUsingWriteHost.md b/docs/Rules/AvoidUsingWriteHost.md index 561914168..a02571c79 100644 --- a/docs/Rules/AvoidUsingWriteHost.md +++ b/docs/Rules/AvoidUsingWriteHost.md @@ -1,6 +1,6 @@ --- description: Avoid Using Write-Host -ms.date: 06/28/2023 +ms.date: 12/05/2024 ms.topic: reference title: AvoidUsingWriteHost --- @@ -10,10 +10,15 @@ title: AvoidUsingWriteHost ## Description -The use of `Write-Host` is greatly discouraged unless in the use of commands with the `Show` verb. -The `Show` verb explicitly means 'show on the screen, with no other possibilities'. +The primary purpose of the `Write-Host` cmdlet is to produce display-only output in the host. For +example: printing colored text or prompting the user for input when combined with `Read-Host`. +`Write-Host` uses the `ToString()` method to write the output. The particular result depends on the +program that's hosting PowerShell. The output from `Write-Host` isn't sent to the pipeline. To +output data to the pipeline, use `Write-Output` or implicit output. -Commands with the `Show` verb do not have this check applied. +The use of `Write-Host` in a function is discouraged unless the function uses the `Show` verb. The +`Show` verb explicitly means _display information to the user_. This rule doesn't apply to functions +with the `Show` verb. ## How @@ -27,22 +32,22 @@ logging or returning one or more objects. ```powershell function Get-MeaningOfLife { - ... Write-Host 'Computing the answer to the ultimate question of life, the universe and everything' - ... Write-Host 42 } ``` ### Correct +Use `Write-Verbose` for informational messages. The user can decide whether to see the message by +providing the **Verbose** parameter. + ```powershell function Get-MeaningOfLife { - [CmdletBinding()]Param() # to make it possible to set the VerbosePreference when calling the function - ... + [CmdletBinding()]Param() # makes it possible to support Verbose output + Write-Verbose 'Computing the answer to the ultimate question of life, the universe and everything' - ... Write-Output 42 } @@ -51,3 +56,7 @@ function Show-Something Write-Host 'show something on screen' } ``` + +## More information + +[Write-Host](xref:Microsoft.PowerShell.Utility.Write-Host) diff --git a/docs/Rules/PlaceOpenBrace.md b/docs/Rules/PlaceOpenBrace.md index faa6d4c5d..a523ec4e8 100644 --- a/docs/Rules/PlaceOpenBrace.md +++ b/docs/Rules/PlaceOpenBrace.md @@ -45,5 +45,5 @@ Enforce a new line character after an open brace. The default value is true. #### IgnoreOneLineBlock: bool (Default value is `$true`) Indicates if open braces in a one line block should be ignored or not. For example, -` $x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule +`$x = if ($true) { 'blah' } else { 'blah blah' }`, if the property is set to true then the rule doesn't fire a violation. diff --git a/docs/Rules/PossibleIncorrectComparisonWithNull.md b/docs/Rules/PossibleIncorrectComparisonWithNull.md index 28c9c7075..9a28646f4 100644 --- a/docs/Rules/PossibleIncorrectComparisonWithNull.md +++ b/docs/Rules/PossibleIncorrectComparisonWithNull.md @@ -1,6 +1,6 @@ --- description: Null Comparison -ms.date: 06/28/2023 +ms.date: 12/03/2024 ms.topic: reference title: PossibleIncorrectComparisonWithNull --- @@ -18,8 +18,8 @@ There are multiple reasons why this occurs: - `$null` is a scalar value. When the value on the left side of an operator is a scalar, comparison operators return a **Boolean** value. When the value is a collection, the comparison operators return any matching values or an empty array if there are no matches in the collection. -- PowerShell performs type casting left to right, resulting in incorrect comparisons when `$null` is - cast to other scalar types. +- PowerShell performs type casting on the right-hand operand, resulting in incorrect comparisons + when `$null` is cast to other scalar types. The only way to reliably check if a value is `$null` is to place `$null` on the left side of the operator so that a scalar comparison is performed. @@ -55,10 +55,10 @@ function Test-CompareWithNull ## Try it Yourself ```powershell -# Both expressions below return 'false' because the comparison does not return an -# object and therefore the if statement always falls through: +# This example returns 'false' because the comparison does not return any objects from the array if (@() -eq $null) { 'true' } else { 'false' } -if (@() -ne $null) { 'true' } else { 'false' } +# This example returns 'true' because the array is empty +if ($null -ne @()) { 'true' } else { 'false' } ``` This is how the comparison operator works by-design. But, as demonstrated, this can lead diff --git a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md index bbaf437b8..11c5d23f1 100644 --- a/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md +++ b/docs/Rules/PossibleIncorrectUsageOfAssignmentOperator.md @@ -52,7 +52,7 @@ if ($a = Get-Something) # Only execute action if command returns something and a } ``` -## Implicit suppresion using Clang style +## Implicit suppression using Clang style There are some rare cases where assignment of variable inside an `if` statement is by design. Instead of suppressing the rule, one can also signal that assignment was intentional by wrapping the diff --git a/docs/Rules/ProvideCommentHelp.md b/docs/Rules/ProvideCommentHelp.md index 8c642419f..19e83681a 100644 --- a/docs/Rules/ProvideCommentHelp.md +++ b/docs/Rules/ProvideCommentHelp.md @@ -36,7 +36,7 @@ Rules = @{ ### Parameters -- `Enable`: **bool** (Default valus is `$true`) +- `Enable`: **bool** (Default value is `$true`) Enable or disable the rule during ScriptAnalyzer invocation. diff --git a/docs/Rules/UseBOMForUnicodeEncodedFile.md b/docs/Rules/UseBOMForUnicodeEncodedFile.md index 6fffaa7fe..e0a46b1e2 100644 --- a/docs/Rules/UseBOMForUnicodeEncodedFile.md +++ b/docs/Rules/UseBOMForUnicodeEncodedFile.md @@ -1,6 +1,6 @@ --- description: Use BOM encoding for non-ASCII files -ms.date: 06/28/2023 +ms.date: 01/07/2025 ms.topic: reference title: UseBOMForUnicodeEncodedFile --- @@ -13,6 +13,30 @@ title: UseBOMForUnicodeEncodedFile For a file encoded with a format other than ASCII, ensure Byte Order Mark (BOM) is present to ensure that any application consuming this file can interpret it correctly. +You can use this rule to test any arbitrary text file, but the intent is to ensure that PowerShell +scripts are saved with a BOM when using a Unicode encoding. + ## How -Ensure that the file is encoded with BOM present. +For PowerShell commands that write to files, ensure that you set the encoding parameter to a value +that produces a BOM. In PowerShell 7 and higher, the following values of the **Encoding** parameter +produce a BOM: + +- `bigendianunicode` +- `bigendianutf32` +- `oem` +- `unicode` +- `utf32` +- `utf8BOM` + +When you create a script file using a text editor, ensure that the editor is configured to save the +file with a BOM. Consult the documentation for your text editor for instructions on how to save +files with a BOM. + +## Further reading + +For more information, see the following articles: + +- [about_Character_Encoding](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_character_encoding) +- [Set-Content](https://learn.microsoft.com/powershell/module/microsoft.powershell.management/set-content) +- [Understanding file encoding in VS Code and PowerShell](https://learn.microsoft.com/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding) diff --git a/docs/Rules/UseCompatibleCmdlets.md b/docs/Rules/UseCompatibleCmdlets.md index 1fc9520d4..4cd52340e 100644 --- a/docs/Rules/UseCompatibleCmdlets.md +++ b/docs/Rules/UseCompatibleCmdlets.md @@ -1,6 +1,6 @@ --- description: Use compatible cmdlets -ms.date: 06/28/2023 +ms.date: 12/12/2024 ms.topic: reference title: UseCompatibleCmdlets --- @@ -10,8 +10,8 @@ title: UseCompatibleCmdlets ## Description -This rule flags cmdlets that are not available in a given Edition/Version of PowerShell on a given -Operating System. It works by comparing a cmdlet against a set of allowlists which ship with +This rule flags cmdlets that aren't available in a given Edition and Version of PowerShell on a +given Operating System. It works by comparing a cmdlet against a set of allowlists which ship with PSScriptAnalyzer. They can be found at `/path/to/PSScriptAnalyzerModule/Settings`. These files are of the form, `--.json` where `` can be either `Core` or `Desktop`, `` can be either `Windows`, `Linux` or `MacOS`, and `` is the PowerShell @@ -41,7 +41,10 @@ The parameter `compatibility` is a list that contain any of the following Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major and minor versions of PowerShell are supplied. You can also create a custom settings file with the -[New-CommandDataFile.ps1](https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Utils/New-CommandDataFile.ps1) -script. Place the created `.json` file in the `Settings` folder of the `PSScriptAnalyzer` module -folder. Then the `compatibility` parameter values is just the filename. Note that the `core-6.0.2-*` -files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 reached it's end of life. +[New-CommandDataFile.ps1][01] script. Place the created `.json` file in the `Settings` folder of the +`PSScriptAnalyzer` module folder. Then the `compatibility` parameter values is just the filename. +Note that the `core-6.0.2-*` files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 +reached it's end of life. + + +[01]: https://github.com/PowerShell/PSScriptAnalyzer/blob/main/Utils/New-CommandDataFile.ps1 diff --git a/docs/Rules/UseCompatibleCommands.md b/docs/Rules/UseCompatibleCommands.md index 00b768ba3..ae74862ba 100644 --- a/docs/Rules/UseCompatibleCommands.md +++ b/docs/Rules/UseCompatibleCommands.md @@ -1,6 +1,6 @@ --- description: Use compatible commands -ms.date: 06/28/2023 +ms.date: 12/12/2024 ms.topic: reference title: UseCompatibleCommands --- @@ -46,35 +46,33 @@ your configuration. Platforms bundled by default are: -| PowerShell Version | Operating System | ID | -| ------------------ | --------------------- | --------------------------------------------------------------------- | -| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` | -| 4.0 | Windows Server 2012R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` | -| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` | -| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | -| 5.1 | Windows 10 1809 (RS5) | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | -| 6.2 | Windows Server 2016 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Windows Server 2019 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Windows 10 1809 (RS5) | `win-4_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` | -| 7.0 | Windows Server 2016 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` | -| 7.0 | Windows Server 2019 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` | -| 7.0 | Windows 10 1809 (RS5) | `win-4_x64_10.0.17763.0_6.2.4_x64_3.1.2_core` | -| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_3.1.2_core` | - -Other profiles can be found in the -[GitHub repo](https://github.com/PowerShell/PSScriptAnalyzer/tree/development/PSCompatibilityCollector/optional_profiles). - -You can also generate your own platform profile using the -[PSCompatibilityCollector module](https://github.com/PowerShell/PSScriptAnalyzer/tree/development/PSCompatibilityCollector). +| PowerShell Version | Operating System | ID | +| :----------------: | ---------------------- | --------------------------------------------------------------------- | +| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` | +| 4.0 | Windows Server 2012 R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` | +| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` | +| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | +| 5.1 | Windows 10 Pro | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | +| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_6.2.4_x64_4.0.30319.42000_core` | +| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_7.0.0_x64_3.1.2_core` | + +Other profiles can be found in the [GitHub repo][02]. + +You can also generate your own platform profile using the [PSCompatibilityCollector module][01]. The compatibility profile settings takes a list of platforms to target under `TargetProfiles`. A platform can be specified as: - A platform name (like `ubuntu_x64_18.04_6.1.1_x64_4.0.30319.42000_core`), which will have `.json` added to the end and is searched for in the default profile directory. -- A filename (like `my_custom_platform.json`), which will be searched for the in the default - profile directory. +- A filename (like `my_custom_platform.json`), which will be searched for the in the default profile + directory. - An absolute path to a file (like `D:\PowerShellProfiles\TargetMachine.json`). The default profile directory is under the PSScriptAnalzyer module at @@ -82,7 +80,7 @@ The default profile directory is under the PSScriptAnalzyer module at containing `PSScriptAnalyzer.psd1`). The compatibility analysis compares a command used to both a target profile and a 'union' profile -(containing all commands available in *any* profile in the profile dir). If a command is not present +(containing all commands available in _any_ profile in the profile dir). If a command is not present in the union profile, it is assumed to be locally created and ignored. Otherwise, if a command is present in the union profile but not present in a target, it is deemed to be incompatible with that target. @@ -131,11 +129,17 @@ scriptblock as with other rules. The rule can also be suppressed only for particular commands: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'Start-Service')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', + 'Start-Service')] ``` And also suppressed only for parameters: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'Import-Module/FullyQualifiedName')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', + 'Import-Module/FullyQualifiedName')] ``` + + +[01]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector +[02]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector/optional_profiles diff --git a/docs/Rules/UseCompatibleTypes.md b/docs/Rules/UseCompatibleTypes.md index 355f35bed..9bff5fa76 100644 --- a/docs/Rules/UseCompatibleTypes.md +++ b/docs/Rules/UseCompatibleTypes.md @@ -1,6 +1,6 @@ --- description: Use compatible types -ms.date: 06/28/2023 +ms.date: 12/12/2024 ms.topic: reference title: UseCompatibleTypes --- @@ -47,27 +47,25 @@ your configuration. Platforms bundled by default are: -| PowerShell Version | Operating System | ID | -| ------------------ | --------------------- | --------------------------------------------------------------------- | -| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` | -| 4.0 | Windows Server 2012R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` | -| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` | -| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | -| 5.1 | Windows 10 1809 (RS5) | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | -| 6.2 | Windows Server 2016 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Windows Server 2019 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Windows 10 1809 (RS5) | `win-4_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | -| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` | -| 7.0 | Windows Server 2016 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` | -| 7.0 | Windows Server 2019 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` | -| 7.0 | Windows 10 1809 (RS5) | `win-4_x64_10.0.17763.0_6.2.4_x64_3.1.2_core` | -| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_3.1.2_core` | - -Other profiles can be found in the -[GitHub repo](https://github.com/PowerShell/PSScriptAnalyzer/tree/development/PSCompatibilityCollector/optional_profiles). - -You can also generate your own platform profile using the -[PSCompatibilityCollector module](https://github.com/PowerShell/PSScriptAnalyzer/tree/development/PSCompatibilityCollector). +| PowerShell Version | Operating System | ID | +| :----------------: | ---------------------- | --------------------------------------------------------------------- | +| 3.0 | Windows Server 2012 | `win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework` | +| 4.0 | Windows Server 2012 R2 | `win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework` | +| 5.1 | Windows Server 2016 | `win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework` | +| 5.1 | Windows Server 2019 | `win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | +| 5.1 | Windows 10 Pro | `win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework` | +| 6.2 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_6.2.4_x64_4.0.30319.42000_core` | +| 6.2 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_6.2.4_x64_4.0.30319.42000_core` | +| 7.0 | Ubuntu 18.04 LTS | `ubuntu_x64_18.04_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.14393 | `win-8_x64_10.0.14393.0_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.17763 | `win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core` | +| 7.0 | Windows 10.0.18362 | `win-4_x64_10.0.18362.0_7.0.0_x64_3.1.2_core` | + +Other profiles can be found in the [GitHub repo][02]. + +You can also generate your own platform profile using the [PSCompatibilityCollector module][01]. The compatibility profile settings takes a list of platforms to target under `TargetProfiles`. A platform can be specified as: @@ -130,7 +128,7 @@ PS> $settings = @{ } } } -PS> Invoke-ScriptAnalyzer -Settings $settings -ScriptDefinition '[System.Management.Automation.SemanticVersion]'1.18.0-rc1'' +PS> Invoke-ScriptAnalyzer -Settings $settings -ScriptDefinition "[System.Management.Automation.SemanticVersion]'1.18.0-rc1'" RuleName Severity ScriptName Line Message -------- -------- ---------- ---- ------- @@ -151,11 +149,17 @@ scriptblock as with other rules. The rule can also be suppressed only for particular types: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes', 'System.Management.Automation.Security.SystemPolicy')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleTypes', + 'System.Management.Automation.Security.SystemPolicy')] ``` And also suppressed only for type members: ```powershell -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', 'System.Management.Automation.LanguagePrimitives/ConvertTypeNameToPSTypeName')] +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', + 'System.Management.Automation.LanguagePrimitives/ConvertTypeNameToPSTypeName')] ``` + + +[01]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector +[02]: https://github.com/PowerShell/PSScriptAnalyzer/tree/main/PSCompatibilityCollector/optional_profiles diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 3d6c3d5a9..1cd53297c 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -10,35 +10,26 @@ title: UseCorrectCasing ## Description -This is a style/formatting rule. PowerShell is case insensitive wherever possible, -so the casing of cmdlet names, parameters, keywords and operators does not matter. -This rule nonetheless ensures consistent casing for clarity and readability. -Using lowercase keywords helps distinguish them from commands. -Using lowercase operators helps distinguish them from parameters. +This is a style formatting rule. PowerShell is case insensitive where applicable. The casing of +cmdlet names or parameters does not matter but this rule ensures that the casing matches for +consistency and also because most cmdlets/parameters start with an upper case and using that +improves readability to the human eye. ## How -Use exact casing for type names. - Use exact casing of the cmdlet and its parameters, e.g. `Invoke-Command { 'foo' } -RunAsAdministrator`. -Use lowercase for language keywords and operators. - ## Example ### Wrong ```powershell -ForEach ($file IN get-childitem -recurse) { - $file.Extension -Eq '.txt' -} +invoke-command { 'foo' } -runasadministrator ``` ### Correct ```powershell -foreach ($file in Get-ChildItem -Recurse) { - $file.Extension -eq '.txt' -} +Invoke-Command { 'foo' } -RunAsAdministrator ``` diff --git a/docs/Rules/UseShouldProcessForStateChangingFunctions.md b/docs/Rules/UseShouldProcessForStateChangingFunctions.md index f0e102da3..97bb97767 100644 --- a/docs/Rules/UseShouldProcessForStateChangingFunctions.md +++ b/docs/Rules/UseShouldProcessForStateChangingFunctions.md @@ -1,6 +1,6 @@ --- description: Use ShouldProcess For State Changing Functions -ms.date: 06/28/2023 +ms.date: 12/05/2024 ms.topic: reference title: UseShouldProcessForStateChangingFunctions --- @@ -10,7 +10,12 @@ title: UseShouldProcessForStateChangingFunctions ## Description -Functions whose verbs change system state should support `ShouldProcess`. +Functions whose verbs change system state should support `ShouldProcess`. To enable the +`ShouldProcess` feature, set the `SupportsShouldProcess` argument in the `CmdletBinding` attribute. +The `SupportsShouldProcess` argument adds **Confirm** and **WhatIf** parameters to the function. The +**Confirm** parameter prompts the user before it runs the command on each object in the pipeline. +The **WhatIf** parameter lists the changes that the command would make, instead of running the +command. Verbs that should support `ShouldProcess`: @@ -58,3 +63,16 @@ function Set-ServiceObject ... } ``` + +## More information + +- [about_Functions_CmdletBindingAttribute][01] +- [Everything you wanted to know about ShouldProcess][04] +- [Required Development Guidelines][03] +- [Requesting Confirmation from Cmdlets][02] + + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute +[02]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/requesting-confirmation-from-cmdlets +[03]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/required-development-guidelines#support-confirmation-requests-rd04 +[04]: https://learn.microsoft.com/powershell/scripting/learn/deep-dives/everything-about-shouldprocess diff --git a/docs/Rules/UseSupportsShouldProcess.md b/docs/Rules/UseSupportsShouldProcess.md index 04ca2bfe7..904ad0773 100644 --- a/docs/Rules/UseSupportsShouldProcess.md +++ b/docs/Rules/UseSupportsShouldProcess.md @@ -18,7 +18,7 @@ authors to provide the desired interactive experience while using the cmdlet. ## Example -### Wrong: +### Wrong ```powershell function foo { @@ -30,7 +30,7 @@ function foo { } ``` -### Correct: +### Correct ```powershell function foo { diff --git a/docs/Rules/UseUTF8EncodingForHelpFile.md b/docs/Rules/UseUTF8EncodingForHelpFile.md index 31c525db6..6d8e0f3c2 100644 --- a/docs/Rules/UseUTF8EncodingForHelpFile.md +++ b/docs/Rules/UseUTF8EncodingForHelpFile.md @@ -1,6 +1,6 @@ --- description: Use UTF8 Encoding For Help File -ms.date: 06/28/2023 +ms.date: 01/07/2025 ms.topic: reference title: UseUTF8EncodingForHelpFile --- @@ -10,4 +10,24 @@ title: UseUTF8EncodingForHelpFile ## Description -Check if help file uses UTF-8 encoding. +Check that an `about_` help file uses UTF-8 encoding. The filename must start with `about_` and end +with `.help.txt`. The rule uses the **CurrentEncoding** property of the **StreamReader** class to +determine the encoding of the file. + +## How + +For PowerShell commands that write to files, ensure that you set the encoding parameter to `utf8`, +`utf8BOM`, or `utf8NoBOM`. + +When you create a help file using a text editor, ensure that the editor is configured to save the +file in a UTF8 format. Consult the documentation for your text editor for instructions on how to +save files with a specific encoding. + +## Further reading + +For more information, see the following articles: + +- [System.IO.StreamReader](https://learn.microsoft.com/dotnet/api/system.io.streamreader.currentencoding) +- [about_Character_Encoding](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_character_encoding) +- [Set-Content](https://learn.microsoft.com/powershell/module/microsoft.powershell.management/set-content) +- [Understanding file encoding in VS Code and PowerShell](https://learn.microsoft.com/powershell/scripting/dev-cross-plat/vscode/understanding-file-encoding) From 6c4f1d2eb3415d58d2aff19a2b32e3bee1aa1092 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Mon, 17 Mar 2025 17:37:23 +0000 Subject: [PATCH 093/130] Document new optional parameters added in 1704 (#2086) --- docs/Rules/UseCorrectCasing.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 1cd53297c..6afcb3a4b 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -15,6 +15,35 @@ cmdlet names or parameters does not matter but this rule ensures that the casing consistency and also because most cmdlets/parameters start with an upper case and using that improves readability to the human eye. +## Configuration + +```powershell +Rules = @{ + PS UseCorrectCasing = @{ + Enable = $true + CheckCommands = $true + CheckKeyword = $true + CheckOperator = $true + } +} +``` + +### Enable: bool (Default value is `$false`) + +Enable or disable the rule during ScriptAnalyzer invocation. + +### CheckCommands: bool (Default value is `$true`) + +If true, require the case of all operators to be lowercase. + +### CheckKeyword: bool (Default value is `$true`) + +If true, require the case of all keywords to be lowercase. + +### CheckOperator: bool (Default value is `$true`) + +If true, require the case of all commands to match their actual casing. + ## How Use exact casing of the cmdlet and its parameters, e.g. From 4b4a136d3b669a1fc127f182e7360160e4919acb Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:16:28 -0700 Subject: [PATCH 094/130] v1.24.0: Thanks to all the new contributors! (#2084) Co-authored-by: Christoph Bergmeister --- .pipelines/PSScriptAnalyzer-Official.yml | 2 +- CHANGELOG.MD | 81 ++++++++++++++++++++++-- Directory.Build.props | 2 +- docs/Cmdlets/PSScriptAnalyzer.md | 2 +- 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml index b3f2c2cb4..abea9ab3c 100644 --- a/.pipelines/PSScriptAnalyzer-Official.yml +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -138,7 +138,7 @@ extends: target: main assets: $(Pipeline.Workspace)/PSScriptAnalyzer.$(version).nupkg tagSource: userSpecifiedTag - tag: v$(version) + tag: $(version) isDraft: true addChangeLog: false releaseNotesSource: inline diff --git a/CHANGELOG.MD b/CHANGELOG.MD index b948c475a..76352e7c7 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,8 +1,73 @@ # CHANGELOG +## [1.24.0](https://github.com/PowerShell/PSScriptAnalyzer/releases/tag/1.24.0) + +### What's Changed +#### Breaking Changes + +Minimum required PowerShell version raised from 3 to 5.1 +* Drop v3 and v4 support from build by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2081 + +#### New Features + +* Add new options (enabled by default) to formatting rule `UseCorrectCasing` to also correct operators, keywords and commands - Add UseConsistentCasing by @Jaykul in https://github.com/PowerShell/PSScriptAnalyzer/pull/1704 + +#### Enhancements + +* PSAlignAssignmentStatement: Ignore hashtables with a single key-value pair by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1986 +* Use `RequiredResource` hashtable to specify PowerShell module versions by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2053 +* Set exit code of `Invoke-ScriptAnalyzer -EnableExit` to total number of diagnostics (#2054) by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2055 +* PSAvoidAssignmentToAutomaticVariable: Ignore when a Parameter has an Attribute that contains a Variable expression by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1988 +* Trim unnecessary trailing spaces from string resources in Strings.resx by @XPlantefeve in https://github.com/PowerShell/PSScriptAnalyzer/pull/1972 +* Do not print summary repeatedly for each logger by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2058 +* Make Settings type detection more robust by @Tadas in https://github.com/PowerShell/PSScriptAnalyzer/pull/1967 +* Add foreach Assignment to AvoidAssignmentToAutomaticVariable by @PoshAJ in https://github.com/PowerShell/PSScriptAnalyzer/pull/2021 +* Invoke-ScriptAnalyzer: Stream diagnostics instead of batching by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2062 +* Invoke-ScriptAnalyzer: Print summary only once per invocation by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2063 +* Invoke-ScriptAnalyzer: Include parse errors in reported error count by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2069 +* Add exception message for missing rules by @Tadas in https://github.com/PowerShell/PSScriptAnalyzer/pull/1968 + +#### Bug Fixes + +* Update links in module manifest by @martincostello in https://github.com/PowerShell/PSScriptAnalyzer/pull/2034 +* Fix incorrect `-ReportSummary` Pester test grouping by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2057 +* Fixed erroneous PSUseDeclaredVarsMoreThanAssignments for some globals variables by @John-Leitch in https://github.com/PowerShell/PSScriptAnalyzer/pull/2013 +* PSReservedParams: Make severity Error instead of Warning by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1989 +* PSUseConsistentIndentation: Check indentation of lines where first token is a LParen not followed by comment or new line by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1995 +* PSUseConsistentWhitespace: Correctly fix whitespace between command parameters when parameter value spans multiple lines by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2064 +* PSAvoidTrailingWhitespace: Rule not applied when using formatter + single character lines with trailing whitespace are truncated by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1993 +* PSUseConsistentWhitespace: Ignore whitespace between separator and comment by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2065 +* PSReviewUnusedParameter false positive for ValueFromPipeline by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2072 +* Change severity of UseCorrectCasing to be Information by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2082 + +#### Process Changes + +* Copy more files to module root by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2037 +* Upgrade to .NET 8 since .NET 6 is past EOL by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2073 +* Use -NoProfile when invoking pwsh in Pester tests by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2061 +* Add GitHub Actions Ubuntu's dotnet path by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2080 +* Update README.md with recent upgrade to .NET 8 by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2076 +* Update CHANGELOG.MD with 1.23.0 release notes by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2078 +* Bring back Codespaces by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2077 +* Update SMA version to 7.4.7 by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2075 +* Test PowerShell Preview in CI by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2070 +* Backport MSDocs changes by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2085 +* Document new optional parameters added to UseCorrectCasing by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2086 + +### New Contributors +* @martincostello made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2034 +* @MatejKafka made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2055 +* @XPlantefeve made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1972 +* @John-Leitch made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2013 +* @Tadas made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1967 +* @PoshAJ made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2021 +* @Jaykul made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1704 + +**Full Changelog**: https://github.com/PowerShell/PSScriptAnalyzer/compare/1.23.0...1.24.0 + ## [1.23.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.23.0) - 2024-10-09 -## What's Changed +### What's Changed * Adding OneBranch pipeline YAML config file for OSS_Microsoft_PSSA-Official by @adityapatwardhan in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 * Update format and grammar of AvoidUsingAllowUnencryptedAuthentication by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1974 * Move to OneBranch Signing and SBOM generation by @TravisEz13 in https://github.com/PowerShell/PSScriptAnalyzer/pull/1982 @@ -19,20 +84,22 @@ * v1.23.0: Update version for new release by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2032 * Migrate release pipeline to DeployBox by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2033 -## New Contributors +### New Contributors * @adityapatwardhan made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 +**Full Changelog**: https://github.com/PowerShell/PSScriptAnalyzer/compare/1.22.0...1.23.0 + ## [1.22.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.22.0) - 2024-03-05 Minimum required version when using PowerShell 7 is now `7.2.11`. -## New Rule +### New Rule - Add AvoidUsingAllowUnencryptedAuthentication by @MJVL in (#1857) - Add the AvoidExclaimOperator rule to warn about the use of the ! negation operator. Fixes (#1826) by @liamjpeters in (#1922) -## Enhancements +### Enhancements - Enable suppression of PSAvoidAssignmentToAutomaticVariable for specific variable or parameter by @fflaten in (#1896) @@ -46,11 +113,11 @@ Minimum required version when using PowerShell 7 is now `7.2.11`. CommandAllowList by @bergmeister in (#1850) - PSReviewUnusedParameter: Add CommandsToTraverse option by @FriedrichWeinmann in (#1921) -## Fixes +### Fixes - Prevent NullReferenceException for null analysis type. by @hubuk in (#1949) -## Build & Test, Documentation and Maintenance +### Build & Test, Documentation and Maintenance - UseApprovedVerbs.md: Backport minor change of PR 104 in PowerShell-Docs-Modules by @bergmeister in (#1849) @@ -89,7 +156,7 @@ Minimum required version when using PowerShell 7 is now `7.2.11`. - Remove Appveyor badge from main README by @bergmeister in (#1962) - Do not hard code common parameters in module help test any more by @bergmeister in (#1963) -## New Contributors +### New Contributors - @fflaten made their first contribution in (#1897) - @ALiwoto made their first contribution in (#1902) diff --git a/Directory.Build.props b/Directory.Build.props index d2db04cd1..f7d809c1d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.23.0 + 1.24.0 true diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index 36ad283c1..0af4bf2b5 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -1,6 +1,6 @@ --- Download Help Link: https://aka.ms/ps-modules-help -Help Version: 1.23.0 +Help Version: 1.24.0 Locale: en-US Module Guid: d6245802-193d-4068-a631-8863a4342a18 Module Name: PSScriptAnalyzer From ea73edc53a469de79245ade567349478b639224d Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Thu, 20 Mar 2025 15:05:46 -0500 Subject: [PATCH 095/130] Add configuration instructions for UseCorrectCasing (again) (#2090) * Add configuration instructions (again) * Add example for keyword * Fix typo --- docs/Rules/UseCorrectCasing.md | 45 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 6afcb3a4b..1874a7a84 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -1,6 +1,6 @@ --- description: Use exact casing of cmdlet/function/parameter name. -ms.date: 06/28/2023 +ms.date: 03/19/2025 ms.topic: reference title: UseCorrectCasing --- @@ -10,10 +10,16 @@ title: UseCorrectCasing ## Description -This is a style formatting rule. PowerShell is case insensitive where applicable. The casing of -cmdlet names or parameters does not matter but this rule ensures that the casing matches for -consistency and also because most cmdlets/parameters start with an upper case and using that -improves readability to the human eye. +This is a style/formatting rule. PowerShell is case insensitive wherever possible, so the casing of +cmdlet names, parameters, keywords and operators does not matter. This rule nonetheless ensures +consistent casing for clarity and readability. Using lowercase keywords helps distinguish them from +commands. Using lowercase operators helps distinguish them from parameters. + +## How + +- Use exact casing for type names. +- Use exact casing of the cmdlet and its parameters. +- Use lowercase for language keywords and operators. ## Configuration @@ -28,37 +34,42 @@ Rules = @{ } ``` -### Enable: bool (Default value is `$false`) +### Parameters + +#### Enable: bool (Default value is `$false`) Enable or disable the rule during ScriptAnalyzer invocation. -### CheckCommands: bool (Default value is `$true`) +#### CheckCommands: bool (Default value is `$true`) If true, require the case of all operators to be lowercase. -### CheckKeyword: bool (Default value is `$true`) +#### CheckKeyword: bool (Default value is `$true`) If true, require the case of all keywords to be lowercase. -### CheckOperator: bool (Default value is `$true`) +#### CheckOperator: bool (Default value is `$true`) If true, require the case of all commands to match their actual casing. -## How - -Use exact casing of the cmdlet and its parameters, e.g. -`Invoke-Command { 'foo' } -RunAsAdministrator`. +## Examples -## Example - -### Wrong +### Wrong way ```powershell +ForEach ($file in Get-childitem -Recurse) { + $file.Extension -eq '.txt' +} + invoke-command { 'foo' } -runasadministrator ``` -### Correct +### Correct way ```powershell +foreach ($file in Get-ChildItem -Recurse) { + $file.Extension -eq '.txt' +} + Invoke-Command { 'foo' } -RunAsAdministrator ``` From 2287a13e9ce7d96d15675a97f100630892a9f378 Mon Sep 17 00:00:00 2001 From: AlexandraDorey-Magnet <139377801+AlexandraDorey@users.noreply.github.com> Date: Thu, 3 Apr 2025 22:19:25 -0300 Subject: [PATCH 096/130] Fix for PSUseConsistantWhiteSpace when using statement is present (#2091) * Add failng test that should pass * Try simple fix * Test and Fix for test * Update Tests/Rules/UseConsistentWhitespace.tests.ps1 Co-authored-by: Christoph Bergmeister * Fix encoding --------- Co-authored-by: Alexandra Dorey Co-authored-by: Christoph Bergmeister --- Engine/FindAstPositionVisitor.cs | 2 +- Tests/Rules/UseConsistentWhitespace.tests.ps1 | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Engine/FindAstPositionVisitor.cs b/Engine/FindAstPositionVisitor.cs index 05532c456..459581cbc 100644 --- a/Engine/FindAstPositionVisitor.cs +++ b/Engine/FindAstPositionVisitor.cs @@ -333,7 +333,7 @@ public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinit public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatementAst) { - return Visit(usingStatementAst); + return AstVisitAction.Continue; } #endif diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1 index 03a6fbc35..952e49909 100644 --- a/Tests/Rules/UseConsistentWhitespace.tests.ps1 +++ b/Tests/Rules/UseConsistentWhitespace.tests.ps1 @@ -212,6 +212,19 @@ $ht = @{ $ruleConfiguration.CheckSeparator = $false $ruleConfiguration.IgnoreAssignmentOperatorInsideHashTable = $true } + + It "Should not find violation if assignment operator is in multi-line hash table and a using statement is present" { + $def = @' +using namespace System.IO + +$ht = @{ + variable = 3 + other = 4 +} +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + It "Should not find violation if assignment operator is in multi-line hash table" { $def = @' $ht = @{ @@ -628,11 +641,11 @@ bar -h i ` } It "Should fix script when a parameter value is a script block spanning multiple lines" { - $def = {foo { + $def = {foo { bar } -baz} - $expected = {foo { + $expected = {foo { bar } -baz} Invoke-Formatter -ScriptDefinition "$def" -Settings $settings | From 78ee946c1de2ba7120ed956b32d5b40834f0081d Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Fri, 4 Apr 2025 21:47:17 +0100 Subject: [PATCH 097/130] Exclude PSNativeCommandArgumentPassing variable (#2093) * Exclude PSNativeCommandArgumentPassing variable * Update UseDeclaredVarsMoreThanAssignments.tests.ps1 * Add missing comma in SpecialVars.cs --- Engine/SpecialVars.cs | 2 ++ Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index a8be18b92..3416a5ad2 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -160,6 +160,7 @@ internal enum PreferenceVariable internal const string PSEmailServer = "PSEmailServer"; internal const string PSDefaultParameterValues = "PSDefaultParameterValues"; internal const string PSModuleAutoLoadingPreference = "PSModuleAutoLoadingPreference"; + internal const string PSNativeCommandArgumentPassing = "PSNativeCommandArgumentPassing"; internal const string pwd = "PWD"; internal const string Null = "null"; internal const string True = "true"; @@ -182,6 +183,7 @@ internal enum PreferenceVariable PSEmailServer, PSDefaultParameterValues, PSModuleAutoLoadingPreference, + PSNativeCommandArgumentPassing, pwd, Null, True, diff --git a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 index 592aecc91..113563796 100644 --- a/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 +++ b/Tests/Rules/UseDeclaredVarsMoreThanAssignments.tests.ps1 @@ -58,6 +58,12 @@ function MyFunc2() { Should -Be 0 } + It "does not flag `$PSNativeCommandArgumentPassing variable" { + Invoke-ScriptAnalyzer -ScriptDefinition '$PSNativeCommandArgumentPassing=None' -IncludeRule $violationName | ` + Get-Count | ` + Should -Be 0 + } + It "does not flag global variable" { Invoke-ScriptAnalyzer -ScriptDefinition '$global:x=$null' -IncludeRule $violationName | ` Get-Count | ` From ea70855e9d6de8c214757b4dad7b6ed92e8348c8 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Mon, 9 Jun 2025 22:38:57 +0100 Subject: [PATCH 098/130] Update version check in PSScriptAnalyzer.psm1 to align with SMA version for PowerShell 7 (#2107) * Update PSScriptAnalyzer.psm1 * Update Directory.Packages.props --- Directory.Packages.props | 1 + Engine/PSScriptAnalyzer.psm1 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 50150e6ac..d79bb8840 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index 166e48e61..121e96d6f 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -9,7 +9,7 @@ $PSModuleRoot = $PSModule.ModuleBase # Import the appropriate nested binary module based on the current PowerShell version $binaryModuleRoot = $PSModuleRoot -[Version] $minimumPowerShellCoreVersion = '7.2.11' +[Version] $minimumPowerShellCoreVersion = '7.4.7' if ($PSVersionTable.PSVersion.Major -ge 6) { $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" # Minimum PowerShell Core version given by PowerShell Core support itself and From 08bb38b17ddb49aa07e30932440df0bb698dae11 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Tue, 14 Oct 2025 21:27:55 +0100 Subject: [PATCH 099/130] Change how looking up CommandInfo works when using commands in the form module\cmdletName (#2125) --- Engine/CommandInfoCache.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Engine/CommandInfoCache.cs b/Engine/CommandInfoCache.cs index 71c37d83c..aa9d725f3 100644 --- a/Engine/CommandInfoCache.cs +++ b/Engine/CommandInfoCache.cs @@ -80,16 +80,31 @@ public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes /// Returns null if command does not exists private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? commandType) { + string moduleName = null; + string actualCmdName = cmdName; + + // Check if cmdName is in the format "moduleName\CmdletName" (exactly one backslash) + int backslashIndex = cmdName.IndexOf('\\'); + if ( + backslashIndex > 0 && + backslashIndex == cmdName.LastIndexOf('\\') && + backslashIndex != cmdName.Length - 1 && + backslashIndex != 0 + ) + { + moduleName = cmdName.Substring(0, backslashIndex); + actualCmdName = cmdName.Substring(backslashIndex + 1); + } // 'Get-Command ?' would return % for example due to PowerShell interpreting is a single-character-wildcard search and not just the ? alias. // For more details see https://github.com/PowerShell/PowerShell/issues/9308 - cmdName = WildcardPattern.Escape(cmdName); + actualCmdName = WildcardPattern.Escape(actualCmdName); using (var ps = System.Management.Automation.PowerShell.Create()) { ps.RunspacePool = _runspacePool; ps.AddCommand("Get-Command") - .AddParameter("Name", cmdName) + .AddParameter("Name", actualCmdName) .AddParameter("ErrorAction", "SilentlyContinue"); if (commandType != null) @@ -97,6 +112,11 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command ps.AddParameter("CommandType", commandType); } + if (!string.IsNullOrEmpty(moduleName)) + { + ps.AddParameter("Module", moduleName); + } + return ps.Invoke() .FirstOrDefault(); } From dc55078c549b6e7caef7b5a0f5cfb68f98e9ec54 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 15 Oct 2025 19:36:21 +0100 Subject: [PATCH 100/130] PSAvoidDefaultValueForMandatoryParameter: Fix param block and parameter set handling (#2121) * Updated rule * Remove no-longer relevant comment * Inline unnecessary function calls * Tidy up HasMandatoryInAllParameterAttributes --- .../AvoidDefaultValueForMandatoryParameter.cs | 116 ++++++------ ...efaultValueForMandatoryParameter.tests.ps1 | 173 ++++++++++++++++-- 2 files changed, 220 insertions(+), 69 deletions(-) diff --git a/Rules/AvoidDefaultValueForMandatoryParameter.cs b/Rules/AvoidDefaultValueForMandatoryParameter.cs index f3d66d973..17925ae97 100644 --- a/Rules/AvoidDefaultValueForMandatoryParameter.cs +++ b/Rules/AvoidDefaultValueForMandatoryParameter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Management.Automation.Language; #if !CORECLR using System.ComponentModel.Composition; @@ -27,59 +28,73 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - // Finds all functionAst - IEnumerable functionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true); - - foreach (FunctionDefinitionAst funcAst in functionAsts) + // Find all ParameterAst which are children of a ParamBlockAst. This + // doesn't pick up where they appear as children of a + // FunctionDefinitionAst. i.e. + // + // function foo ($a,$b){} -> $a and $b are `ParameterAst` + // + // Include only parameters which have a default value (as without + // one this rule would never alert) + // Include only parameters where ALL parameter attributes have the + // mandatory named argument set to true (implicitly or explicitly) + + var mandatoryParametersWithDefaultValues = + ast.FindAll(testAst => testAst is ParamBlockAst, true) + .Cast() + .Where(pb => pb.Parameters?.Count > 0) + .SelectMany(pb => pb.Parameters) + .Where(paramAst => + paramAst.DefaultValue != null && + HasMandatoryInAllParameterAttributes(paramAst) + ); + + // Report diagnostics for each parameter that violates the rule + foreach (var parameter in mandatoryParametersWithDefaultValues) { - if (funcAst.Body != null && funcAst.Body.ParamBlock != null - && funcAst.Body.ParamBlock.Attributes != null && funcAst.Body.ParamBlock.Parameters != null) - { - foreach (var paramAst in funcAst.Body.ParamBlock.Parameters) - { - bool mandatory = false; - - // check that param is mandatory - foreach (var paramAstAttribute in paramAst.Attributes) - { - if (paramAstAttribute is AttributeAst) - { - var namedArguments = (paramAstAttribute as AttributeAst).NamedArguments; - if (namedArguments != null) - { - foreach (NamedAttributeArgumentAst namedArgument in namedArguments) - { - if (String.Equals(namedArgument.ArgumentName, "mandatory", StringComparison.OrdinalIgnoreCase)) - { - // 3 cases: [Parameter(Mandatory)], [Parameter(Mandatory=$true)] and [Parameter(Mandatory=value)] where value is not equal to 0. - if (namedArgument.ExpressionOmitted - || (String.Equals(namedArgument.Argument.Extent.Text, "$true", StringComparison.OrdinalIgnoreCase)) - || (int.TryParse(namedArgument.Argument.Extent.Text, out int mandatoryValue) && mandatoryValue != 0)) - { - mandatory = true; - break; - } - } - } - } - } - } - - if (!mandatory) - { - break; - } - - if (paramAst.DefaultValue != null) - { - yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.AvoidDefaultValueForMandatoryParameterError, paramAst.Name.VariablePath.UserPath), - paramAst.Name.Extent, GetName(), DiagnosticSeverity.Warning, fileName, paramAst.Name.VariablePath.UserPath); - } - } - } + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidDefaultValueForMandatoryParameterError, + parameter.Name.VariablePath.UserPath + ), + parameter.Name.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + parameter.Name.VariablePath.UserPath + ); } } + /// + /// Determines if a parameter is mandatory in all of its Parameter attributes. + /// A parameter may have multiple [Parameter] attributes for different parameter sets. + /// This method returns true only if ALL [Parameter] attributes have Mandatory=true. + /// + /// The parameter AST to examine + /// String comparer for case-insensitive attribute name matching + /// + /// True if the parameter has at least one [Parameter] attribute and ALL of them + /// have the Mandatory named argument set to true (explicitly or implicitly). + /// False if the parameter has no [Parameter] attributes or if any [Parameter] + /// attribute does not have Mandatory=true. + /// + private static bool HasMandatoryInAllParameterAttributes(ParameterAst paramAst) + { + var parameterAttributes = paramAst.Attributes.OfType() + .Where(attr => string.Equals(attr.TypeName?.Name, "parameter", StringComparison.OrdinalIgnoreCase)); + + return parameterAttributes.Any() && + parameterAttributes.All(attr => + attr.NamedArguments.OfType() + .Any(namedArg => + string.Equals(namedArg.ArgumentName, "mandatory", StringComparison.OrdinalIgnoreCase) && + Helper.Instance.GetNamedArgumentAttributeValue(namedArg) + ) + ); + } + /// /// GetName: Retrieves the name of this rule. /// @@ -134,6 +149,3 @@ public string GetSourceName() } } - - - diff --git a/Tests/Rules/AvoidDefaultValueForMandatoryParameter.tests.ps1 b/Tests/Rules/AvoidDefaultValueForMandatoryParameter.tests.ps1 index a087a97f4..32a62f9be 100644 --- a/Tests/Rules/AvoidDefaultValueForMandatoryParameter.tests.ps1 +++ b/Tests/Rules/AvoidDefaultValueForMandatoryParameter.tests.ps1 @@ -6,37 +6,176 @@ BeforeAll { } Describe "AvoidDefaultValueForMandatoryParameter" { - Context "When there are violations" { - It "has 1 provide default value for mandatory parameter violation with CmdletBinding" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Function foo{ [CmdletBinding()]Param([Parameter(Mandatory)]$Param1=''defaultValue'') }' | - Where-Object { $_.RuleName -eq $ruleName } + + Context "Basic mandatory parameter violations" { + It "should flag mandatory parameter with default value (implicit)" { + $script = 'Function Test { Param([Parameter(Mandatory)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 1 + } + + It "should flag mandatory parameter with default value (explicit true)" { + $script = 'Function Test { Param([Parameter(Mandatory=$true)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 1 + } + + It "should flag mandatory parameter with default value (numeric true)" { + $script = 'Function Test { Param([Parameter(Mandatory=1)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 1 + } + } + + Context "Parameter sets (multiple Parameter attributes)" { + It "should NOT flag parameter mandatory in some but not all parameter sets" { + $script = @' +Function Test { + Param( + [Parameter(Mandatory, ParameterSetName='Set1')] + [Parameter(ParameterSetName='Set2')] + $Param = 'default' + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + + It "should flag parameter mandatory in ALL parameter sets" { + $script = @' +Function Test { + Param( + [Parameter(Mandatory, ParameterSetName='Set1')] + [Parameter(Mandatory, ParameterSetName='Set2')] + $Param = 'default' + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } $violations.Count | Should -Be 1 } - It "has 1 provide default value for mandatory=$true parameter violation without CmdletBinding" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Function foo{ Param([Parameter(Mandatory=$true)]$Param1=''defaultValue'') }' | - Where-Object { $_.RuleName -eq $ruleName } + It "should handle mixed mandatory/non-mandatory in multiple parameter sets" { + $script = @' +Function Test { + Param( + [Parameter(Mandatory=$true, ParameterSetName='Set1')] + [Parameter(Mandatory=$false, ParameterSetName='Set2')] + [Parameter(ParameterSetName='Set3')] + $Param = 'default' + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + } + + Context "Script-level param blocks" { + It "should flag mandatory parameters with defaults in script-level param blocks" { + $script = @' +Param( + [Parameter(Mandatory)] + $ScriptParam = 'default' +) +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } $violations.Count | Should -Be 1 } - It "returns violations when the parameter is specified as mandatory=1 and has a default value" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Function foo{ Param([Parameter(Mandatory=1)]$Param1=''defaultValue'') }' | - Where-Object { $_.RuleName -eq $ruleName } + It "should NOT flag non-mandatory parameters in script-level param blocks" { + $script = @' +Param( + [Parameter()] + $ScriptParam = 'default' +) +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + } + + Context "Non-Parameter attributes" { + It "should NOT flag non-Parameter attributes with Mandatory property" { + $script = 'Function Test { Param([MyCustomAttribute(Mandatory)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + + It "should NOT flag parameters with only validation attributes" { + $script = 'Function Test { Param([ValidateNotNull()]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + } + + Context "Valid scenarios (no violations)" { + It "should NOT flag mandatory parameters without default values" { + $script = 'Function Test { Param([Parameter(Mandatory)]$Param) }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + + It "should NOT flag non-mandatory parameters with default values" { + $script = 'Function Test { Param([Parameter(Mandatory=$false)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + + It "should NOT flag parameters without Parameter attributes" { + $script = 'Function Test { Param($Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + + It "should NOT flag mandatory=0 parameters" { + $script = 'Function Test { Param([Parameter(Mandatory=0)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 0 + } + } + + Context "Complex scenarios" { + It "should handle multiple parameters with mixed violations" { + $script = @' +Function Test { + Param( + [Parameter(Mandatory)]$BadParam = "default", + [Parameter()]$GoodParam = "default", + [Parameter(Mandatory)]$AnotherBadParam = "default" + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 2 + } + + It "should work with CmdletBinding" { + $script = 'Function Test { [CmdletBinding()]Param([Parameter(Mandatory)]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } $violations.Count | Should -Be 1 } } - Context "When there are no violations" { - It "has 1 provide default value for mandatory parameter violation" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Function foo{ Param([Parameter(Mandatory=$false)]$Param1=''val1'', [Parameter(Mandatory)]$Param2=''val2'', $Param3=''val3'') }' | - Where-Object { $_.RuleName -eq $ruleName } + Context "Edge cases" { + It "should handle empty param blocks gracefully" { + $script = 'Function Test { Param() }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } $violations.Count | Should -Be 0 } - It "returns no violations when the parameter is specified as mandatory=0 and has a default value" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Function foo{ Param([Parameter(Mandatory=0)]$Param1=''val1'') }' | - Where-Object { $_.RuleName -eq $ruleName } + It "should handle null/empty default values" { + $script = 'Function Test { Param([Parameter(Mandatory)]$Param = $null) }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } + $violations.Count | Should -Be 1 + } + + It "should handle parameters with multiple non-Parameter attributes" { + $script = 'Function Test { Param([ValidateNotNull()][Alias("P")]$Param = "default") }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $script | Where-Object { $_.RuleName -eq $ruleName } $violations.Count | Should -Be 0 } } + } From a9898a6e2d0a17e8beba652df7a45963e3c39a22 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 22 Oct 2025 21:35:13 +0100 Subject: [PATCH 101/130] Add AvoidReservedWordsAsFunctionNames Rule (#2128) * fix: Update helper GetScriptExtentForFunctionName to return correct extent when function name is 'function' * Add AvoidReservedWordsAsFunctionNames rule * fix: Handle functions with scopes * Copyright header * Extend Substring tests to starts with reserved word, ends with reserved word, and reserved word with a letter missing * Check if the function name is 'Function' case insensitively. Add a test for good measure * Fix casing of common name Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> * AvoidReservedWordsAsFunctionNames: factor our function name for readability Co-authored-by: Patrick Meinecke * Remove reserved words which do not pose an issue: assembly, base, command, hidden, in, inlinescript, interface, module, namespace, private, public, static --------- Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Co-authored-by: Patrick Meinecke --- Engine/Helper.cs | 11 +- Rules/AvoidReservedWordsAsFunctionNames.cs | 103 ++++++++++++ Rules/Strings.resx | 12 ++ Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 2 +- ...voidReservedWordsAsFunctionNames.tests.ps1 | 147 ++++++++++++++++++ .../AvoidReservedWordsAsFunctionNames.md | 42 +++++ docs/Rules/README.md | 1 + 7 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 Rules/AvoidReservedWordsAsFunctionNames.cs create mode 100644 Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1 create mode 100644 docs/Rules/AvoidReservedWordsAsFunctionNames.md diff --git a/Engine/Helper.cs b/Engine/Helper.cs index 82948a4fc..098d8a276 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -761,8 +761,15 @@ public IScriptExtent GetScriptExtentForFunctionName(FunctionDefinitionAst functi token => ContainsExtent(functionDefinitionAst.Extent, token.Extent) && token.Text.Equals(functionDefinitionAst.Name)); - var funcNameToken = funcNameTokens.FirstOrDefault(); - return funcNameToken == null ? null : funcNameToken.Extent; + + // If the functions name is 'function' then the first token in the + // list is the function keyword itself, so we need to skip it + if (functionDefinitionAst.Name.Equals("function", StringComparison.OrdinalIgnoreCase)) + { + var funcNameToken = funcNameTokens.Skip(1).FirstOrDefault() ?? funcNameTokens.FirstOrDefault(); + return funcNameToken?.Extent; + } + return funcNameTokens.FirstOrDefault()?.Extent; } /// diff --git a/Rules/AvoidReservedWordsAsFunctionNames.cs b/Rules/AvoidReservedWordsAsFunctionNames.cs new file mode 100644 index 000000000..921909704 --- /dev/null +++ b/Rules/AvoidReservedWordsAsFunctionNames.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation.Language; +using System.Linq; + +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that warns when reserved words are used as function names + /// + public class AvoidReservedWordsAsFunctionNames : IScriptRule + { + + // The list of PowerShell reserved words. + // https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words + // + // The Below are omitted as they don't pose an issue being a function + // name: + // assembly, base, command, hidden, in, inlinescript, interface, module, + // namespace, private, public, static + static readonly HashSet reservedWords = new HashSet( + new[] { + "begin", "break", "catch", "class", "configuration", + "continue", "data", "define", "do", + "dynamicparam", "else", "elseif", "end", + "enum", "exit", "filter", "finally", + "for", "foreach", "from", "function", + "if", "parallel", "param", "process", + "return", "sequence", "switch", + "throw", "trap", "try", "type", + "until", "using","var", "while", "workflow" + }, + StringComparer.OrdinalIgnoreCase + ); + + /// + /// Analyzes the PowerShell AST for uses of reserved words as function names. + /// + /// The PowerShell Abstract Syntax Tree to analyze. + /// The name of the file being analyzed (for diagnostic reporting). + /// A collection of diagnostic records for each violation. + public IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException(Strings.NullAstErrorMessage); + } + + // Find all FunctionDefinitionAst in the Ast + var functionDefinitions = ast.FindAll( + astNode => astNode is FunctionDefinitionAst, + true + ).Cast(); + + foreach (var function in functionDefinitions) + { + string functionName = Helper.Instance.FunctionNameWithoutScope(function.Name); + if (reservedWords.Contains(functionName)) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidReservedWordsAsFunctionNamesError, + functionName), + Helper.Instance.GetScriptExtentForFunctionName(function) ?? function.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + } + } + + public string GetCommonName() => Strings.AvoidReservedWordsAsFunctionNamesCommonName; + + public string GetDescription() => Strings.AvoidReservedWordsAsFunctionNamesDescription; + + public string GetName() => string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidReservedWordsAsFunctionNamesName); + + public RuleSeverity GetSeverity() => RuleSeverity.Warning; + + public string GetSourceName() => Strings.SourceName; + + public SourceType GetSourceType() => SourceType.Builtin; + } +} \ No newline at end of file diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 260214967..c7645c9cf 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1224,4 +1224,16 @@ AvoidUsingAllowUnencryptedAuthentication + + Avoid reserved words as function names + + + Avoid using reserved words as function names. Using reserved words as function names can cause errors or unexpected behavior in scripts. + + + AvoidReservedWordsAsFunctionNames + + + The reserved word '{0}' was used as a function name. This should be avoided. + \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 8d61c1c7f..c3b744803 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -63,7 +63,7 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 70 + $expectedNumRules = 71 if ($PSVersionTable.PSVersion.Major -le 4) { # for PSv3 PSAvoidGlobalAliases is not shipped because diff --git a/Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1 b/Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1 new file mode 100644 index 000000000..260b9fe3a --- /dev/null +++ b/Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1 @@ -0,0 +1,147 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Keep in sync with the rule's reserved words list in +# Rules/AvoidReservedWordsAsFunctionNames.cs +$reservedWords = @( + 'begin','break','catch','class','configuration', + 'continue','data','define','do', + 'dynamicparam','else','elseif','end', + 'enum','exit','filter','finally', + 'for','foreach','from','function', + 'if','parallel','param','process', + 'return','sequence','switch', + 'throw','trap','try','type', + 'until','using','var','while','workflow' +) + +$randomCasedReservedWords = @( + 'bEgIN','bReAk','cAtCh','CLasS','cONfiGuRaTioN', + 'cONtiNuE','dAtA','dEFInE','Do', + 'DyNaMiCpArAm','eLsE','eLsEiF','EnD', + 'EnUm','eXiT','fIlTeR','fINaLLy', + 'FoR','fOrEaCh','fROm','fUnCtIoN', + 'iF','pArAlLeL','PaRaM','pRoCeSs', + 'ReTuRn','sEqUeNcE','SwItCh', + 'tHrOw','TrAp','tRy','TyPe', + 'uNtIl','UsInG','VaR','wHiLe','wOrKfLoW' +) + +$functionScopes = @( + "global", "local", "script", "private" +) + +# Generate all combinations of reserved words and function scopes +$scopedReservedWordCases = foreach ($scope in $functionScopes) { + foreach ($word in $reservedWords) { + @{ + Scope = $scope + Name = $word + } + } +} + +# Build variants of reserved words where the reserverd word: +# appearing at the start and end of a function +# name. +$substringReservedWords = $reservedWords | ForEach-Object { + "$($_)A", "A$($_)", $_.Substring(0, $_.Length - 1) +} + +$safeFunctionNames = @( + 'Get-Something','Do-Work','Classify-Data','Begin-Process' +) + +BeforeAll { + $ruleName = 'PSAvoidReservedWordsAsFunctionNames' +} + +Describe 'AvoidReservedWordsAsFunctionNames' { + Context 'When function names are reserved words' { + It 'flags reserved word "<_>" as a violation' -TestCases $reservedWords { + + $scriptDefinition = "function $_ { 'test' }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].RuleName | Should -Be $ruleName + # Message text should include the function name as used + $violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided." + # Extent should ideally capture only the function name + $violations[0].Extent.Text | Should -Be $_ + } + + It 'flags the correct extent for a function named Function' { + + $scriptDefinition = "Function Function { 'test' }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].RuleName | Should -Be $ruleName + # Message text should include the function name as used + $violations[0].Message | Should -Be "The reserved word 'Function' was used as a function name. This should be avoided." + # Extent should ideally capture only the function name + $violations[0].Extent.Text | Should -Be 'Function' + + # Make sure the extent is the correct `Function` (not the one at the + # very start) + $violations[0].Extent.StartOffset | Should -not -Be 0 + } + + # Functions can have scopes. So function global:function {} should still + # alert. + It 'flags reserved word "" with scope "" as a violation' -TestCases $scopedReservedWordCases { + param($Scope, $Name) + + $scriptDefinition = "function $($Scope):$($Name) { 'test' }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].RuleName | Should -Be $ruleName + $violations[0].Message | Should -Be "The reserved word '$Name' was used as a function name. This should be avoided." + $violations[0].Extent.Text | Should -Be "$($Scope):$($Name)" + } + + + It 'detects case-insensitively for "<_>"' -TestCases $randomCasedReservedWords { + $scriptDefinition = "function $_ { }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided." + } + + It 'reports one finding per offending function' { + $scriptDefinition = 'function class { };function For { };function Safe-Name { };function TRy { }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + + $violations.Count | Should -Be 3 + $violations | ForEach-Object { $_.Severity | Should -Be 'Warning' } + ($violations | Select-Object -ExpandProperty Extent | Select-Object -ExpandProperty Text) | + Sort-Object | + Should -Be @('class','For','TRy') + } + } + + Context 'When there are no violations' { + It 'does not flag safe function name "<_>"' -TestCases $safeFunctionNames { + $scriptDefinition = "function $_ { }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 0 + } + + It 'does not flag when script has no functions' { + $scriptDefinition = '"hello";$x = 1..3 | ForEach-Object { $_ }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 0 + } + + It 'does not flag substring-like name "<_>"' -TestCases $substringReservedWords { + $scriptDefinition = "function $_ { }" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 0 + } + } +} diff --git a/docs/Rules/AvoidReservedWordsAsFunctionNames.md b/docs/Rules/AvoidReservedWordsAsFunctionNames.md new file mode 100644 index 000000000..cfe6b92ca --- /dev/null +++ b/docs/Rules/AvoidReservedWordsAsFunctionNames.md @@ -0,0 +1,42 @@ +--- +description: Avoid reserved words as function names +ms.date: 08/31/2025 +ms.topic: reference +title: AvoidReservedWordsAsFunctionNames +--- +# AvoidReservedWordsAsFunctionNames + +**Severity Level: Warning** + +## Description + +Avoid using reserved words as function names. Using reserved words as function +names can cause errors or unexpected behavior in scripts. + +## How to Fix + +Avoid using any of the reserved words as function names. Instead, choose a +different name that is not reserved. + +See [`about_Reserved_Words`](https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words) for a list of reserved +words in PowerShell. + +## Example + +### Wrong + +```powershell +# Function is a reserved word +function function { + Write-Host "Hello, World!" +} +``` + +### Correct + +```powershell +# myFunction is not a reserved word +function myFunction { + Write-Host "Hello, World!" +} +``` \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 06f27d2da..da1058bc2 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -23,6 +23,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidMultipleTypeAttributes1](./AvoidMultipleTypeAttributes.md) | Warning | Yes | | | [AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | Yes | | | [AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | Yes | Yes | +| [AvoidReservedWordsAsFunctionNames](./AvoidReservedWordsAsFunctionNames.md) | Warning | Yes | | | [AvoidSemicolonsAsLineTerminators](./AvoidSemicolonsAsLineTerminators.md) | Warning | No | | | [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | | | [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | | From aa2180a6681157102f8f0678b6de73c62bcdce38 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:18:20 -0800 Subject: [PATCH 102/130] Update .NET SDK, PowerShell SDK, and Newtonsoft.Json --- Directory.Packages.props | 4 ++-- Engine/PSScriptAnalyzer.psm1 | 2 +- global.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d79bb8840..f5afcab86 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,11 @@ - + - + diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index 121e96d6f..6d2563a49 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -9,7 +9,7 @@ $PSModuleRoot = $PSModule.ModuleBase # Import the appropriate nested binary module based on the current PowerShell version $binaryModuleRoot = $PSModuleRoot -[Version] $minimumPowerShellCoreVersion = '7.4.7' +[Version] $minimumPowerShellCoreVersion = '7.4.13' if ($PSVersionTable.PSVersion.Major -ge 6) { $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" # Minimum PowerShell Core version given by PowerShell Core support itself and diff --git a/global.json b/global.json index 37239305b..875b92095 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.406", + "version": "8.0.416", "rollForward": "latestFeature" } } From a7b06bbf581305c5556be6ad781c4c016fa15a2d Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:59:07 -0800 Subject: [PATCH 103/130] Add PowerShell Extension GitHub team to Codeowners Otherwise we're stuck unable to merge. --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 983234361..3c53a0af4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ # Default owners -* @andyleejordan @bergmeister +* @PowerShell/extension @bergmeister # Version bumps and documentation updates Directory.Build.props @sdwheeler @michaeltlombardi -/docs/ @sdwheeler @michaeltlombardi +/docs/ @PowerShell/extension @sdwheeler @michaeltlombardi From 7d2b8aec5a96f6bf446a6482089c2e138cc54e27 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 3 Dec 2025 00:45:54 +0000 Subject: [PATCH 104/130] Don't add inner brace whitespace to braces forming a braced member access $a.{Prop}. (#2140) --- Engine/TokenOperations.cs | 160 ++++++++++++++++ Rules/UseConsistentWhitespace.cs | 11 ++ Tests/Engine/TokenOperations.tests.ps1 | 177 ++++++++++++++++++ Tests/Rules/UseConsistentWhitespace.tests.ps1 | 127 +++++++++++++ 4 files changed, 475 insertions(+) diff --git a/Engine/TokenOperations.cs b/Engine/TokenOperations.cs index fa9a1978a..4845ab8c4 100644 --- a/Engine/TokenOperations.cs +++ b/Engine/TokenOperations.cs @@ -245,5 +245,165 @@ public Ast GetAstPosition(Token token) return findAstVisitor.AstPosition; } + /// + /// Returns a list of non-overlapping ranges (startOffset,endOffset) representing the start + /// and end of braced member access expressions. These are member accesses where the name is + /// enclosed in braces. The contents of such braces are treated literally as a member name. + /// Altering the contents of these braces by formatting is likely to break code. + /// + public List> GetBracedMemberAccessRanges() + { + // A list of (startOffset, endOffset) pairs representing the start + // and end braces of braced member access expressions. + var ranges = new List>(); + + var node = tokensLL.Value.First; + while (node != null) + { + switch (node.Value.Kind) + { +#if CORECLR + // TokenKind added in PS7 + case TokenKind.QuestionDot: +#endif + case TokenKind.Dot: + break; + default: + node = node.Next; + continue; + } + + // Note: We don't check if the dot is part of an existing range. When we find + // a valid range, we skip all tokens inside it - so we won't ever evaluate a token + // which already part of a previously found range. + + // Backward scan: + // Determine if this 'dot' is part of a member access. + // Walk left over contiguous comment tokens that are 'touching'. + // After skipping comments, the preceding non-comment token must also be 'touching' + // and one of the expected TokenKinds. + var leftToken = node.Previous; + var rightToken = node; + while (leftToken != null && leftToken.Value.Kind == TokenKind.Comment) + { + if (leftToken.Value.Extent.EndOffset != rightToken.Value.Extent.StartOffset) + { + leftToken = null; + break; + } + rightToken = leftToken; + leftToken = leftToken.Previous; + } + if (leftToken == null) + { + // We ran out of tokens before finding a non-comment token to the left or there + // was intervening whitespace. + node = node.Next; + continue; + } + + if (leftToken.Value.Extent.EndOffset != rightToken.Value.Extent.StartOffset) + { + // There's whitespace between the two tokens + node = node.Next; + continue; + } + + // Limit to valid token kinds that can precede a 'dot' in a member access. + switch (leftToken.Value.Kind) + { + // Note: TokenKind.Number isn't in the list as 5.{Prop} is a syntax error + // (Unexpected token). Numbers also have no properties - only methods. + case TokenKind.Variable: + case TokenKind.Identifier: + case TokenKind.StringLiteral: + case TokenKind.StringExpandable: + case TokenKind.HereStringLiteral: + case TokenKind.HereStringExpandable: + case TokenKind.RParen: + case TokenKind.RCurly: + case TokenKind.RBracket: + // allowed + break; + default: + // not allowed + node = node.Next; + continue; + } + + // Forward Scan: + // Check that the next significant token is an LCurly + // Starting from the token after the 'dot', walk right skipping trivia tokens: + // - Comment + // - NewLine + // - LineContinuation (`) + // These may be multi-line and need not be 'touching' the dot. + // The first non-trivia token encountered must be an opening curly brace (LCurly) for + // this dot to begin a braced member access. If it is not LCurly or we run out + // of tokens, this dot is ignored. + var scan = node.Next; + while (scan != null) + { + if ( + scan.Value.Kind == TokenKind.Comment || + scan.Value.Kind == TokenKind.NewLine || + scan.Value.Kind == TokenKind.LineContinuation + ) + { + scan = scan.Next; + continue; + } + break; + } + + // If we reached the end without finding a significant token, or if the found token + // is not LCurly, continue. + if (scan == null || scan.Value.Kind != TokenKind.LCurly) + { + node = node.Next; + continue; + } + + // We have a valid token, followed by a dot, followed by an LCurly. + // Find the matching RCurly and create the range. + var lCurlyNode = scan; + + // Depth count braces to find the RCurly which closes the LCurly. + int depth = 0; + LinkedListNode rcurlyNode = null; + while (scan != null) + { + if (scan.Value.Kind == TokenKind.LCurly) depth++; + else if (scan.Value.Kind == TokenKind.RCurly) + { + depth--; + if (depth == 0) + { + rcurlyNode = scan; + break; + } + } + scan = scan.Next; + } + + // If we didn't find a matching RCurly, something has gone wrong. + // Should an unmatched pair be caught by the parser as a parse error? + if (rcurlyNode == null) + { + node = node.Next; + continue; + } + + ranges.Add(new Tuple( + lCurlyNode.Value.Extent.StartOffset, + rcurlyNode.Value.Extent.EndOffset + )); + + // Skip all tokens inside the excluded range. + node = rcurlyNode.Next; + } + + return ranges; + } } } diff --git a/Rules/UseConsistentWhitespace.cs b/Rules/UseConsistentWhitespace.cs index e6d4cff99..7f7550ffe 100644 --- a/Rules/UseConsistentWhitespace.cs +++ b/Rules/UseConsistentWhitespace.cs @@ -257,6 +257,9 @@ private IEnumerable FindOpenBraceViolations(TokenOperations to private IEnumerable FindInnerBraceViolations(TokenOperations tokenOperations) { + // Ranges which represent braced member access. Tokens within these ranges should be + // excluded from formatting. + var exclusionRanges = tokenOperations.GetBracedMemberAccessRanges(); foreach (var lCurly in tokenOperations.GetTokenNodes(TokenKind.LCurly)) { if (lCurly.Next == null @@ -264,6 +267,10 @@ private IEnumerable FindInnerBraceViolations(TokenOperations t || lCurly.Next.Value.Kind == TokenKind.NewLine || lCurly.Next.Value.Kind == TokenKind.LineContinuation || lCurly.Next.Value.Kind == TokenKind.RCurly + || exclusionRanges.Any(range => + lCurly.Value.Extent.StartOffset >= range.Item1 && + lCurly.Value.Extent.EndOffset <= range.Item2 + ) ) { continue; @@ -290,6 +297,10 @@ private IEnumerable FindInnerBraceViolations(TokenOperations t || rCurly.Previous.Value.Kind == TokenKind.NewLine || rCurly.Previous.Value.Kind == TokenKind.LineContinuation || rCurly.Previous.Value.Kind == TokenKind.AtCurly + || exclusionRanges.Any(range => + rCurly.Value.Extent.StartOffset >= range.Item1 && + rCurly.Value.Extent.EndOffset <= range.Item2 + ) ) { continue; diff --git a/Tests/Engine/TokenOperations.tests.ps1 b/Tests/Engine/TokenOperations.tests.ps1 index 97fef3958..1bb1d9298 100644 --- a/Tests/Engine/TokenOperations.tests.ps1 +++ b/Tests/Engine/TokenOperations.tests.ps1 @@ -18,4 +18,181 @@ $h = @{ $hashTableAst | Should -BeOfType [System.Management.Automation.Language.HashTableAst] $hashTableAst.Extent.Text | Should -Be '@{ z = "hi" }' } + + Context 'Braced Member Access Ranges' { + + BeforeDiscovery { + $RangeTests = @( + @{ + Name = 'No braced member access' + ScriptDef = '$object.Prop' + ExpectedRanges = @() + } + @{ + Name = 'No braced member access on braced variable name' + ScriptDef = '${object}.Prop' + ExpectedRanges = @() + } + @{ + Name = 'Braced member access' + ScriptDef = '$object.{Prop}' + ExpectedRanges = @( + ,@(8, 14) + ) + } + @{ + Name = 'Braced member access with spaces' + ScriptDef = '$object. { Prop }' + ExpectedRanges = @( + ,@(9, 17) + ) + } + @{ + Name = 'Braced member access with newline' + ScriptDef = "`$object.`n{ Prop }" + ExpectedRanges = @( + ,@(9, 17) + ) + } + @{ + Name = 'Braced member access with comment' + ScriptDef = "`$object. <#comment#>{Prop}" + ExpectedRanges = @( + ,@(20, 26) + ) + } + @{ + Name = 'Braced member access with multi-line comment' + ScriptDef = "`$object. <#`ncomment`n#>{Prop}" + ExpectedRanges = @( + ,@(22, 28) + ) + } + @{ + Name = 'Braced member access with inline comment' + ScriptDef = "`$object. #comment`n{Prop}" + ExpectedRanges = @( + ,@(18, 24) + ) + } + @{ + Name = 'Braced member access with inner curly braces' + ScriptDef = "`$object.{{Prop}}" + ExpectedRanges = @( + ,@(8, 16) + ) + } + @{ + Name = 'Indexed Braced member access' + ScriptDef = "`$object[0].{Prop}" + ExpectedRanges = @( + ,@(11, 17) + ) + } + @{ + Name = 'Parenthesized Braced member access' + ScriptDef = "(`$object).{Prop}" + ExpectedRanges = @( + ,@(10, 16) + ) + } + @{ + Name = 'Chained Braced member access' + ScriptDef = "`$object.{Prop}.{InnerProp}" + ExpectedRanges = @( + ,@(8, 14) + ,@(15, 26) + ) + } + @{ + Name = 'Multiple Braced member access in larger script' + ScriptDef = @' +$var = 1 +$a.prop.{{inner}} +$a.{ + $a.{Prop} +} +'@ + ExpectedRanges = @( + ,@(17, 26) + ,@(30, 47) + ) + } + ) + } + + It 'Should correctly identify range for ' -ForEach $RangeTests { + $tokens = $null + $parseErrors = $null + $scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($ScriptDef, [ref] $tokens, [ref] $parseErrors) + $tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst) + $ranges = $tokenOperations.GetBracedMemberAccessRanges() + $ranges.Count | Should -Be $ExpectedRanges.Count + for ($i = 0; $i -lt $ranges.Count; $i++) { + $ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0] + $ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1] + } + } + + It 'Should not identify dot-sourcing as braced member access' { + $scriptText = @' +. {5+5} +$a=4;. {10+15} +'@ + $tokens = $null + $parseErrors = $null + $scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors) + $tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst) + $ranges = $tokenOperations.GetBracedMemberAccessRanges() + $ranges.Count | Should -Be 0 + } + + It 'Should not return a range for an incomplete bracket pair (parse error)' { + $scriptText = @' +$object.{MemberName +'@ + $tokens = $null + $parseErrors = $null + $scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors) + $tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst) + $ranges = $tokenOperations.GetBracedMemberAccessRanges() + $ranges.Count | Should -Be 0 + } + + It 'Should find the correct range for null-conditional braced member access' { + $scriptText = '$object?.{Prop}' + $tokens = $null + $parseErrors = $null + $scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors) + $tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst) + $ranges = $tokenOperations.GetBracedMemberAccessRanges() + $ranges.Count | Should -Be 1 + $ExpectedRanges = @( + ,@(9, 15) + ) + for ($i = 0; $i -lt $ranges.Count; $i++) { + $ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0] + $ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1] + } + } -Skip:$($PSVersionTable.PSVersion.Major -lt 7) + + It 'Should find the correct range for nested null-conditional braced member access' { + $scriptText = '$object?.{Prop?.{InnerProp}}' + $tokens = $null + $parseErrors = $null + $scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors) + $tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst) + $ranges = $tokenOperations.GetBracedMemberAccessRanges() + $ranges.Count | Should -Be 1 + $ExpectedRanges = @( + ,@(9, 28) + ) + for ($i = 0; $i -lt $ranges.Count; $i++) { + $ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0] + $ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1] + } + } -Skip:$($PSVersionTable.PSVersion.Major -lt 7) + + } + } \ No newline at end of file diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1 index 952e49909..c2013fa31 100644 --- a/Tests/Rules/UseConsistentWhitespace.tests.ps1 +++ b/Tests/Rules/UseConsistentWhitespace.tests.ps1 @@ -684,4 +684,131 @@ bar -h i ` Should -Be $expected } } + + Context "Braced Member Accessor Handling" { + BeforeAll { + $ruleConfiguration.CheckInnerBrace = $true + $ruleConfiguration.CheckOpenBrace = $false + $ruleConfiguration.CheckOpenParen = $false + $ruleConfiguration.CheckOperator = $false + $ruleConfiguration.CheckPipe = $false + $ruleConfiguration.CheckSeparator = $false + $ruleConfiguration.CheckParameter = $false + } + + It 'Should not find a violation for a simple braced member accessor with no whitespace' { + $def = '$variable.{Property}' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a simple braced member accessor with whitespace after dot' { + $def = '$object. {Member}' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a simple braced member accessor with newline after dot' { + $def = "`$object.`n{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a simple braced member accessor with inline comment after dot' { + $def = "`$object.<#comment#>{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a simple braced member accessor with inline comment before dot' { + $def = "`$object<#comment#>.{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a simple braced member accessor with multiple touching inline comment before dot' { + $def = "`$object<#a#><#b#><#c#><#d#>.{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for an indexed braced member accessor' { + $def = "`$object[0].{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a parenthesized braced member accessor' { + $def = "(`$object).{Member}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a string literal braced member accessor' { + $def = "'StringLiteral'.{Length}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for an expandable string literal braced member accessor' { + $def = "`"StringLiteral`".{Length}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a here-string braced member accessor' { + $def = "@' +string +'@.{Length}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a doublequoted here-string braced member accessor' { + $def = "@`" +string +`"@.{Length}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for a type braced member accessor' { + $def = "[System.DayOfWeek].{Assembly}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for an braced member accessor on an identifier' { + $def = "`$Object.Property.{Prop}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for an braced member accessor with nested braces' { + $def = "`$Object.{{Prop}}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + It 'Should not find a violation for an braced member accessor with nested inner dot' { + $def = "`$Object.{`$InnerObject.{Prop}}" + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } + + # Check that dot-sourcing still causes formatting violations + It 'Should find violations for dot-sourcing a script (no space after dot)' { + $def = '.{5+5}' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 2 + } + + It 'Should find violations for dot-sourcing a script (space after dot)' { + $def = '. {5+5}' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 2 + } + + It 'Should find violations for dot-sourcing a script (Semi-Colon before dot)' { + $def = '$a = 4;. {5+5}' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 2 + } + + # PS7 Specific behaviour. QuestionDot token. + It 'Should not find a violation for a null conditional braced member accessor' { + $def = '${Object}?.{Prop}' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } -Skip:$($PSVersionTable.PSVersion.Major -lt 7) + + It 'Should not find a violation for a nested null conditional braced member accessor' { + $def = '${Object}?.{${InnerObject}?.{Prop}}' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Should -BeNullOrEmpty + } -Skip:$($PSVersionTable.PSVersion.Major -lt 7) + + } } From 4ad5980046cba7e98cd04005ef705457068b39ec Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 3 Dec 2025 01:10:21 +0000 Subject: [PATCH 105/130] Update docs and diagnostic message for UseCorrectCasing (#2138) * Inline the 2 overloads of GetDiagnosticRecord to their individual call sites It made it cumbersome to pass a different number of arguments to the string.Format call and didn't seem necessary * Add tests for diagnostic messages related to casing of functions, parameters, and operators * Remove errant space in rule name * Swap over and expand descriptions of CheckCommands and CheckOperator * Update the 'Wrong way' section to use an operator in the wrong way * inline local message var --------- Co-authored-by: Christoph Bergmeister --- Rules/UseCorrectCasing.cs | 55 ++++++++++++-------------- Tests/Rules/UseCorrectCasing.tests.ps1 | 21 ++++++++++ docs/Rules/UseCorrectCasing.md | 8 ++-- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/Rules/UseCorrectCasing.cs b/Rules/UseCorrectCasing.cs index 5d1393d45..f4f2c40b7 100644 --- a/Rules/UseCorrectCasing.cs +++ b/Rules/UseCorrectCasing.cs @@ -103,7 +103,19 @@ public override IEnumerable AnalyzeScript(Ast ast, string file if (!commandName.Equals(correctlyCasedCommandName, StringComparison.Ordinal)) { - yield return GetDiagnosticRecord(commandAst, fileName, correctlyCasedCommandName, Strings.UseCorrectCasingError); + var extent = GetCommandExtent(commandAst); + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseCorrectCasingError, + commandName, + correctlyCasedCommandName), + extent, + GetName(), + DiagnosticSeverity.Information, + fileName, + correctlyCasedCommandName, + GetCorrectionExtent(commandAst, extent, correctlyCasedCommandName)); } var commandParameterAsts = commandAst.FindAll( @@ -129,7 +141,19 @@ public override IEnumerable AnalyzeScript(Ast ast, string file var correctlyCasedParameterName = parameterMetaData.Name; if (!parameterName.Equals(correctlyCasedParameterName, StringComparison.Ordinal)) { - yield return GetDiagnosticRecord(commandParameterAst, fileName, correctlyCasedParameterName, Strings.UseCorrectCasingError); + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseCorrectCasingParameterError, + commandParameterAst.Extent.Text, + commandName, + correctlyCasedParameterName), + commandParameterAst.Extent, + GetName(), + DiagnosticSeverity.Information, + fileName, + correctlyCasedParameterName, + GetCorrectionExtent(commandParameterAst, commandParameterAst.Extent, correctlyCasedParameterName)); } } } @@ -137,7 +161,6 @@ public override IEnumerable AnalyzeScript(Ast ast, string file } } - /// /// For a command like "gci -path c:", returns the extent of "gci" in the command /// @@ -197,32 +220,6 @@ private DiagnosticRecord GetDiagnosticRecord(Token token, string fileName, strin suggestedCorrections: extents); } - private DiagnosticRecord GetDiagnosticRecord(Ast ast, string fileName, string correction, string message) - { - var extent = ast is CommandAst ? GetCommandExtent((CommandAst)ast) : ast.Extent; - return new DiagnosticRecord( - string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), - extent, - GetName(), - DiagnosticSeverity.Information, - fileName, - correction, - suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); - } - - private DiagnosticRecord GetDiagnosticRecord(CommandParameterAst ast, string fileName, string correction, string message) - { - var extent = ast.Extent; - return new DiagnosticRecord( - string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction), - extent, - GetName(), - DiagnosticSeverity.Information, - fileName, - correction, - suggestedCorrections: GetCorrectionExtent(ast, extent, correction)); - } - /// /// GetName: Retrieves the name of this rule. /// diff --git a/Tests/Rules/UseCorrectCasing.tests.ps1 b/Tests/Rules/UseCorrectCasing.tests.ps1 index c142dd2da..7caafe4e0 100644 --- a/Tests/Rules/UseCorrectCasing.tests.ps1 +++ b/Tests/Rules/UseCorrectCasing.tests.ps1 @@ -103,6 +103,27 @@ Describe "UseCorrectCasing" { Should -BeExactly '$A++; $B--' } + It "Shows relevant diagnostic message for function/command name casing" { + $settings = @{ 'Rules' = @{ 'PSUseCorrectCasing' = @{ 'Enable' = $true; CheckCommands = $true; CheckKeywords = $true; CheckOperators = $true } } } + $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'WHERE-OBJECT Name -EQ "Value"' -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Be "Function/Cmdlet 'WHERE-OBJECT' does not match its exact casing 'Where-Object'." + } + + It "Shows relevant diagnostic message for parameter casing" { + $settings = @{ 'Rules' = @{ 'PSUseCorrectCasing' = @{ 'Enable' = $true; CheckCommands = $true; CheckKeywords = $true; CheckOperators = $true } } } + $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Where-Object Name -eq "Value"' -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Be "Parameter '-eq' of function/cmdlet 'Where-Object' does not match its exact casing 'EQ'." + } + + It "Shows relevant diagnostic message for operator casing" { + $settings = @{ 'Rules' = @{ 'PSUseCorrectCasing' = @{ 'Enable' = $true; CheckCommands = $true; CheckKeywords = $true; CheckOperators = $true } } } + $violations = Invoke-ScriptAnalyzer -ScriptDefinition '$a -EQ 1' -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Be "Operator '-EQ' does not match the expected case '-eq'." + } + Context "Inconsistent Keywords" { It "Corrects keyword case" { Invoke-Formatter 'ForEach ($x IN $y) { $x }' | diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 1874a7a84..9d7e96a18 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -25,7 +25,7 @@ commands. Using lowercase operators helps distinguish them from parameters. ```powershell Rules = @{ - PS UseCorrectCasing = @{ + PSUseCorrectCasing = @{ Enable = $true CheckCommands = $true CheckKeyword = $true @@ -42,7 +42,7 @@ Enable or disable the rule during ScriptAnalyzer invocation. #### CheckCommands: bool (Default value is `$true`) -If true, require the case of all operators to be lowercase. +If true, require the case of all command and parameter names to match their canonical casing. #### CheckKeyword: bool (Default value is `$true`) @@ -50,7 +50,7 @@ If true, require the case of all keywords to be lowercase. #### CheckOperator: bool (Default value is `$true`) -If true, require the case of all commands to match their actual casing. +If true, require the case of all operators (e.g. -eq, -ne, -gt) to be lowercase. ## Examples @@ -58,7 +58,7 @@ If true, require the case of all commands to match their actual casing. ```powershell ForEach ($file in Get-childitem -Recurse) { - $file.Extension -eq '.txt' + $file.Extension -EQ '.txt' } invoke-command { 'foo' } -runasadministrator From 67ffb24e9f86384931ae89f8961f2c80e792df7e Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Tue, 2 Dec 2025 17:26:37 -0800 Subject: [PATCH 106/130] Fix SuppressMessage CustomRule (#2142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix RuleSuppressionID not working with named arguments Problem: SuppressMessageAttribute failed when using named arguments for RuleSuppressionID. Users could not use syntax like: [SuppressMessage("RuleName", RuleSuppressionId="MyId")] Root Cause: In RuleSuppression.cs, the named argument parser had two bugs: 1. Checked if RuleName was set instead of RuleSuppressionID 2. Assigned the value to RuleName instead of RuleSuppressionID This broke selective rule suppression for custom rules. Solution: - Fixed conflict check to validate RuleSuppressionID instead of RuleName - Fixed assignment to set RuleSuppressionID instead of RuleName - Added comprehensive tests for named argument syntax - Minor formatting improvements Now both syntaxes work correctly: [SuppressMessage("Rule", RuleSuppressionId="Id", Scope="Function")] [SuppressMessage("Rule", "Id", Scope="Function")] * test: ✨ Add test for custom rule suppression with RuleSuppressionID * Implemented a test case to validate the functionality of custom rules with targeted suppression. * The test recreates the scenario from GitHub issue #1686, ensuring that `RuleSuppressionID` works correctly with named arguments. * Verified that violations are suppressed as expected based on the defined rules. --- Engine/Generic/RuleSuppression.cs | 8 +- Tests/Engine/RuleSuppression.tests.ps1 | 191 ++++++++++++++++++++++--- 2 files changed, 178 insertions(+), 21 deletions(-) diff --git a/Engine/Generic/RuleSuppression.cs b/Engine/Generic/RuleSuppression.cs index d912eee0c..7daab3e86 100644 --- a/Engine/Generic/RuleSuppression.cs +++ b/Engine/Generic/RuleSuppression.cs @@ -193,12 +193,12 @@ public RuleSuppression(AttributeAst attrAst, int start, int end) } else if (argumentName.Equals("rulesuppressionid", StringComparison.OrdinalIgnoreCase)) { - if (!String.IsNullOrWhiteSpace(RuleName)) + if (!String.IsNullOrWhiteSpace(RuleSuppressionID)) { Error = String.Format(Strings.NamedAndPositionalArgumentsConflictError, name); } - RuleName = (name.Argument as StringConstantExpressionAst).Value; + RuleSuppressionID = (name.Argument as StringConstantExpressionAst).Value; } else if (argumentName.Equals("scope", StringComparison.OrdinalIgnoreCase)) { @@ -333,12 +333,12 @@ public static List GetSuppressions(IEnumerable at { targetAsts = scopeAst.FindAll(ast => ast is FunctionDefinitionAst && reg.IsMatch((ast as FunctionDefinitionAst).Name), true); } - #if !(PSV3 || PSV4) +#if !(PSV3 || PSV4) else if (scope.Equals("class", StringComparison.OrdinalIgnoreCase)) { targetAsts = scopeAst.FindAll(ast => ast is TypeDefinitionAst && reg.IsMatch((ast as TypeDefinitionAst).Name), true); } - #endif +#endif if (targetAsts != null) { diff --git a/Tests/Engine/RuleSuppression.tests.ps1 b/Tests/Engine/RuleSuppression.tests.ps1 index c014dbc12..2d81f6305 100644 --- a/Tests/Engine/RuleSuppression.tests.ps1 +++ b/Tests/Engine/RuleSuppression.tests.ps1 @@ -56,28 +56,28 @@ Describe "RuleSuppressionWithoutScope" { It "Suppresses rule with extent created using ScriptExtent constructor" { Invoke-ScriptAnalyzer ` - -ScriptDefinition $ruleSuppressionAvoidUsernameAndPassword ` - -IncludeRule "PSAvoidUsingUserNameAndPassWordParams" ` - -OutVariable ruleViolations ` - -SuppressedOnly + -ScriptDefinition $ruleSuppressionAvoidUsernameAndPassword ` + -IncludeRule "PSAvoidUsingUserNameAndPassWordParams" ` + -OutVariable ruleViolations ` + -SuppressedOnly $ruleViolations.Count | Should -Be 1 - } + } } Context "Script" { It "Does not raise violations" { - $suppression = $violations | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } + $suppression = $violations | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" } $suppression.Count | Should -Be 0 - $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } + $suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" } $suppression.Count | Should -Be 0 } } Context "RuleSuppressionID" { It "Only suppress violations for that ID" { - $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } + $suppression = $violations | Where-Object { $_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } $suppression.Count | Should -Be 1 - $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } + $suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } $suppression.Count | Should -Be 1 } @@ -93,10 +93,10 @@ function SuppressPwdParam() } '@ Invoke-ScriptAnalyzer ` - -ScriptDefinition $ruleSuppressionIdAvoidPlainTextPassword ` - -IncludeRule "PSAvoidUsingPlainTextForPassword" ` - -OutVariable ruleViolations ` - -SuppressedOnly + -ScriptDefinition $ruleSuppressionIdAvoidPlainTextPassword ` + -IncludeRule "PSAvoidUsingPlainTextForPassword" ` + -OutVariable ruleViolations ` + -SuppressedOnly $ruleViolations.Count | Should -Be 1 } @@ -246,8 +246,165 @@ function MyFunc } } + Context "RuleSuppressionID with named arguments" { + It "Should work with named argument syntax" { + $scriptWithNamedArgs = @' +function SuppressPasswordParam() +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(RuleName="PSAvoidUsingPlainTextForPassword", RuleSuppressionId="password1")] + param( + [string] $password1, + [string] $password2 + ) +} +'@ + + $diagnostics = Invoke-ScriptAnalyzer ` + -ScriptDefinition $scriptWithNamedArgs ` + -IncludeRule "PSAvoidUsingPlainTextForPassword" + $suppressions = Invoke-ScriptAnalyzer ` + -ScriptDefinition $scriptWithNamedArgs ` + -IncludeRule "PSAvoidUsingPlainTextForPassword" ` + -SuppressedOnly + + # There should be one unsuppressed diagnostic (password2) and one suppressed diagnostic (password1) + $diagnostics | Should -HaveCount 1 + $diagnostics[0].RuleName | Should -BeExactly "PSAvoidUsingPlainTextForPassword" + $diagnostics[0].RuleSuppressionID | Should -BeExactly "password2" + + $suppressions | Should -HaveCount 1 + $suppressions[0].RuleName | Should -BeExactly "PSAvoidUsingPlainTextForPassword" + $suppressions[0].RuleSuppressionID | Should -BeExactly "password1" + } + + It "Should work with mixed positional and named argument syntax" { + $scriptWithMixedArgs = @' +function SuppressPasswordParam() +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", Scope="Function")] + param( + [string] $password1, + [string] $password2 + ) +} +'@ + + $diagnostics = Invoke-ScriptAnalyzer ` + -ScriptDefinition $scriptWithMixedArgs ` + -IncludeRule "PSAvoidUsingPlainTextForPassword" + + # All violations should be suppressed since there's no RuleSuppressionID filtering + $diagnostics | Should -HaveCount 0 + } + + It "Should work with custom rule from issue #1686 comment" { + # This test recreates the exact scenario from GitHub issue 1686 comment + # with a custom rule that populates RuleSuppressionID for targeted suppression + + # Custom rule module that creates violations with specific RuleSuppressionIDs + $customRuleScript = @' +function Measure-AvoidFooBarCommand { + [CmdletBinding()] + [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + $results = @() + + # Find all command expressions + $commandAsts = $ScriptBlockAst.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.CommandAst] + }, $true) + + foreach ($commandAst in $commandAsts) { + $commandName = $commandAst.GetCommandName() + if ($commandName -match '^(Get-FooBar|Set-FooBar)$') { + # Create a diagnostic with the command name as RuleSuppressionID + $result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new( + "Avoid using $commandName command", + $commandAst.Extent, + 'Measure-AvoidFooBarCommand', + 'Warning', + $null, + $commandName # This becomes the RuleSuppressionID + ) + $results += $result + } + } + + return $results +} + +Export-ModuleMember -Function Measure-AvoidFooBarCommand +'@ + + # Script that uses the custom rule with targeted suppression + $scriptWithCustomRuleSuppression = @' +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Measure-AvoidFooBarCommand', RuleSuppressionId = 'Get-FooBar', Scope = 'Function', Target = 'Allow-GetFooBar')] +param() + +function Test-BadCommands { + Get-FooBar # Line 6 - Should NOT be suppressed (wrong function) + Set-FooBar # Line 7 - Should NOT be suppressed (different RuleSuppressionID) +} + +function Allow-GetFooBar { + Get-FooBar # Line 11 - Should be suppressed (matches RuleSuppressionId and Target) + Set-FooBar # Line 12 - Should NOT be suppressed (different RuleSuppressionID) +} +'@ + + # Save custom rule to temporary file + $customRuleFile = [System.IO.Path]::GetTempFileName() + $customRuleModuleFile = [System.IO.Path]::ChangeExtension($customRuleFile, '.psm1') + Set-Content -Path $customRuleModuleFile -Value $customRuleScript + + try + { + # Check suppressed violations - this is the key test for our fix + $suppressions = Invoke-ScriptAnalyzer ` + -ScriptDefinition $scriptWithCustomRuleSuppression ` + -CustomRulePath $customRuleModuleFile ` + -SuppressedOnly ` + -ErrorAction SilentlyContinue + + # The core functionality: RuleSuppressionID with named arguments should work for custom rules + # We should have at least one suppressed Get-FooBar violation + $suppressions | Should -Not -BeNullOrEmpty -Because "RuleSuppressionID named arguments should work for custom rules" + + $getFooBarSuppressions = $suppressions | Where-Object { $_.RuleSuppressionID -eq 'Get-FooBar' } + $getFooBarSuppressions | Should -Not -BeNullOrEmpty -Because "Get-FooBar should be suppressed based on RuleSuppressionID" + + # Verify the suppression occurred in the right function (Allow-GetFooBar) + $getFooBarSuppressions | Should -Not -BeNullOrEmpty + $getFooBarSuppressions[0].RuleName | Should -BeExactly 'Measure-AvoidFooBarCommand' + + # Get unsuppressed violations to verify selective suppression + $diagnostics = Invoke-ScriptAnalyzer ` + -ScriptDefinition $scriptWithCustomRuleSuppression ` + -CustomRulePath $customRuleModuleFile ` + -ErrorAction SilentlyContinue + + # Should still have violations for Set-FooBar (different RuleSuppressionID) and Get-FooBar in wrong function + $setFooBarViolations = $diagnostics | Where-Object { $_.RuleSuppressionID -eq 'Set-FooBar' } + $setFooBarViolations | Should -Not -BeNullOrEmpty -Because "Set-FooBar should not be suppressed (different RuleSuppressionID)" + + } + finally + { + Remove-Item -Path $customRuleModuleFile -ErrorAction SilentlyContinue + Remove-Item -Path $customRuleFile -ErrorAction SilentlyContinue + } + } + } + Context "Rule suppression within DSC Configuration definition" { - It "Suppresses rule" -skip:($IsLinux -or $IsMacOS -or ($PSVersionTable.PSVersion.Major -lt 5)) { + It "Suppresses rule" -Skip:($IsLinux -or $IsMacOS -or ($PSVersionTable.PSVersion.Major -lt 5)) { $suppressedRule = Invoke-ScriptAnalyzer -ScriptDefinition $ruleSuppressionInConfiguration -SuppressedOnly $suppressedRule.Count | Should -Be 1 } @@ -281,9 +438,9 @@ function MyFunc Describe "RuleSuppressionWithScope" { Context "FunctionScope" { It "Does not raise violations" { - $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } + $suppression = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingPositionalParameters" } $suppression.Count | Should -Be 0 - $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } + $suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingPositionalParameters" } $suppression.Count | Should -Be 0 } } @@ -353,4 +510,4 @@ Describe "RuleSuppressionWithScope" { $suppressed.Count | Should -Be 1 } } - } +} From e2804796f43eab60f5d105f6bbaea83fb446a13d Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Wed, 3 Dec 2025 14:22:04 -0600 Subject: [PATCH 107/130] Sync rules docs from docs repo (#2144) * Sync rules docs from docs repo * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update codeowners --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- docs/Rules/AvoidLongLines.md | 15 ++++++++------- docs/Rules/AvoidReservedWordsAsFunctionNames.md | 16 +++++++++------- docs/Rules/UseCorrectCasing.md | 14 +++++++------- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3c53a0af4..8de3ce904 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,5 +2,5 @@ * @PowerShell/extension @bergmeister # Version bumps and documentation updates -Directory.Build.props @sdwheeler @michaeltlombardi +Directory.Build.props @PowerShell/extension @sdwheeler @michaeltlombardi /docs/ @PowerShell/extension @sdwheeler @michaeltlombardi diff --git a/docs/Rules/AvoidLongLines.md b/docs/Rules/AvoidLongLines.md index cc2603c51..c36daaa86 100644 --- a/docs/Rules/AvoidLongLines.md +++ b/docs/Rules/AvoidLongLines.md @@ -1,6 +1,6 @@ --- description: Avoid long lines -ms.date: 06/28/2023 +ms.date: 04/29/2025 ms.topic: reference title: AvoidLongLines --- @@ -10,10 +10,11 @@ title: AvoidLongLines ## Description -Lines should be no longer than a configured number of characters (default: 120), including leading -whitespace (indentation). +The length of lines, including leading spaces (indentation), should be less than the configured number +of characters. The default length is 120 characters. -**Note**: This rule is not enabled by default. The user needs to enable it through settings. +> [!NOTE] +> This rule isn't enabled by default. The user needs to enable it through settings. ## Configuration @@ -26,12 +27,12 @@ Rules = @{ } ``` -### Parameters +## Parameters -#### Enable: bool (Default value is `$false`) +### `Enable`: bool (Default value is `$false`) Enable or disable the rule during ScriptAnalyzer invocation. -#### MaximumLineLength: int (Default value is 120) +### `MaximumLineLength`: int (Default value is 120) Optional parameter to override the default maximum line length. diff --git a/docs/Rules/AvoidReservedWordsAsFunctionNames.md b/docs/Rules/AvoidReservedWordsAsFunctionNames.md index cfe6b92ca..769cbb85f 100644 --- a/docs/Rules/AvoidReservedWordsAsFunctionNames.md +++ b/docs/Rules/AvoidReservedWordsAsFunctionNames.md @@ -10,16 +10,15 @@ title: AvoidReservedWordsAsFunctionNames ## Description -Avoid using reserved words as function names. Using reserved words as function -names can cause errors or unexpected behavior in scripts. +Avoid using reserved words as function names. Using reserved words as function names can cause +errors or unexpected behavior in scripts. ## How to Fix -Avoid using any of the reserved words as function names. Instead, choose a -different name that is not reserved. +Avoid using any of the reserved words as function names. Choose a different name that's not a +reserved word. -See [`about_Reserved_Words`](https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words) for a list of reserved -words in PowerShell. +See [about_Reserved_Words][01] for a list of reserved words in PowerShell. ## Example @@ -39,4 +38,7 @@ function function { function myFunction { Write-Host "Hello, World!" } -``` \ No newline at end of file +``` + + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_reserved_words diff --git a/docs/Rules/UseCorrectCasing.md b/docs/Rules/UseCorrectCasing.md index 9d7e96a18..b73df6415 100644 --- a/docs/Rules/UseCorrectCasing.md +++ b/docs/Rules/UseCorrectCasing.md @@ -11,7 +11,7 @@ title: UseCorrectCasing ## Description This is a style/formatting rule. PowerShell is case insensitive wherever possible, so the casing of -cmdlet names, parameters, keywords and operators does not matter. This rule nonetheless ensures +cmdlet names, parameters, keywords and operators doesn't matter. This rule nonetheless ensures consistent casing for clarity and readability. Using lowercase keywords helps distinguish them from commands. Using lowercase operators helps distinguish them from parameters. @@ -34,23 +34,23 @@ Rules = @{ } ``` -### Parameters +## Parameters -#### Enable: bool (Default value is `$false`) +### Enable: bool (Default value is `$false`) Enable or disable the rule during ScriptAnalyzer invocation. -#### CheckCommands: bool (Default value is `$true`) +### CheckCommands: bool (Default value is `$true`) If true, require the case of all command and parameter names to match their canonical casing. -#### CheckKeyword: bool (Default value is `$true`) +### CheckKeyword: bool (Default value is `$true`) If true, require the case of all keywords to be lowercase. -#### CheckOperator: bool (Default value is `$true`) +### CheckOperator: bool (Default value is `$true`) -If true, require the case of all operators (e.g. -eq, -ne, -gt) to be lowercase. +If true, require the case of all operators to be lowercase. For example: `-eq`, `-ne`, `-gt` ## Examples From 8b533c307de80bc78d1cf6ee0f36aa86b72007e1 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Wed, 18 Feb 2026 18:48:25 +0000 Subject: [PATCH 108/130] Allow contributors to run CI manually (#2153) --- .github/workflows/ci-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 5469c54a5..0a48b30fb 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -1,6 +1,7 @@ name: CI Tests on: + workflow_dispatch: # to allow contributors to trigger CI manually in their fork push: branches: [ main ] pull_request: From aba29c31151925bd7180e7dfd578e1bfacdaf2a2 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 19 Feb 2026 14:13:13 +0000 Subject: [PATCH 109/130] Remove Conditional Compilation and Runtime Checks for v3 or v4 (#2150) * Remove conditional compilation gates using PSV3 and PSV4 * Replace HashtableAst.SafeGetValue() compatibility shim with actual implementation * Remove/simplify conditional checks which would would always be met with a minimum version of 5.1 (no longer supporting v3 or v4) --- .../Commands/InvokeScriptAnalyzerCommand.cs | 4 - Engine/FindAstPositionVisitor.cs | 6 - Engine/Generic/IDSCResourceRule.cs | 4 - Engine/Generic/ModuleDependencyHandler.cs | 4 +- Engine/Generic/RuleSuppression.cs | 2 - Engine/Helper.cs | 140 +------- Engine/PSScriptAnalyzer.psm1 | 8 +- Engine/ScriptAnalyzer.cs | 53 +-- Engine/Settings.cs | 4 +- Engine/VariableAnalysis.cs | 36 -- Engine/VariableAnalysisBase.cs | 70 +--- Rules/AlignAssignmentStatement.cs | 2 - Rules/AvoidGlobalAliases.cs | 5 +- .../CompatibilityRules/UseCompatibleSyntax.cs | 6 - Rules/DscExamplesPresent.cs | 4 - Rules/DscTestsPresent.cs | 4 - Rules/PossibleIncorrectComparisonWithNull.cs | 8 - Rules/ReturnCorrectTypesForDSCFunctions.cs | 17 - Rules/Rules.csproj | 8 - Rules/UseIdenticalMandatoryParametersDSC.cs | 6 +- Rules/UseOutputTypeCorrectly.cs | 16 - Rules/UseStandardDSCFunctionsInResource.cs | 8 - Rules/UseUsingScopeModifierInNewRunspaces.cs | 4 - .../Documentation/RuleDocumentation.tests.ps1 | 6 - .../CommunityAnalyzerRules.psm1 | 307 +++++++++--------- Tests/Engine/CustomizedRule.tests.ps1 | 4 +- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 14 +- Tests/Engine/Helper.tests.ps1 | 2 +- Tests/Engine/InvokeScriptAnalyzer.tests.ps1 | 6 +- .../Engine/ModuleDependencyHandler.tests.ps1 | 11 +- Tests/Engine/ModuleHelp.Tests.ps1 | 13 +- Tests/Engine/RuleSuppression.tests.ps1 | 2 +- Tests/Engine/RuleSuppressionClass.tests.ps1 | 17 +- .../Rules/AlignAssignmentStatement.tests.ps1 | 26 +- Tests/Rules/AvoidGlobalAliases.tests.ps1 | 8 +- Tests/Rules/DscExamplesPresent.tests.ps1 | 2 +- Tests/Rules/DscTestsPresent.tests.ps1 | 2 +- Tests/Rules/PSCredentialType.tests.ps1 | 3 - Tests/Rules/ProvideCommentHelp.tests.ps1 | 6 +- ...eturnCorrectTypesForDSCFunctions.tests.ps1 | 9 +- Tests/Rules/UseCompatibleSyntax.Tests.ps1 | 22 +- Tests/Rules/UseDSCResourceFunctions.tests.ps1 | 10 +- .../Rules/UseIdenticalParametersDSC.tests.ps1 | 14 +- Tests/Rules/UseOutputTypeCorrectly.tests.ps1 | 11 +- .../Rules/UseShouldProcessCorrectly.tests.ps1 | 6 +- build.psm1 | 16 +- 46 files changed, 218 insertions(+), 718 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index a444327e0..bf1e740c3 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -227,7 +227,6 @@ public object Settings private bool stopProcessing; -#if !PSV3 /// /// Resolve DSC resource dependency /// @@ -238,7 +237,6 @@ public SwitchParameter SaveDscDependency set { saveDscDependency = value; } } private bool saveDscDependency; -#endif // !PSV3 #if DEBUG /// @@ -387,7 +385,6 @@ protected override void ProcessRecord() ProcessPath(); } -#if !PSV3 // TODO Support dependency resolution for analyzing script definitions if (saveDscDependency) { @@ -407,7 +404,6 @@ protected override void ProcessRecord() } return; } -#endif ProcessInput(); } diff --git a/Engine/FindAstPositionVisitor.cs b/Engine/FindAstPositionVisitor.cs index 459581cbc..c281cfef2 100644 --- a/Engine/FindAstPositionVisitor.cs +++ b/Engine/FindAstPositionVisitor.cs @@ -8,11 +8,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer /// /// Provides an efficient way to find the position in the AST corresponding to a given script position. /// -#if !(PSV3 || PSV4) internal class FindAstPositionVisitor : AstVisitor2 -#else - internal class FindAstPositionVisitor : AstVisitor -#endif { private IScriptPosition searchPosition; @@ -300,7 +296,6 @@ public override AstVisitAction VisitWhileStatement(WhileStatementAst whileStatem return Visit(whileStatementAst); } -#if !(PSV3 || PSV4) public override AstVisitAction VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) { return Visit(baseCtorInvokeMemberExpressionAst); @@ -335,7 +330,6 @@ public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatem { return AstVisitAction.Continue; } -#endif #if !(NET462 || PSV7) // net462 includes V3,4,5 public override AstVisitAction VisitPipelineChain(PipelineChainAst pipelineChainAst) diff --git a/Engine/Generic/IDSCResourceRule.cs b/Engine/Generic/IDSCResourceRule.cs index 3ef20cde7..5f4c2da64 100644 --- a/Engine/Generic/IDSCResourceRule.cs +++ b/Engine/Generic/IDSCResourceRule.cs @@ -19,8 +19,6 @@ public interface IDSCResourceRule : IRule /// The results of the analysis IEnumerable AnalyzeDSCResource(Ast ast, string fileName); - #if !PSV3 - /// /// Analyze dsc classes (if any) in the file /// @@ -29,7 +27,5 @@ public interface IDSCResourceRule : IRule /// IEnumerable AnalyzeDSCClass(Ast ast, string fileName); - #endif - } } \ No newline at end of file diff --git a/Engine/Generic/ModuleDependencyHandler.cs b/Engine/Generic/ModuleDependencyHandler.cs index 31a43d6ca..91e557d64 100644 --- a/Engine/Generic/ModuleDependencyHandler.cs +++ b/Engine/Generic/ModuleDependencyHandler.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !PSV3 using System; using System.Collections.Generic; using System.IO; @@ -519,5 +518,4 @@ public void Dispose() #endregion Public Methods } -} -#endif // !PSV3 \ No newline at end of file +} \ No newline at end of file diff --git a/Engine/Generic/RuleSuppression.cs b/Engine/Generic/RuleSuppression.cs index 7daab3e86..279eec58f 100644 --- a/Engine/Generic/RuleSuppression.cs +++ b/Engine/Generic/RuleSuppression.cs @@ -333,12 +333,10 @@ public static List GetSuppressions(IEnumerable at { targetAsts = scopeAst.FindAll(ast => ast is FunctionDefinitionAst && reg.IsMatch((ast as FunctionDefinitionAst).Name), true); } -#if !(PSV3 || PSV4) else if (scope.Equals("class", StringComparison.OrdinalIgnoreCase)) { targetAsts = scopeAst.FindAll(ast => ast is TypeDefinitionAst && reg.IsMatch((ast as TypeDefinitionAst).Name), true); } -#endif if (targetAsts != null) { diff --git a/Engine/Helper.cs b/Engine/Helper.cs index 098d8a276..a162bfbcf 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -512,8 +512,6 @@ public bool IsDscResourceClassBased(ScriptBlockAst ast) return false; } - #if !(PSV3||PSV4) - List dscResourceFunctionNames = new List(new string[] { "Test", "Get", "Set" }); IEnumerable dscClasses = ast.FindAll(item => @@ -528,8 +526,6 @@ item is TypeDefinitionAst return true; } - #endif - return false; } @@ -953,15 +949,7 @@ internal VariableAnalysis InitializeVariableAnalysisHelper(Ast ast, VariableAnal /// /// -#if (PSV3||PSV4) - - public string GetTypeFromReturnStatementAst(Ast funcAst, ReturnStatementAst ret) - -#else - public string GetTypeFromReturnStatementAst(Ast funcAst, ReturnStatementAst ret, IEnumerable classes) - -#endif { if (ret == null || funcAst == null) { @@ -992,15 +980,7 @@ public string GetTypeFromReturnStatementAst(Ast funcAst, ReturnStatementAst ret, } else if (cmAst.Expression is MemberExpressionAst) { -#if PSV3 - - result = GetTypeFromMemberExpressionAst(cmAst.Expression as MemberExpressionAst, funcAst); - -#else - result = GetTypeFromMemberExpressionAst(cmAst.Expression as MemberExpressionAst, funcAst, classes); - -#endif } } } @@ -1024,15 +1004,7 @@ public string GetTypeFromReturnStatementAst(Ast funcAst, ReturnStatementAst ret, /// /// -#if (PSV3||PSV4) - - public string GetTypeFromMemberExpressionAst(MemberExpressionAst memberAst, Ast scopeAst) - -#else - public string GetTypeFromMemberExpressionAst(MemberExpressionAst memberAst, Ast scopeAst, IEnumerable classes) - -#endif { if (memberAst == null) { @@ -1041,38 +1013,22 @@ public string GetTypeFromMemberExpressionAst(MemberExpressionAst memberAst, Ast VariableAnalysisDetails details = null; -#if !(PSV3||PSV4) - TypeDefinitionAst psClass = null; -#endif - if (memberAst.Expression is VariableExpressionAst && VariableAnalysisDictionary.ContainsKey(scopeAst)) { VariableAnalysis VarTypeAnalysis = VariableAnalysisDictionary[scopeAst]; // Get the analysis detail for the variable details = VarTypeAnalysis.GetVariableAnalysis(memberAst.Expression as VariableExpressionAst); -#if !PSV3 - if (details != null && classes != null) { // Get the class that corresponds to the name of the type (if possible, the type is not available in the case of a static Singleton) psClass = classes.FirstOrDefault(item => String.Equals(item.Name, details.Type?.FullName, StringComparison.OrdinalIgnoreCase)); } - -#endif } -#if PSV3 - - return GetTypeFromMemberExpressionAstHelper(memberAst, details); - -#else - - return GetTypeFromMemberExpressionAstHelper(memberAst, psClass, details); - -#endif + return GetTypeFromMemberExpressionAstHelper(memberAst, psClass, details); } /// @@ -1084,28 +1040,17 @@ public string GetTypeFromMemberExpressionAst(MemberExpressionAst memberAst, Ast /// /// -#if (PSV3||PSV4) - - internal string GetTypeFromMemberExpressionAstHelper(MemberExpressionAst memberAst, VariableAnalysisDetails analysisDetails) - -#else - internal string GetTypeFromMemberExpressionAstHelper(MemberExpressionAst memberAst, TypeDefinitionAst psClass, VariableAnalysisDetails analysisDetails) - -#endif { //Try to get the type without using psClass first Type result = AssignmentTarget.GetTypeFromMemberExpressionAst(memberAst); -#if !(PSV3||PSV4) - //If we can't get the type, then it may be that the type of the object being invoked on is a powershell class if (result == null && psClass != null && analysisDetails != null) { result = AssignmentTarget.GetTypeFromMemberExpressionAst(memberAst, analysisDetails, psClass); } -#endif if (result != null) { @@ -1205,7 +1150,6 @@ public Dictionary> GetRuleSuppression(Ast ast) ruleSuppressionList.AddRange(GetSuppressionsFunction(funcAst)); } -#if !(PSV3||PSV4) // Get rule suppression from classes IEnumerable typeAsts = ast.FindAll(item => item is TypeDefinitionAst, true).Cast(); @@ -1221,7 +1165,6 @@ public Dictionary> GetRuleSuppression(Ast ast) { ruleSuppressionList.AddRange(GetSuppressionsConfiguration(configDefAst)); } -#endif // !PSV3 ruleSuppressionList.Sort((item, item2) => item.StartOffset.CompareTo(item2.StartOffset)); @@ -1257,7 +1200,6 @@ internal List GetSuppressionsFunction(FunctionDefinitionAst fun return result; } -#if !(PSV3||PSV4) /// /// Returns a list of rule suppression from the class /// @@ -1313,8 +1255,6 @@ internal List GetSuppressionsConfiguration(ConfigurationDefinit return result; } -#endif // !PSV3 - /// /// Suppress the rules from the diagnostic records list. /// Returns a list of suppressed records as well as the ones that are not suppressed @@ -2116,15 +2056,8 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) // We already run variable analysis if the parent is a function so skip these. // Otherwise, we have to do variable analysis using the outer scope variables. -#if PSV3 - - if (!(scriptBlockAst.Parent is FunctionDefinitionAst)) - -#else if (!(scriptBlockAst.Parent is FunctionDefinitionAst) && !(scriptBlockAst.Parent is FunctionMemberAst)) - -#endif { OuterAnalysis = Helper.Instance.InitializeVariableAnalysisHelper(scriptBlockAst, OuterAnalysis); } @@ -2152,15 +2085,7 @@ public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) VariableAnalysis innerAnalysis = OuterAnalysis; OuterAnalysis = previousOuter; -#if PSV3 - - if (!(scriptBlockAst.Parent is FunctionDefinitionAst)) - -#else - if (!(scriptBlockAst.Parent is FunctionDefinitionAst) && !(scriptBlockAst.Parent is FunctionMemberAst)) - -#endif { // Update the variable analysis of the outer script block VariableAnalysis.UpdateOuterAnalysis(OuterAnalysis, innerAnalysis); @@ -2181,12 +2106,6 @@ private object VisitStatementHelper(StatementAst statementAst) return null; } -#if (PSV3||PSV4) - - statementAst.Visit(this); - -#else - TypeDefinitionAst typeAst = statementAst as TypeDefinitionAst; if (typeAst == null) @@ -2212,14 +2131,9 @@ private object VisitStatementHelper(StatementAst statementAst) OuterAnalysis = previousOuter; } } - -#endif - return null; } -#if !PSV3 - /// /// Do nothing /// @@ -2230,8 +2144,6 @@ public object VisitUsingStatement(UsingStatementAst usingStatement) return null; } -#endif - /// /// Do nothing /// @@ -2897,12 +2809,8 @@ public class FindPipelineOutput : ICustomAstVisitor { List> outputTypes; -#if !(PSV3||PSV4) - IEnumerable classes; -#endif - FunctionDefinitionAst myFunction; /// /// These binary operators will always return boolean value @@ -2939,24 +2847,12 @@ static FindPipelineOutput() /// /// -#if (PSV3||PSV4) - - public FindPipelineOutput(FunctionDefinitionAst ast) - -#else - public FindPipelineOutput(FunctionDefinitionAst ast, IEnumerable classes) - -#endif { outputTypes = new List>(); -#if !PSV3 - this.classes = classes; -#endif - myFunction = ast; if (myFunction != null) @@ -2970,21 +2866,11 @@ public FindPipelineOutput(FunctionDefinitionAst ast, IEnumerable /// -#if (PSV3||PSV4) - - public static List> OutputTypes(FunctionDefinitionAst funcAst) - { - return (new FindPipelineOutput(funcAst)).outputTypes; - } - -#else public static List> OutputTypes(FunctionDefinitionAst funcAst, IEnumerable classes) { return (new FindPipelineOutput(funcAst, classes)).outputTypes; } -#endif - /// /// Ignore assignment statement /// @@ -3436,15 +3322,7 @@ public object VisitCommandExpression(CommandExpressionAst commandAst) /// public object VisitReturnStatement(ReturnStatementAst returnStatementAst) { -#if PSV3 - - return Helper.Instance.GetTypeFromReturnStatementAst(myFunction, returnStatementAst); - -#else - return Helper.Instance.GetTypeFromReturnStatementAst(myFunction, returnStatementAst, classes); - -#endif } /// @@ -3454,15 +3332,7 @@ public object VisitReturnStatement(ReturnStatementAst returnStatementAst) /// public object VisitMemberExpression(MemberExpressionAst memAst) { -#if PSV3 - - return Helper.Instance.GetTypeFromMemberExpressionAst(memAst, myFunction); - -#else - return Helper.Instance.GetTypeFromMemberExpressionAst(memAst, myFunction, classes); - -#endif } /// @@ -3472,15 +3342,7 @@ public object VisitMemberExpression(MemberExpressionAst memAst) /// public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeAst) { -#if PSV3 - - return Helper.Instance.GetTypeFromMemberExpressionAst(invokeAst, myFunction); - -#else - return Helper.Instance.GetTypeFromMemberExpressionAst(invokeAst, myFunction, classes); - -#endif } /// diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index 6d2563a49..acd9daf76 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -19,16 +19,10 @@ if ($PSVersionTable.PSVersion.Major -ge 6) { if ($PSVersionTable.PSVersion -lt $minimumPowerShellCoreVersion) { throw "Minimum supported version of PSScriptAnalyzer for PowerShell Core is $minimumPowerShellCoreVersion but current version is '$($PSVersionTable.PSVersion)'. Please update PowerShell Core." } -} -elseif ($PSVersionTable.PSVersion.Major -eq 5) { +} else { # Without this, PSSA tries to load this from $PSHome Add-Type -Path "$PSScriptRoot/Newtonsoft.Json.dll" } -elseif ($PSVersionTable.PSVersion.Major -le 4) { - $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" - # Without this, PSSA tries to load this from $PSHome - Add-Type -Path "$binaryModuleRoot/Newtonsoft.Json.dll" -} $binaryModulePath = Join-Path -Path $binaryModuleRoot -ChildPath 'Microsoft.Windows.PowerShell.ScriptAnalyzer.dll' $binaryModule = Import-Module -Name $binaryModulePath -PassThru diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index f250336b5..c564dc8fa 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -50,9 +50,7 @@ public sealed class ScriptAnalyzer List includeRegexList; List excludeRegexList; private SuppressionPreference _suppressionPreference; -#if !PSV3 ModuleDependencyHandler moduleHandler; -#endif #endregion #region Singleton @@ -98,7 +96,6 @@ public static ScriptAnalyzer Instance internal List ExternalRules { get; set; } -#if !PSV3 public ModuleDependencyHandler ModuleHandler { get { @@ -110,7 +107,6 @@ internal set moduleHandler = value; } } -#endif #endregion @@ -622,36 +618,6 @@ private bool ParseProfileString(string profile, PathIntrinsics path, IOutputWrit else { HashtableAst hashTableAst = hashTableAsts.First() as HashtableAst; -#if PSV3 - settings = GetDictionaryFromHashTableAst( - hashTableAst, - writer, - profile, - out hasError); - foreach (var key in settings.Keys) - { - var rhsList = settings[key] as List; - if (rhsList == null) - { - continue; - } - if (!AddProfileItem(key, rhsList, severityList, includeRuleList, excludeRuleList)) - { - writer.WriteError( - new ErrorRecord( - new InvalidDataException( - string.Format( - CultureInfo.CurrentCulture, - Strings.WrongKey, - key, - profile)), - Strings.WrongConfigurationKey, - ErrorCategory.InvalidData, - profile)); - hasError = true; - } - } -#else try { @@ -668,7 +634,6 @@ private bool ParseProfileString(string profile, PathIntrinsics path, IOutputWrit profile)); hasError = true; } -#endif // PSV3 } } @@ -691,10 +656,7 @@ private void Initialize( { throw new ArgumentNullException("outputWriter"); } -#if !PSV3 this.moduleHandler = null; -#endif - this.outputWriter = outputWriter; #region Verifies rule extensions @@ -1843,7 +1805,6 @@ private void BuildScriptPathList( } } -#if !PSV3 private bool TrySaveModules(ParseError[] errors, ScriptBlockAst scriptAst) { bool modulesSaved = false; @@ -1877,7 +1838,6 @@ private bool TrySaveModules(ParseError[] errors, ScriptBlockAst scriptAst) } return modulesSaved; } -#endif // !PSV3 private IEnumerable AnalyzeFile(string filePath) { @@ -1915,13 +1875,13 @@ private IEnumerable AnalyzeFile(string filePath) this.outputWriter.WriteWarning(e.ToString()); return null; } -#if !PSV3 + //try parsing again if (TrySaveModules(errors, scriptAst)) { scriptAst = Parser.ParseFile(filePath, out scriptTokens, out errors); } -#endif //!PSV3 + IEnumerable relevantParseErrors = RemoveTypeNotFoundParseErrors(errors, out diagnosticRecords); // First, add all parse errors if they've been requested @@ -2254,17 +2214,8 @@ public IEnumerable AnalyzeSyntaxTree( // We want the Engine to continue functioning even if one or more Rules throws an exception try { -#if PSV3 - var errRecs = new List(); - var records = Helper.Instance.SuppressRule( - dscResourceRule.GetName(), - ruleSuppressions, - null, - out errRecs); -#else var ruleRecords = dscResourceRule.AnalyzeDSCClass(scriptAst, filePath).ToList(); var records = SuppressRule(dscResourceRule.GetName(), ruleSuppressions, ruleRecords); -#endif foreach (var record in records.Item2) { diagnostics.Add(record); diff --git a/Engine/Settings.cs b/Engine/Settings.cs index b0c424c64..124e5488a 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -462,9 +462,7 @@ private void parseSettingsFile(string settingsFilePath) Hashtable hashtable; try { - // ideally we should use HashtableAst.SafeGetValue() but since - // it is not available on PSv3, we resort to our own narrow implementation. - hashtable = Helper.GetSafeValueFromHashtableAst(hashTableAst); + hashtable = (Hashtable) hashTableAst.SafeGetValue(); } catch (InvalidOperationException e) { diff --git a/Engine/VariableAnalysis.cs b/Engine/VariableAnalysis.cs index 2870d442f..2bb8068d4 100644 --- a/Engine/VariableAnalysis.cs +++ b/Engine/VariableAnalysis.cs @@ -134,15 +134,7 @@ private void ProcessParameters(IEnumerable parameters) public void AnalyzeImpl(Ast ast, VariableAnalysis outerAnalysis) { - #if PSV3 - - if (!(ast is ScriptBlockAst || ast is FunctionDefinitionAst)) - - #else - if (!(ast is ScriptBlockAst || ast is FunctionMemberAst || ast is FunctionDefinitionAst)) - - #endif { return; } @@ -151,15 +143,7 @@ public void AnalyzeImpl(Ast ast, VariableAnalysis outerAnalysis) Init(); - #if PSV3 - - if (ast is FunctionDefinitionAst) - - #else - if (ast is FunctionMemberAst || ast is FunctionDefinitionAst) - - #endif { IEnumerable parameters = FindParameters(ast, ast.GetType()); if (parameters != null) @@ -176,20 +160,11 @@ public void AnalyzeImpl(Ast ast, VariableAnalysis outerAnalysis) } } - #if PSV3 - - if (ast is FunctionDefinitionAst) - - #else - if (ast is FunctionMemberAst) { (ast as FunctionMemberAst).Body.Visit(this.Decorator); } else if (ast is FunctionDefinitionAst) - - #endif - { (ast as FunctionDefinitionAst).Body.Visit(this.Decorator); } @@ -205,14 +180,10 @@ public void AnalyzeImpl(Ast ast, VariableAnalysis outerAnalysis) parent = parent.Parent; } - #if !(PSV3||PSV4) - List classes = parent.FindAll(item => item is TypeDefinitionAst && (item as TypeDefinitionAst).IsClass, true) .Cast().ToList(); - #endif - if (outerAnalysis != null) { // Initialize the variables from outside @@ -250,15 +221,8 @@ public void AnalyzeImpl(Ast ast, VariableAnalysis outerAnalysis) } } - #if PSV3 - - var dictionaries = Block.SparseSimpleConstants(_variables, Entry); - - #else - var dictionaries = Block.SparseSimpleConstants(_variables, Entry, classes); - #endif VariablesDictionary = dictionaries.Item1; InternalVariablesDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/Engine/VariableAnalysisBase.cs b/Engine/VariableAnalysisBase.cs index 77c421f3c..218bdc8ea 100644 --- a/Engine/VariableAnalysisBase.cs +++ b/Engine/VariableAnalysisBase.cs @@ -97,16 +97,7 @@ public class FindAllVariablesVisitor : AstVisitor /// public static Dictionary Visit(Ast ast) { - #if PSV3 - - if (!(ast is ScriptBlockAst || ast is FunctionDefinitionAst)) - - #else - if (!(ast is ScriptBlockAst || ast is FunctionMemberAst || ast is FunctionDefinitionAst)) - - #endif - { return null; } @@ -122,34 +113,20 @@ public static Dictionary Visit(Ast ast) { (ast as ScriptBlockAst).Visit(visitor); } - - #if !PSV3 - else if (ast is FunctionMemberAst) { (ast as FunctionMemberAst).Body.Visit(visitor); } - - #endif - else if (ast is FunctionDefinitionAst) { (ast as FunctionDefinitionAst).Body.Visit(visitor); } - #if PSV3 - - if (ast is FunctionDefinitionAst && (ast as FunctionDefinitionAst).Parameters != null) - - #else - if (ast is FunctionMemberAst && (ast as FunctionMemberAst).Parameters != null) { visitor.VisitParameters((ast as FunctionMemberAst).Parameters); } else if (ast is FunctionDefinitionAst && (ast as FunctionDefinitionAst).Parameters != null) - - #endif { visitor.VisitParameters((ast as FunctionDefinitionAst).Parameters); } @@ -165,8 +142,6 @@ internal void InitializeVariables(Ast ast) _variables.Add("true", new VariableAnalysisDetails { Name = "true", RealName = "true", Type = typeof(bool) }); _variables.Add("false", new VariableAnalysisDetails { Name = "false", RealName = "true", Type = typeof(bool) }); - #if !(PSV3||PSV4) - if (ast is FunctionMemberAst) { TypeDefinitionAst psClass = AssignmentTarget.FindClassAncestor(ast); @@ -175,9 +150,6 @@ internal void InitializeVariables(Ast ast) _variables.Add("this", new VariableAnalysisDetails { Name = "this", RealName = "this", Constant = SpecialVars.ThisVariable }); } } - - #endif - } internal void VisitParameters(ReadOnlyCollection parameters) @@ -808,16 +780,8 @@ internal static void InitializeSSA(Dictionary V /// /// /// - #if (PSV3||PSV4) - internal static Tuple, Dictionary> SparseSimpleConstants( - Dictionary Variables, Block Entry) - - #else - internal static Tuple, Dictionary> SparseSimpleConstants( - Dictionary Variables, Block Entry, List Classes) - - #endif + Dictionary Variables, Block Entry, List Classes) { List blocks = GenerateReverseDepthFirstOrder(Entry); @@ -989,17 +953,9 @@ internal static Tuple, Dictionary String.Equals(item.Name, analysis.Type?.FullName, StringComparison.OrdinalIgnoreCase)); Type possibleType = AssignmentTarget.GetTypeFromMemberExpressionAst(memAst, analysis, psClass); - #endif - if (possibleType != null && possibleType != assigned.Type) { assigned.Type = possibleType; @@ -1370,24 +1326,13 @@ public AssignmentTarget(string variableName, Type type) /// /// /// - - #if (PSV3||PSV4) - - internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memAst, VariableAnalysisDetails analysis) - - #else - internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memAst, VariableAnalysisDetails analysis, TypeDefinitionAst psClass) - - #endif { if (memAst != null && memAst.Expression is VariableExpressionAst && memAst.Member is StringConstantExpressionAst && !String.Equals((memAst.Expression as VariableExpressionAst).VariablePath.UserPath, "this", StringComparison.OrdinalIgnoreCase)) { string fieldName = (memAst.Member as StringConstantExpressionAst).Value; - #if !PSV3 - if (psClass == null && analysis.Constant == SpecialVars.ThisVariable) { psClass = AssignmentTarget.FindClassAncestor(memAst); @@ -1404,8 +1349,6 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memAst, } } - #endif - // If the type is not a ps class or there are some types of the same name. if (analysis != null && analysis.Type != null && analysis.Type != typeof(object) && analysis.Type != typeof(Unreached) && analysis.Type != typeof(Undetermined)) @@ -1460,7 +1403,6 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memberAs // isStatic is true result = GetTypeFromInvokeMemberAst(type, imeAst, methodName, true); } - #if !(PSV3||PSV4) else { // Check for classes @@ -1478,7 +1420,6 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memberAs } } } - #endif } #endregion @@ -1498,7 +1439,6 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memberAs { result = GetPropertyOrFieldTypeFromMemberExpressionAst(expressionType, fieldName); } - #if !(PSV3||PSV4) else { // check for class type @@ -1514,7 +1454,6 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memberAs } } } - #endif } #endregion @@ -1531,15 +1470,11 @@ internal static Type GetTypeFromMemberExpressionAst(MemberExpressionAst memberAs if (memberAst.Expression is VariableExpressionAst && String.Equals((memberAst.Expression as VariableExpressionAst).VariablePath.UserPath, "this", StringComparison.OrdinalIgnoreCase)) { - #if !(PSV3||PSV4) - // Check that we are in a class TypeDefinitionAst psClass = FindClassAncestor(memberAst); // Is static is false for this case result = GetTypeFromClass(psClass, memberAst); - - #endif } return result; @@ -1598,7 +1533,6 @@ internal static Type GetPropertyOrFieldTypeFromMemberExpressionAst(Type type, st return result; } -#if !(PSV3||PSV4) /// /// Checks whether a class with the name name exists in the script that contains ast /// @@ -1686,8 +1620,6 @@ internal static Type GetTypeFromClass(TypeDefinitionAst psClass, MemberExpressio return result; } -#endif // !PSV3 - private void SetVariableName() { ExpressionAst lhs = (_targetAst is ConvertExpressionAst) ? (_targetAst as ConvertExpressionAst).Child : _targetAst; diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 1d79870f2..14cce7d37 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -147,7 +147,6 @@ private IEnumerable FindHashtableViolations(TokenOperations to } } -#if !PSV3 var configAsts = tokenOps.Ast.FindAll(ast => ast is ConfigurationDefinitionAst, true); if (configAsts != null) { @@ -163,7 +162,6 @@ private IEnumerable FindHashtableViolations(TokenOperations to groups.AddRange(GetCommandElementExtentGroups(configAst)); } } -#endif // it is probably much easier have a hashtable writer that formats the hashtable and writes it // but it makes handling comments hard. So we need to use this approach. diff --git a/Rules/AvoidGlobalAliases.cs b/Rules/AvoidGlobalAliases.cs index afe7978e6..8697ad1ca 100644 --- a/Rules/AvoidGlobalAliases.cs +++ b/Rules/AvoidGlobalAliases.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !PSV3 using System; using System.Collections.Generic; #if !CORECLR @@ -136,6 +135,4 @@ public SourceType GetSourceType() return SourceType.Builtin; } } -} - -#endif // !PSV3 \ No newline at end of file +} \ No newline at end of file diff --git a/Rules/CompatibilityRules/UseCompatibleSyntax.cs b/Rules/CompatibilityRules/UseCompatibleSyntax.cs index 3af6cdd1c..c7d10c19d 100644 --- a/Rules/CompatibilityRules/UseCompatibleSyntax.cs +++ b/Rules/CompatibilityRules/UseCompatibleSyntax.cs @@ -149,11 +149,7 @@ private static HashSet GetTargetedVersions(string[] versionSettings) return targetVersions; } -#if !(PSV3 || PSV4) private class SyntaxCompatibilityVisitor : AstVisitor2 -#else - private class SyntaxCompatibilityVisitor : AstVisitor -#endif { private readonly UseCompatibleSyntax _rule; @@ -260,7 +256,6 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun return AstVisitAction.Continue; } -#if !(PSV3 || PSV4) public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatementAst) { // Look for 'using ...;' at the top of scripts @@ -306,7 +301,6 @@ public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinit return AstVisitAction.Continue; } -#endif #if PSV7 public override AstVisitAction VisitMemberExpression(MemberExpressionAst memberExpressionAst) diff --git a/Rules/DscExamplesPresent.cs b/Rules/DscExamplesPresent.cs index 6d0a01a3b..17cca2a1c 100644 --- a/Rules/DscExamplesPresent.cs +++ b/Rules/DscExamplesPresent.cs @@ -65,8 +65,6 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName } } - #if !(PSV3||PSV4) - /// /// AnalyzeDSCClass: Analyzes given DSC class /// @@ -116,8 +114,6 @@ item is TypeDefinitionAst } } - #endif - /// /// GetName: Retrieves the name of this rule. /// diff --git a/Rules/DscTestsPresent.cs b/Rules/DscTestsPresent.cs index 5c09ede8a..3a19902d9 100644 --- a/Rules/DscTestsPresent.cs +++ b/Rules/DscTestsPresent.cs @@ -65,8 +65,6 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName } } - #if !(PSV3||PSV4) - /// /// AnalyzeDSCClass: Analyzes given DSC class /// @@ -116,8 +114,6 @@ item is TypeDefinitionAst } } - #endif - /// /// GetName: Retrieves the name of this rule. /// diff --git a/Rules/PossibleIncorrectComparisonWithNull.cs b/Rules/PossibleIncorrectComparisonWithNull.cs index 74db4f523..3302583ae 100644 --- a/Rules/PossibleIncorrectComparisonWithNull.cs +++ b/Rules/PossibleIncorrectComparisonWithNull.cs @@ -44,16 +44,8 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { } } - #if PSV3 - - IEnumerable funcAsts = ast.FindAll(item => item is FunctionDefinitionAst, true); - - #else - IEnumerable funcAsts = ast.FindAll(item => item is FunctionDefinitionAst, true).Union(ast.FindAll(item => item is FunctionMemberAst, true)); - #endif - foreach (Ast funcAst in funcAsts) { IEnumerable binAsts = funcAst.FindAll(item => item is BinaryExpressionAst, true); diff --git a/Rules/ReturnCorrectTypesForDSCFunctions.cs b/Rules/ReturnCorrectTypesForDSCFunctions.cs index 08eb59df0..0c750571c 100644 --- a/Rules/ReturnCorrectTypesForDSCFunctions.cs +++ b/Rules/ReturnCorrectTypesForDSCFunctions.cs @@ -35,28 +35,16 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName IEnumerable functionDefinitionAsts = Helper.Instance.DscResourceFunctions(ast); - #if !(PSV3||PSV4) - IEnumerable classes = ast.FindAll(item => item is TypeDefinitionAst && ((item as TypeDefinitionAst).IsClass), true).Cast(); - #endif foreach (FunctionDefinitionAst func in functionDefinitionAsts) { - #if PSV3 || PSV4 - - List> outputTypes = FindPipelineOutput.OutputTypes(func); - - #else - List> outputTypes = FindPipelineOutput.OutputTypes(func, classes); - #endif - - if (String.Equals(func.Name, "Set-TargetResource", StringComparison.OrdinalIgnoreCase)) { foreach (Tuple outputType in outputTypes) @@ -93,8 +81,6 @@ item is TypeDefinitionAst } } - #if !(PSV3||PSV4) - /// /// AnalyzeDSCClass: Analyzes given DSC Resource /// @@ -184,9 +170,6 @@ item is TypeDefinitionAst } } - #endif - - /// /// GetName: Retrieves the name of this rule. /// diff --git a/Rules/Rules.csproj b/Rules/Rules.csproj index 6e485c4e9..7641bb033 100644 --- a/Rules/Rules.csproj +++ b/Rules/Rules.csproj @@ -53,14 +53,6 @@ PrepareResources;$(CompileDependsOn) - - $(DefineConstants);PSV3 - - - - $(DefineConstants);PSV3;PSV4 - - $(DefineConstants);PSV7;CORECLR diff --git a/Rules/UseIdenticalMandatoryParametersDSC.cs b/Rules/UseIdenticalMandatoryParametersDSC.cs index 56acb5b48..9c453e3a1 100644 --- a/Rules/UseIdenticalMandatoryParametersDSC.cs +++ b/Rules/UseIdenticalMandatoryParametersDSC.cs @@ -2,8 +2,6 @@ // Licensed under the MIT License. // this rule can only compile on v4+ -#if (PSV4 || !PSV3) - using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -334,6 +332,4 @@ private FileInfo GetModuleManifest(string fileName) .FirstOrDefault(); } } -} - -#endif +} \ No newline at end of file diff --git a/Rules/UseOutputTypeCorrectly.cs b/Rules/UseOutputTypeCorrectly.cs index 099cd9f25..ac7ff4a01 100644 --- a/Rules/UseOutputTypeCorrectly.cs +++ b/Rules/UseOutputTypeCorrectly.cs @@ -22,12 +22,8 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif public class UseOutputTypeCorrectly : SkipTypeDefinition, IScriptRule { - #if !(PSV3||PSV4) - private IEnumerable _classes; - #endif - /// /// AnalyzeScript: Checks that objects returned in a cmdlet have their types declared in OutputType Attribute /// @@ -41,12 +37,8 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) DiagnosticRecords.Clear(); this.fileName = fileName; - #if !(PSV3||PSV4) - _classes = ast.FindAll(item => item is TypeDefinitionAst && ((item as TypeDefinitionAst).IsClass), true).Cast(); - #endif - ast.Visit(this); return DiagnosticRecords; @@ -103,16 +95,8 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun } } - #if PSV3 - - List> returnTypes = FindPipelineOutput.OutputTypes(funcAst); - - #else - List> returnTypes = FindPipelineOutput.OutputTypes(funcAst, _classes); - #endif - HashSet specialTypes = new HashSet(StringComparer.OrdinalIgnoreCase); specialTypes.Add(typeof(Unreached).FullName); specialTypes.Add(typeof(Undetermined).FullName); diff --git a/Rules/UseStandardDSCFunctionsInResource.cs b/Rules/UseStandardDSCFunctionsInResource.cs index 7022da2d4..386198ba5 100644 --- a/Rules/UseStandardDSCFunctionsInResource.cs +++ b/Rules/UseStandardDSCFunctionsInResource.cs @@ -64,12 +64,6 @@ public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - #if (PSV3||PSV4) - - return null; - - #else - List resourceFunctionNames = new List(new string[] {"Test", "Get", "Set"}); IEnumerable dscClasses = ast.FindAll(item => @@ -90,8 +84,6 @@ item is TypeDefinitionAst } } } - - #endif } /// diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a435c1d31..46e1eea8a 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -91,11 +91,7 @@ public string GetSourceName() return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); } -#if !(PSV3 || PSV4) private class SyntaxCompatibilityVisitor : AstVisitor2 -#else - private class SyntaxCompatibilityVisitor : AstVisitor -#endif { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; diff --git a/Tests/Documentation/RuleDocumentation.tests.ps1 b/Tests/Documentation/RuleDocumentation.tests.ps1 index a62c8506e..d8779af4f 100644 --- a/Tests/Documentation/RuleDocumentation.tests.ps1 +++ b/Tests/Documentation/RuleDocumentation.tests.ps1 @@ -15,12 +15,6 @@ Describe "Validate rule documentation files" { }} | Sort-Object - # Remove rules from the diff list that aren't supported on old PS version - if ($PSVersionTable.PSVersion.Major -eq 4) { - $docs = $docs | Where-Object {$_ -notmatch '^PSAvoidGlobalAliases$'} - $readmeRules = $readmeRules | Where-Object { $_ -notmatch '^PSAvoidGlobalAliases$' } - } - $rulesDocsDiff = Compare-Object -ReferenceObject $rules -DifferenceObject $docs -SyncWindow 25 $rulesReadmeDiff = Compare-Object -ReferenceObject $rules -DifferenceObject $readmeRules -SyncWindow 25 } diff --git a/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1 b/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1 index f9abf9950..c28d80ec8 100644 --- a/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1 +++ b/Tests/Engine/CommunityAnalyzerRules/CommunityAnalyzerRules.psm1 @@ -232,165 +232,158 @@ function Measure-RequiresModules } } +<# +.SYNOPSIS + You can store the type name in a variable or using -f operator to reduce the amount of redundant information in your script. +.DESCRIPTION + When interacting with classes that have long type names, you want to reduce the amount of redundant information in your script. + To fix a violation of this rule, please store the type name in a variable or using -f operator. For example: + $namespace = "System.Collections.{0}"; $arrayList = New-Object ($namespace -f "ArrayList"); $queue = New-Object ($namespace -f "Queue") +.EXAMPLE + Measure-LongClassName -CommandAst $CommandAst +.INPUTS + [System.Management.Automation.Language.CommandAst] +.OUTPUTS + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] +.NOTES + Reference: 3.11. Reduce Typying for Long Class Names, Windows PowerShell Cookbook, Third Edition +#> +function Measure-LongClassName +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.CommandAst] + $CommandAst + ) + + Process + { + $results = @() + + # The StaticParameterBinder help us to find the argument of TypeName. + $spBinder = [System.Management.Automation.Language.StaticParameterBinder] + + # Checks New-Object without ComObject parameter command only. + if ($null -ne $CommandAst.GetCommandName()) + { + if ($CommandAst.GetCommandName() -ne "new-object") + { + return $results + } + } + else + { + return $results + } + + try + { + [System.Management.Automation.Language.StaticBindingResult]$sbResults = $spBinder::BindCommand($CommandAst, $true) + foreach ($sbResult in $sbResults) + { + # TypeName cannot be found if user run command like, New-Object -ComObject Scripting.FileSystemObject. + if ($null -eq $sbResult.BoundParameters["TypeName"].ConstantValue) { continue } + + if ($sbResult.BoundParameters["TypeName"].ConstantValue.ToString().Split('.').Length -ge 3) + { + # $sbResult.BoundParameters["TypeName"].Value is a CommandElementAst, so we can return an extent. + $result = New-Object ` + -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` + -ArgumentList $Messages.MeasureLongClassName,$sbResult.BoundParameters["TypeName"].Value.Extent,$PSCmdlet.MyInvocation.InvocationName,Information,$null + + $results += $result + } + } + + return $results + } + catch + { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + + + } +} -# The two rules in the following if block use StaticParameterBinder class. -# StaticParameterBinder class was introduced in PSv4. -if ($PSVersionTable.PSVersion -ge [Version]'4.0.0') +<# +.SYNOPSIS + Please do not use COM objects when calling New-Object. +.DESCRIPTION + If you can't use just PowerShell, use .NET, external commands or COM objects, in that order of preference. COM objects are rarely well-documented, making them harder for someone else to research and understand. + They do not always work flawlessly in PowerShell, as they must be used through .NET's Interop layer, which isn't 100% perfect. + To fix a violation of this rule, please do not use COM objects when calling New-Object. +.EXAMPLE + Measure-ComObject -CommandAst $CommandAst +.INPUTS + [System.Management.Automation.Language.CommandAst] +.OUTPUTS + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] +.NOTES + Reference: The Purity Laws, The Community Book of PowerShell Practices. +#> +function Measure-ComObject { - <# - .SYNOPSIS - You can store the type name in a variable or using -f operator to reduce the amount of redundant information in your script. - .DESCRIPTION - When interacting with classes that have long type names, you want to reduce the amount of redundant information in your script. - To fix a violation of this rule, please store the type name in a variable or using -f operator. For example: - $namespace = "System.Collections.{0}"; $arrayList = New-Object ($namespace -f "ArrayList"); $queue = New-Object ($namespace -f "Queue") - .EXAMPLE - Measure-LongClassName -CommandAst $CommandAst - .INPUTS - [System.Management.Automation.Language.CommandAst] - .OUTPUTS - [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] - .NOTES - Reference: 3.11. Reduce Typying for Long Class Names, Windows PowerShell Cookbook, Third Edition - #> - function Measure-LongClassName - { - [CmdletBinding()] - [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] - Param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.Management.Automation.Language.CommandAst] - $CommandAst - ) - - Process - { - $results = @() - - # The StaticParameterBinder help us to find the argument of TypeName. - $spBinder = [System.Management.Automation.Language.StaticParameterBinder] - - # Checks New-Object without ComObject parameter command only. - if ($null -ne $CommandAst.GetCommandName()) - { - if ($CommandAst.GetCommandName() -ne "new-object") - { - return $results - } - } - else - { - return $results - } - - try - { - [System.Management.Automation.Language.StaticBindingResult]$sbResults = $spBinder::BindCommand($CommandAst, $true) - foreach ($sbResult in $sbResults) - { - # TypeName cannot be found if user run command like, New-Object -ComObject Scripting.FileSystemObject. - if ($null -eq $sbResult.BoundParameters["TypeName"].ConstantValue) { continue } - - if ($sbResult.BoundParameters["TypeName"].ConstantValue.ToString().Split('.').Length -ge 3) - { - # $sbResult.BoundParameters["TypeName"].Value is a CommandElementAst, so we can return an extent. - $result = New-Object ` - -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` - -ArgumentList $Messages.MeasureLongClassName,$sbResult.BoundParameters["TypeName"].Value.Extent,$PSCmdlet.MyInvocation.InvocationName,Information,$null - - $results += $result - } - } - - return $results - } - catch - { - $PSCmdlet.ThrowTerminatingError($PSItem) - } - - - } - } - - <# - .SYNOPSIS - Please do not use COM objects when calling New-Object. - .DESCRIPTION - If you can't use just PowerShell, use .NET, external commands or COM objects, in that order of preference. COM objects are rarely well-documented, making them harder for someone else to research and understand. - They do not always work flawlessly in PowerShell, as they must be used through .NET's Interop layer, which isn't 100% perfect. - To fix a violation of this rule, please do not use COM objects when calling New-Object. - .EXAMPLE - Measure-ComObject -CommandAst $CommandAst - .INPUTS - [System.Management.Automation.Language.CommandAst] - .OUTPUTS - [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] - .NOTES - Reference: The Purity Laws, The Community Book of PowerShell Practices. - #> - function Measure-ComObject - { - [CmdletBinding()] - [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] - Param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.Management.Automation.Language.CommandAst] - $CommandAst - ) - - Process - { - $results = @() - - # The StaticParameterBinder help us to find the argument of TypeName. - $spBinder = [System.Management.Automation.Language.StaticParameterBinder] - - # Checks New-Object without ComObject parameter command only. - if ($null -ne $CommandAst.GetCommandName()) - { - if ($CommandAst.GetCommandName() -ne "new-object") - { - return $results - } - } - else - { - return $results - } - - try - { - [System.Management.Automation.Language.StaticBindingResult]$sbResults = $spBinder::BindCommand($CommandAst, $true) - foreach ($sbResult in $sbResults) - { - if ($sbResults.BoundParameters.ContainsKey("ComObject")) - { - # $sbResult.BoundParameters["TypeName"].Value is a CommandElementAst, so we can return an extent. - $result = New-Object ` - -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` - -ArgumentList $Messages.MeasureComObject,$sbResult.BoundParameters["ComObject"].Value.Extent,$PSCmdlet.MyInvocation.InvocationName,Warning,$null - - $results += $result - } - } - - return $results - } - catch - { - $PSCmdlet.ThrowTerminatingError($PSItem) - } - - - } - } - -} # end if ($PSVersionTable.PSVersion -ge [Version]'4.0') + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.CommandAst] + $CommandAst + ) + + Process + { + $results = @() + + # The StaticParameterBinder help us to find the argument of TypeName. + $spBinder = [System.Management.Automation.Language.StaticParameterBinder] + + # Checks New-Object without ComObject parameter command only. + if ($null -ne $CommandAst.GetCommandName()) + { + if ($CommandAst.GetCommandName() -ne "new-object") + { + return $results + } + } + else + { + return $results + } + + try + { + [System.Management.Automation.Language.StaticBindingResult]$sbResults = $spBinder::BindCommand($CommandAst, $true) + foreach ($sbResult in $sbResults) + { + if ($sbResults.BoundParameters.ContainsKey("ComObject")) + { + # $sbResult.BoundParameters["TypeName"].Value is a CommandElementAst, so we can return an extent. + $result = New-Object ` + -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" ` + -ArgumentList $Messages.MeasureComObject,$sbResult.BoundParameters["ComObject"].Value.Extent,$PSCmdlet.MyInvocation.InvocationName,Warning,$null + + $results += $result + } + } + + return $results + } + catch + { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + + + } +} <# diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 index ba0686d75..ce60f6dce 100644 --- a/Tests/Engine/CustomizedRule.tests.ps1 +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -257,7 +257,7 @@ Describe "Test importing correct customized rules" { $customizedRulePath.Count | Should -Be 1 } - It "loads custom rules that contain version in their path" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "loads custom rules that contain version in their path" { $customizedRulePath = Invoke-ScriptAnalyzer $PSScriptRoot\TestScript.ps1 -CustomRulePath $PSScriptRoot\VersionedSampleRule\SampleRuleWithVersion $customizedRulePath.Count | Should -Be 1 @@ -265,7 +265,7 @@ Describe "Test importing correct customized rules" { $customizedRulePath.Count | Should -Be 1 } - It "loads custom rules that contain version in their path with the RecurseCustomRule switch" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "loads custom rules that contain version in their path with the RecurseCustomRule switch" { $customizedRulePath = Invoke-ScriptAnalyzer $PSScriptRoot\TestScript.ps1 -CustomRulePath $PSScriptRoot\VersionedSampleRule -RecurseCustomRulePath $customizedRulePath.Count | Should -Be 1 diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index c3b744803..8dca8dcdc 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -64,14 +64,6 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule $expectedNumRules = 71 - if ($PSVersionTable.PSVersion.Major -le 4) - { - # for PSv3 PSAvoidGlobalAliases is not shipped because - # it uses StaticParameterBinder.BindCommand which is - # available only on PSv4 and above - - $expectedNumRules-- - } $defaultRules.Count | Should -Be $expectedNumRules } @@ -100,11 +92,7 @@ Describe "Test RuleExtension" { BeforeAll { $community = "CommunityAnalyzerRules" $measureRequired = "Measure-RequiresModules" - $expectedNumCommunityRules = 10 - if ($PSVersionTable.PSVersion -ge [Version]'4.0.0') - { - $expectedNumCommunityRules = 12 - } + $expectedNumCommunityRules = 12 } It "with the module folder path" { $ruleExtension = Get-ScriptAnalyzerRule -CustomizedRulePath $PSScriptRoot\CommunityAnalyzerRules | Where-Object {$_.SourceName -eq $community} diff --git a/Tests/Engine/Helper.tests.ps1 b/Tests/Engine/Helper.tests.ps1 index b0932be32..3d53e71f1 100644 --- a/Tests/Engine/Helper.tests.ps1 +++ b/Tests/Engine/Helper.tests.ps1 @@ -33,7 +33,7 @@ Describe "Test Directed Graph" { } Context "Runspaces should be disposed" { - It "Running analyzer 100 times should only create a limited number of runspaces" -Skip:$($PSVersionTable.PSVersion.Major -le 4) { + It "Running analyzer 100 times should only create a limited number of runspaces" { $null = 1..100 | ForEach-Object { Invoke-ScriptAnalyzer -ScriptDefinition 'gci' } (Get-Runspace).Count | Should -BeLessOrEqual 14 -Because 'Number of Runspaces should be bound (size of runspace pool cache is 10)' } diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index b930c9980..980836218 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -71,7 +71,7 @@ Describe "Test available parameters" { } } - Context "SaveDscDependency parameter" -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { + Context "SaveDscDependency parameter" -Skip:($testingLibraryUsage) { It "has the parameter" { $params.ContainsKey("SaveDscDependency") | Should -BeTrue } @@ -616,7 +616,7 @@ Describe "-ReportSummary switch" { } # using statements are only supported in v5+ -Describe "Handles parse errors due to unknown types" -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { +Describe "Handles parse errors due to unknown types" -Skip:($testingLibraryUsage) { BeforeAll { $script = @' using namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels @@ -640,7 +640,7 @@ Describe "Handles parse errors due to unknown types" -Skip:($testingLibraryUsage } } -Describe 'Handles static Singleton (issue 1182)' -Skip:($testingLibraryUsage -or ($PSVersionTable.PSVersion -lt '5.0')) { +Describe 'Handles static Singleton (issue 1182)' -Skip:($testingLibraryUsage) { It 'Does not throw or return diagnostic record' { $scriptDefinition = 'class T { static [T]$i }; function foo { [CmdletBinding()] param () $script:T.WriteLog() }' Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -ErrorAction Stop | Should -BeNullOrEmpty diff --git a/Tests/Engine/ModuleDependencyHandler.tests.ps1 b/Tests/Engine/ModuleDependencyHandler.tests.ps1 index 09bd74912..92735fb9e 100644 --- a/Tests/Engine/ModuleDependencyHandler.tests.ps1 +++ b/Tests/Engine/ModuleDependencyHandler.tests.ps1 @@ -3,7 +3,7 @@ function Get-Skip { - if ($testingLibararyUsage -or ($PSVersionTable.PSVersion -lt '5.0')) + if ($testingLibararyUsage) { return $true } @@ -50,7 +50,6 @@ Describe "Resolve DSC Resource Dependency" { Context "Module handler class" { BeforeAll { - if ($PSVersionTable.PSVersion -lt '5.0') { return } $moduleHandlerType = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.ModuleDependencyHandler] $oldEnvVars = Get-Item Env:\* | Sort-Object -Property Key $savedPSModulePath = $env:PSModulePath @@ -59,7 +58,7 @@ Describe "Resolve DSC Resource Dependency" { if ( $skipTest ) { return } $env:PSModulePath = $savedPSModulePath } - It "Sets defaults correctly" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "Sets defaults correctly" { $rsp = [runspacefactory]::CreateRunspace() $rsp.Open() $depHandler = $moduleHandlerType::new($rsp) @@ -82,15 +81,15 @@ Describe "Resolve DSC Resource Dependency" { $rsp.Dispose() } - It "Keeps the environment variables unchanged" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "Keeps the environment variables unchanged" { Test-EnvironmentVariables($oldEnvVars) } - It "Throws if runspace is null" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "Throws if runspace is null" { {$moduleHandlerType::new($null)} | Should -Throw } - It "Throws if runspace is not opened" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "Throws if runspace is not opened" { $rsp = [runspacefactory]::CreateRunspace() {$moduleHandlerType::new($rsp)} | Should -Throw $rsp.Dispose() diff --git a/Tests/Engine/ModuleHelp.Tests.ps1 b/Tests/Engine/ModuleHelp.Tests.ps1 index ac40dcf94..21188e019 100644 --- a/Tests/Engine/ModuleHelp.Tests.ps1 +++ b/Tests/Engine/ModuleHelp.Tests.ps1 @@ -62,14 +62,8 @@ $paramBlockList = @( 'AttachAndDebug' # Reason: When building with DEGUG configuration, an additional parameter 'AttachAndDebug' will be added to Invoke-ScriptAnalyzer and Invoke-Formatter, but there is no Help for those, as they are not intended for production usage. ) [string] $ModuleName = 'PSScriptAnalyzer' -if ($PSVersionTable.PSVersion -lt '5.0') { - $ms = New-Object -TypeName 'Microsoft.PowerShell.Commands.ModuleSpecification' -ArgumentList $ModuleName - $commands = Get-Command -Module $ms.Name -} -else { - $ms = [Microsoft.PowerShell.Commands.ModuleSpecification]@{ ModuleName = $ModuleName; RequiredVersion = $RequiredVersion } - $commands = Get-Command -FullyQualifiedModule $ms -} +$ms = [Microsoft.PowerShell.Commands.ModuleSpecification]@{ ModuleName = $ModuleName; RequiredVersion = $RequiredVersion } +$commands = Get-Command -FullyQualifiedModule $ms $testCases = $commands.ForEach{ @{ @@ -92,9 +86,6 @@ BeforeAll { $paramBlockList = @( 'AttachAndDebug' # Reason: When building with DEGUG configuration, an additional parameter 'AttachAndDebug' will be added to Invoke-ScriptAnalyzer and Invoke-Formatter, but there is no Help for those, as they are not intended for production usage. ) - if ($PSVersionTable.PSVersion -lt '5.0') { - $paramBlockList += 'SaveDscDependency' - } } diff --git a/Tests/Engine/RuleSuppression.tests.ps1 b/Tests/Engine/RuleSuppression.tests.ps1 index 2d81f6305..2d31a6ddf 100644 --- a/Tests/Engine/RuleSuppression.tests.ps1 +++ b/Tests/Engine/RuleSuppression.tests.ps1 @@ -404,7 +404,7 @@ function Allow-GetFooBar { } Context "Rule suppression within DSC Configuration definition" { - It "Suppresses rule" -Skip:($IsLinux -or $IsMacOS -or ($PSVersionTable.PSVersion.Major -lt 5)) { + It "Suppresses rule" -Skip:($IsLinux -or $IsMacOS) { $suppressedRule = Invoke-ScriptAnalyzer -ScriptDefinition $ruleSuppressionInConfiguration -SuppressedOnly $suppressedRule.Count | Should -Be 1 } diff --git a/Tests/Engine/RuleSuppressionClass.tests.ps1 b/Tests/Engine/RuleSuppressionClass.tests.ps1 index 28c3aad22..22dc8e333 100644 --- a/Tests/Engine/RuleSuppressionClass.tests.ps1 +++ b/Tests/Engine/RuleSuppressionClass.tests.ps1 @@ -2,11 +2,6 @@ # Licensed under the MIT License. BeforeAll { - $script:skipForV3V4 = $true - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') { - $script:skipForV3V4 = $false - } - $violationsUsingScriptDefinition = Invoke-ScriptAnalyzer -ScriptDefinition (Get-Content -Raw "$PSScriptRoot\RuleSuppression.ps1") $violations = Invoke-ScriptAnalyzer "$PSScriptRoot\RuleSuppression.ps1" } @@ -14,7 +9,7 @@ BeforeAll { Describe "RuleSuppressionWithoutScope" { Context "Class" { - It "Does not raise violations" -skip:$script:skipForV3V4 { + It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingInvokeExpression" } $suppression.Count | Should -Be 0 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingInvokeExpression" } @@ -23,7 +18,7 @@ Describe "RuleSuppressionWithoutScope" { } Context "FunctionInClass" { - It "Does not raise violations" -skip:$script:skipForV3V4 { + It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingCmdletAliases" } $suppression.Count | Should -Be 0 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingCmdletAliases" } @@ -32,7 +27,7 @@ Describe "RuleSuppressionWithoutScope" { } Context "Script" { - It "Does not raise violations" -skip:$script:skipForV3V4 { + It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } $suppression.Count | Should -Be 0 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } @@ -41,7 +36,7 @@ Describe "RuleSuppressionWithoutScope" { } Context "RuleSuppressionID" { - It "Only suppress violations for that ID" -skip:$script:skipForV3V4 { + It "Only suppress violations for that ID" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } $suppression.Count | Should -Be 1 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidDefaultValueForMandatoryParameter" } @@ -52,7 +47,7 @@ Describe "RuleSuppressionWithoutScope" { Describe "RuleSuppressionWithScope" { Context "FunctionScope" { - It "Does not raise violations" -skip:$script:skipForV3V4 { + It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } $suppression.Count | Should -Be 0 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } @@ -61,7 +56,7 @@ Describe "RuleSuppressionWithScope" { } Context "ClassScope" { - It "Does not raise violations" -skip:$script:skipForV3V4 { + It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingConvertToSecureStringWithPlainText" } $suppression.Count | Should -Be 0 $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingConvertToSecureStringWithPlainText" } diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 7558abf88..f252cccb5 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -86,8 +86,8 @@ $x = @{ 'key'="value" } It "Should ignore if a hashtable has a single key-value pair across multiple lines" { $def = @' -$x = @{ - 'key'="value" +$x = @{ + 'key'="value" } '@ Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 @@ -128,10 +128,9 @@ Configuration MyDscConfiguration { } } - if ($PSVersionTable.PSVersion.Major -ge 5) { - Context "When assignment statements are in DSC Configuration that has parse errors" { - It "Should find violations when assignment statements are not aligned" -skip:($IsLinux -or $IsMacOS) { - $def = @' + Context "When assignment statements are in DSC Configuration that has parse errors" { + It "Should find violations when assignment statements are not aligned" -skip:($IsLinux -or $IsMacOS) { + $def = @' Configuration Sample_ChangeDescriptionAndPermissions { Import-DscResource -Module NonExistentModule @@ -152,14 +151,13 @@ Configuration Sample_ChangeDescriptionAndPermissions } } '@ - # This invocation will throw parse error caused by "Undefined DSC resource" because - # NonExistentModule is not really avaiable to load. Therefore we set erroraction to - # SilentlyContinue - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings -ErrorAction SilentlyContinue | - Where-Object { $_.Severity -ne "ParseError" } | - Get-Count | - Should -Be 4 - } + # This invocation will throw parse error caused by "Undefined DSC resource" because + # NonExistentModule is not really avaiable to load. Therefore we set erroraction to + # SilentlyContinue + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings -ErrorAction SilentlyContinue | + Where-Object { $_.Severity -ne "ParseError" } | + Get-Count | + Should -Be 4 } } } diff --git a/Tests/Rules/AvoidGlobalAliases.tests.ps1 b/Tests/Rules/AvoidGlobalAliases.tests.ps1 index e57b00446..bfb1c4e0d 100644 --- a/Tests/Rules/AvoidGlobalAliases.tests.ps1 +++ b/Tests/Rules/AvoidGlobalAliases.tests.ps1 @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -$IsV3OrV4 = ($PSVersionTable.PSVersion.Major -eq 3) -or ($PSVersionTable.PSVersion.Major -eq 4) - BeforeAll { $AvoidGlobalAliasesError = "Avoid creating aliases with a Global scope." $violationName = "PSAvoidGlobalAliases" @@ -12,17 +10,17 @@ BeforeAll { Describe "$violationName " { Context "When there are violations" { - It "Has 4 avoid global alias violations" -Skip:$IsV3OrV4 { + It "Has 4 avoid global alias violations" { $violations.Count | Should -Be 4 } - It "Has the correct description message" -Skip:$IsV3OrV4 { + It "Has the correct description message" { $violations[0].Message | Should -Match $AvoidGlobalAliasesError } } Context "When there are no violations" { - It "Returns no violations" -Skip:$IsV3OrV4 { + It "Returns no violations" { $noViolations.Count | Should -Be 0 } } diff --git a/Tests/Rules/DscExamplesPresent.tests.ps1 b/Tests/Rules/DscExamplesPresent.tests.ps1 index fd98c00dd..e4dc8e547 100644 --- a/Tests/Rules/DscExamplesPresent.tests.ps1 +++ b/Tests/Rules/DscExamplesPresent.tests.ps1 @@ -5,7 +5,7 @@ BeforeAll { $ruleName = "PSDSCDscExamplesPresent" } - Describe "DscExamplesPresent rule in class based resource" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + Describe "DscExamplesPresent rule in class based resource" { BeforeAll { $examplesPath = "$PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\Examples" $classResourcePath = "$PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1" diff --git a/Tests/Rules/DscTestsPresent.tests.ps1 b/Tests/Rules/DscTestsPresent.tests.ps1 index b81104d1d..e49e80623 100644 --- a/Tests/Rules/DscTestsPresent.tests.ps1 +++ b/Tests/Rules/DscTestsPresent.tests.ps1 @@ -5,7 +5,7 @@ BeforeAll { $ruleName = "PSDSCDscTestsPresent" } - Describe "DscTestsPresent rule in class based resource" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + Describe "DscTestsPresent rule in class based resource" { BeforeAll { $testsPath = "$PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\Tests" $classResourcePath = "$PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1" diff --git a/Tests/Rules/PSCredentialType.tests.ps1 b/Tests/Rules/PSCredentialType.tests.ps1 index ceb04e3ee..4aa2c35a7 100644 --- a/Tests/Rules/PSCredentialType.tests.ps1 +++ b/Tests/Rules/PSCredentialType.tests.ps1 @@ -12,9 +12,6 @@ Describe "PSCredentialType" { Context "When there are violations" { BeforeAll { $expectedViolations = 1 - if (($PSVersionTable.PSVersion.Major -eq 3) -or ($PSVersionTable.PSVersion.Major -eq 4)) { - $expectedViolations = 2 - } } It ("has correct count of PSCredential type violations" -f $expectedViolations) { $violations.Count | Should -Be $expectedViolations diff --git a/Tests/Rules/ProvideCommentHelp.tests.ps1 b/Tests/Rules/ProvideCommentHelp.tests.ps1 index 2216f15f0..f0900b07d 100644 --- a/Tests/Rules/ProvideCommentHelp.tests.ps1 +++ b/Tests/Rules/ProvideCommentHelp.tests.ps1 @@ -22,9 +22,7 @@ BeforeAll { $violations = Invoke-ScriptAnalyzer $PSScriptRoot\BadCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') { - $dscViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - } + $dscViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} $noViolations = Invoke-ScriptAnalyzer $PSScriptRoot\GoodCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} @@ -334,7 +332,7 @@ $s$s$s$s } - It "Does not count violation in DSC class" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + It "Does not count violation in DSC class" { $dscViolations.Count | Should -Be 0 } } diff --git a/Tests/Rules/ReturnCorrectTypesForDSCFunctions.tests.ps1 b/Tests/Rules/ReturnCorrectTypesForDSCFunctions.tests.ps1 index 1ed4f610f..fc10ae39c 100644 --- a/Tests/Rules/ReturnCorrectTypesForDSCFunctions.tests.ps1 +++ b/Tests/Rules/ReturnCorrectTypesForDSCFunctions.tests.ps1 @@ -8,11 +8,8 @@ BeforeAll { $violations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1 | Where-Object {$_.RuleName -eq $violationName} $noViolations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAny\MSFT_WaitForAny.psm1 | Where-Object {$_.RuleName -eq $violationName} - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') - { - $classViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\BadDscResource\BadDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - } + $classViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\BadDscResource\BadDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} + $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} } Describe "ReturnCorrectTypesForDSCFunctions" { @@ -33,7 +30,7 @@ Describe "ReturnCorrectTypesForDSCFunctions" { } } - Describe "StandardDSCFunctionsInClass" -Skip:($PSVersionTable.PSVersion -lt '5.0') { + Describe "StandardDSCFunctionsInClass" { Context "When there are violations" { It "has 4 return correct types for DSC functions violations" { $classViolations.Count | Should -Be 4 diff --git a/Tests/Rules/UseCompatibleSyntax.Tests.ps1 b/Tests/Rules/UseCompatibleSyntax.Tests.ps1 index 64ac464ae..263adf8a8 100644 --- a/Tests/Rules/UseCompatibleSyntax.Tests.ps1 +++ b/Tests/Rules/UseCompatibleSyntax.Tests.ps1 @@ -12,15 +12,10 @@ BeforeDiscovery { @{ Script = '$y.$methodWithAVeryLongName()'; Versions = @(3) } @{ Script = '$typeExpression::$staticMember'; Versions = @() } @{ Script = '$typeExpression::$dynamicStaticMethodName()'; Versions = @(3) } + @{ Script = "class MyClass { }"; Versions = @(3,4) } + @{ Script = "enum MyEnum { One; Two }"; Versions = @(3,4) } ) - # PS v3/4 won't parse classes or enums - if ($PSVersionTable.PSVersion.Major -ge 5) - { - $testCases += @( - @{ Script = "class MyClass { }"; Versions = @(3,4) } - @{ Script = "enum MyEnum { One; Two }"; Versions = @(3,4) } - ) - } + # PS v6+ won't parse workflows if ($PSVersionTable.PSVersion.Major -le 5) { @@ -79,16 +74,7 @@ Describe "PSUseCompatibleSyntax" { $diagnostics = Invoke-ScriptAnalyzer -IncludeRule PSUseCompatibleSyntax -Path "$PSScriptRoot/CompatibilityRuleAssets/IncompatibleScript.ps1" -Settings $settings ` | Where-Object { $_.RuleName -eq 'PSUseCompatibleSyntax' } - if ($PSVersionTable.PSVersion.Major -ge 5) - { - $expected = 5 - } - else - { - # PSv3/4 can't detect class/enum parts - $expected = 4 - } - + $expected = 5 $diagnostics.Count | Should -Be $expected } diff --git a/Tests/Rules/UseDSCResourceFunctions.tests.ps1 b/Tests/Rules/UseDSCResourceFunctions.tests.ps1 index 2efc16ec0..9112d6e22 100644 --- a/Tests/Rules/UseDSCResourceFunctions.tests.ps1 +++ b/Tests/Rules/UseDSCResourceFunctions.tests.ps1 @@ -7,12 +7,8 @@ BeforeAll { $violationName = "PSDSCStandardDSCFunctionsInResource" $violations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1 | Where-Object {$_.RuleName -eq $violationName} $noViolations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAny\MSFT_WaitForAny.psm1 | Where-Object {$_.RuleName -eq $violationName} - - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') - { - $classViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\BadDscResource\BadDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - } + $classViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\BadDscResource\BadDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} + $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} } @@ -34,7 +30,7 @@ Describe "StandardDSCFunctionsInResource" { } } -Describe "StandardDSCFunctionsInClass" -Skip:($PSVersionTable.PSVersion -lt '5.0') { +Describe "StandardDSCFunctionsInClass" { Context "When there are violations" { It "has 1 missing standard DSC functions violation" { $classViolations.Count | Should -Be 1 diff --git a/Tests/Rules/UseIdenticalParametersDSC.tests.ps1 b/Tests/Rules/UseIdenticalParametersDSC.tests.ps1 index c47a2bf56..622105087 100644 --- a/Tests/Rules/UseIdenticalParametersDSC.tests.ps1 +++ b/Tests/Rules/UseIdenticalParametersDSC.tests.ps1 @@ -6,11 +6,7 @@ BeforeAll { $violationName = "PSDSCUseIdenticalParametersForDSC" $violations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1 | Where-Object {$_.RuleName -eq $violationName} $noViolations = Invoke-ScriptAnalyzer $PSScriptRoot\DSCResourceModule\DSCResources\MSFT_WaitForAny\MSFT_WaitForAny.psm1 | Where-Object {$_.RuleName -eq $violationName} - - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') - { - $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - } + $noClassViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} } @@ -30,12 +26,8 @@ Describe "UseIdenticalParametersDSC" { $noViolations.Count | Should -Be 0 } - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') - { - - It "returns no violations for DSC Classes" { - $noClassViolations.Count | Should -Be 0 - } + It "returns no violations for DSC Classes" { + $noClassViolations.Count | Should -Be 0 } } } diff --git a/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 b/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 index 8cc657e0b..fa1087847 100644 --- a/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 +++ b/Tests/Rules/UseOutputTypeCorrectly.tests.ps1 @@ -5,10 +5,7 @@ BeforeAll { $violationMessage = "The cmdlet 'Verb-Files' returns an object of type 'System.Collections.Hashtable' but this type is not declared in the OutputType attribute." $violationName = "PSUseOutputTypeCorrectly" $violations = Invoke-ScriptAnalyzer $PSScriptRoot\BadCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') - { - $dscViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} - } + $dscViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $PSScriptRoot\DSCResourceModule\DSCResources\MyDscResource\MyDscResource.psm1 | Where-Object {$_.RuleName -eq $violationName} $noViolations = Invoke-ScriptAnalyzer $PSScriptRoot\GoodCmdlet.ps1 | Where-Object {$_.RuleName -eq $violationName} } @@ -23,10 +20,8 @@ Describe "UseOutputTypeCorrectly" { $violations[1].Message | Should -Match $violationMessage } - if ($PSVersionTable.PSVersion -ge [Version]'5.0.0') { - It "Does not count violation in DSC class" { - $dscViolations.Count | Should -Be 0 - } + It "Does not count violation in DSC class" { + $dscViolations.Count | Should -Be 0 } } diff --git a/Tests/Rules/UseShouldProcessCorrectly.tests.ps1 b/Tests/Rules/UseShouldProcessCorrectly.tests.ps1 index 2c0314f70..ed5b5e084 100644 --- a/Tests/Rules/UseShouldProcessCorrectly.tests.ps1 +++ b/Tests/Rules/UseShouldProcessCorrectly.tests.ps1 @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -$IsV3OrV4 = ($PSVersionTable.PSVersion.Major -eq 3) -or ($PSVersionTable.PSVersion.Major -eq 4) - BeforeAll { $violationMessage = "'Verb-Files' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue." $violationName = "PSShouldProcess" @@ -179,7 +177,7 @@ function Remove-Foo { } # Install-Module is present by default only on PSv5 and above - It "finds no violation when caller declares SupportsShouldProcess and callee is a function with ShouldProcess" -Skip:$IsV3OrV4 { + It "finds no violation when caller declares SupportsShouldProcess and callee is a function with ShouldProcess" { $scriptDef = @' function Install-Foo { [CmdletBinding(SupportsShouldProcess)] @@ -231,7 +229,7 @@ function Install-ModuleWithDeps { } # Install-Module is present by default only on PSv5 and above - It "finds no violation for a function with self reference and implicit call to ShouldProcess" -Skip:$IsV3OrV4 { + It "finds no violation for a function with self reference and implicit call to ShouldProcess" { $scriptDef = @' function Install-ModuleWithDeps { [CmdletBinding(SupportsShouldProcess)] diff --git a/build.psm1 b/build.psm1 index 5daba36ba..041b207a9 100644 --- a/build.psm1 +++ b/build.psm1 @@ -317,20 +317,9 @@ function Test-ScriptAnalyzer # and ".../out/PSScriptAnalyzer" is added to env:PSModulePath # # - $major = $PSVersionTable.PSVersion.Major - if ( $major -lt 5 ) { - # get the directory name of the destination, we need to change it - $versionDirectoryRoot = Split-Path $script:destinationDir - $testModulePath = Join-Path $versionDirectoryRoot $analyzerName - } - else { - $testModulePath = Join-Path "${projectRoot}" -ChildPath out - } + $testModulePath = Join-Path "${projectRoot}" -ChildPath out $testScripts = "'${projectRoot}\Tests\Build','${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" try { - if ( $major -lt 5 ) { - Rename-Item $script:destinationDir ${testModulePath} - } $savedModulePath = $env:PSModulePath $env:PSModulePath = "${testModulePath}{0}${env:PSModulePath}" -f [System.IO.Path]::PathSeparator $analyzerPsd1Path = Join-Path -Path $script:destinationDir -ChildPath "$analyzerName.psd1" @@ -353,9 +342,6 @@ function Test-ScriptAnalyzer } finally { $env:PSModulePath = $savedModulePath - if ( $major -lt 5 ) { - Rename-Item ${testModulePath} ${script:destinationDir} - } } } } From 520d2aa0a371d0d4986d801221c19de8f43defd5 Mon Sep 17 00:00:00 2001 From: Denis Melnikov <87656893+Haimasker@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:40 +0300 Subject: [PATCH 110/130] Add UseCorrectParametersKind rule (#2149) * Consistent function parameters definition * Rule is disabled by default * Possible types of preferred function parameters are: "Inline", "ParamBlock" Co-authored-by: Christoph Bergmeister --- Rules/Strings.resx | 15 + Rules/UseConsistentParametersKind.cs | 171 +++++++ Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 4 +- .../UseConsistentParametersKind.Tests.ps1 | 428 ++++++++++++++++++ docs/Rules/README.md | 1 + docs/Rules/UseConsistentParametersKind.md | 57 +++ 6 files changed, 674 insertions(+), 2 deletions(-) create mode 100644 Rules/UseConsistentParametersKind.cs create mode 100644 Tests/Rules/UseConsistentParametersKind.Tests.ps1 create mode 100644 docs/Rules/UseConsistentParametersKind.md diff --git a/Rules/Strings.resx b/Rules/Strings.resx index c7645c9cf..f94c9cceb 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1236,4 +1236,19 @@ The reserved word '{0}' was used as a function name. This should be avoided. + + Use correct function parameters definition kind. + + + Use consistent parameters definition kind to prevent potential unexpected behavior with inline functions parameters or param() block. + + + UseConsistentParametersKind + + + Use param() block in function body instead of inline parameters. + + + Use inline parameters definition instead of param() block in function body. + \ No newline at end of file diff --git a/Rules/UseConsistentParametersKind.cs b/Rules/UseConsistentParametersKind.cs new file mode 100644 index 000000000..fd2dfa732 --- /dev/null +++ b/Rules/UseConsistentParametersKind.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// UseConsistentParametersKind: Checks if function parameters definition kind is same as preferred. + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class UseConsistentParametersKind : ConfigurableRule + { + private enum ParametersDefinitionKind + { + Inline, + ParamBlock + } + + private ParametersDefinitionKind parametersKind; + + /// + /// Construct an object of UseConsistentParametersKind type. + /// + public UseConsistentParametersKind() : base() + { + Enable = false; // Disable rule by default + } + + /// + /// The type of preferred parameters definition for functions. + /// + /// Default value is "ParamBlock". + /// + [ConfigurableRuleProperty(defaultValue: "ParamBlock")] + public string ParametersKind + { + get + { + return parametersKind.ToString(); + } + set + { + if (String.IsNullOrWhiteSpace(value) || + !Enum.TryParse(value, true, out parametersKind)) + { + parametersKind = ParametersDefinitionKind.ParamBlock; + } + } + } + + /// + /// AnalyzeScript: Analyze the script to check if any function is using not preferred parameters kind. + /// + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) { throw new ArgumentNullException(Strings.NullAstErrorMessage); } + + IEnumerable functionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true); + if (parametersKind == ParametersDefinitionKind.ParamBlock) + { + return checkInlineParameters(functionAsts, fileName); + } + else + { + return checkParamBlockParameters(functionAsts, fileName); + } + } + + private IEnumerable checkInlineParameters(IEnumerable functionAsts, string fileName) + { + foreach (FunctionDefinitionAst functionAst in functionAsts) + { + if (functionAst.Parameters != null) + { + yield return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, Strings.UseConsistentParametersKindInlineError, functionAst.Name), + functionAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + ); + } + } + } + + private IEnumerable checkParamBlockParameters(IEnumerable functionAsts, string fileName) + { + foreach (FunctionDefinitionAst functionAst in functionAsts) + { + if (functionAst.Body.ParamBlock != null) + { + yield return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, Strings.UseConsistentParametersKindParamBlockError, functionAst.Name), + functionAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + ); + } + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseConsistentParametersKindCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseConsistentParametersKindDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseConsistentParametersKindName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 8dca8dcdc..8e433d071 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -63,8 +63,8 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 71 - $defaultRules.Count | Should -Be $expectedNumRules + $expectedNumRules = 72 + $defaultRules.Count | Should -Be $expectedNumRules } It "is a positional parameter" { diff --git a/Tests/Rules/UseConsistentParametersKind.Tests.ps1 b/Tests/Rules/UseConsistentParametersKind.Tests.ps1 new file mode 100644 index 000000000..1dfae19f2 --- /dev/null +++ b/Tests/Rules/UseConsistentParametersKind.Tests.ps1 @@ -0,0 +1,428 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +Describe 'UseConsistentParametersKind' { + Context 'When preferred parameters kind is set to "ParamBlock" explicitly' { + + BeforeAll { + $ruleConfiguration = @{ + Enable = $true + ParametersKind = "ParamBlock" + } + $settings = @{ + IncludeRules = @("PSUseConsistentParametersKind") + Rules = @{ + PSUseConsistentParametersKind = $ruleConfiguration + } + } + } + + It "Returns no violations for parameters outside function" { + $scriptDefinition = @' +[Parameter()]$FirstParam +[Parameter()]$SecondParam + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for param() block outside function" { + $scriptDefinition = @' +param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function without parameters" { + $scriptDefinition = @' +function Test-Function { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty param() block" { + $scriptDefinition = @' +function Test-Function { + param() + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with non-empty param() block" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty inline parameters" { + $scriptDefinition = @' +function Test-Function() { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty inline parameters and non-empty param() block" { + $scriptDefinition = @' +function Test-Function() { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns violations for function with non-empty inline parameters" { + $scriptDefinition = @' +function Test-Function( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + } + + Context 'When preferred parameters kind is set to "ParamBlock" via default value' { + + BeforeAll { + $ruleConfiguration = @{ + Enable = $true + } + $settings = @{ + IncludeRules = @("PSUseConsistentParametersKind") + Rules = @{ + PSUseConsistentParametersKind = $ruleConfiguration + } + } + } + + It "Returns no violations for parameters outside function" { + $scriptDefinition = @' +[Parameter()]$FirstParam +[Parameter()]$SecondParam + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for param() block outside function" { + $scriptDefinition = @' +param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function without parameters" { + $scriptDefinition = @' +function Test-Function { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty param() block" { + $scriptDefinition = @' +function Test-Function { + param() + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with non-empty param() block" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty inline parameters" { + $scriptDefinition = @' +function Test-Function() { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function with empty inline parameters and non-empty param() block" { + $scriptDefinition = @' +function Test-Function() { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns violations for function with non-empty inline parameters" { + $scriptDefinition = @' +function Test-Function( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + } + + Context 'When preferred parameters kind is set to "Inline" explicitly' { + + BeforeAll { + $ruleConfiguration = @{ + Enable = $true + ParametersKind = "Inline" + } + + $settings = @{ + IncludeRules = @("PSUseConsistentParametersKind") + Rules = @{ + PSUseConsistentParametersKind = $ruleConfiguration + } + } + } + + It "Returns no violations for parameters outside function" { + $scriptDefinition = @' +[Parameter()]$FirstParam +[Parameter()]$SecondParam + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for param() block outside function" { + $scriptDefinition = @' +param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) + +$FirstParam | Out-Null +$SecondParam | Out-Null +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns no violations for function without parameters" { + $scriptDefinition = @' +function Test-Function { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns violations for function with empty param() block" { + $scriptDefinition = @' +function Test-Function { + param() + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + + It "Returns violations for function with non-empty param() block" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + + It "Returns no violations for function with empty inline parameters" { + $scriptDefinition = @' +function Test-Function() { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + + It "Returns violations for function with empty inline parameters and non-empty param() block" { + $scriptDefinition = @' +function Test-Function() { + param( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam + ) + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + + It "Returns no violations for function with non-empty inline parameters" { + $scriptDefinition = @' +function Test-Function( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) { + return +} +'@ + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + } + + Context 'When rule is disabled explicitly' { + + BeforeAll { + $ruleConfiguration = @{ + Enable = $false + ParametersKind = "ParamBlock" + } + $settings = @{ + IncludeRules = @("PSUseConsistentParametersKind") + Rules = @{ + PSUseConsistentParametersKind = $ruleConfiguration + } + } + } + + It "Returns no violations for function with non-empty inline parameters" { + $scriptDefinition = @' +function Test-Function( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) { + return +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + } + + Context 'When rule is disabled via default "Enable" value' { + + BeforeAll { + $ruleConfiguration = @{ + ParametersKind = "ParamBlock" + } + $settings = @{ + IncludeRules = @("PSUseConsistentParametersKind") + Rules = @{ + PSUseConsistentParametersKind = $ruleConfiguration + } + } + } + + It "Returns no violations for function with non-empty inline parameters" { + $scriptDefinition = @' +function Test-Function( + [Parameter()]$FirstParam, + [Parameter()]$SecondParam +) { + return +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index da1058bc2..51858d0de 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -68,6 +68,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [UseCompatibleSyntax](./UseCompatibleSyntax.md) | Warning | No | Yes | | [UseCompatibleTypes](./UseCompatibleTypes.md) | Warning | No | Yes | | [UseConsistentIndentation](./UseConsistentIndentation.md) | Warning | No | Yes | +| [UseConsistentParametersKind](./UseConsistentParametersKind.md) | Warning | No | Yes | | [UseConsistentWhitespace](./UseConsistentWhitespace.md) | Warning | No | Yes | | [UseCorrectCasing](./UseCorrectCasing.md) | Information | No | Yes | | [UseDeclaredVarsMoreThanAssignments](./UseDeclaredVarsMoreThanAssignments.md) | Warning | Yes | | diff --git a/docs/Rules/UseConsistentParametersKind.md b/docs/Rules/UseConsistentParametersKind.md new file mode 100644 index 000000000..04a323b3d --- /dev/null +++ b/docs/Rules/UseConsistentParametersKind.md @@ -0,0 +1,57 @@ +# UseConsistentParametersKind + +**Severity Level: Warning** + +## Description + +All functions should have same parameters definition kind specified in the rule. +Possible kinds are: +1. `Inline`, i.e.: +```PowerShell +function f([Parameter()]$FirstParam) { + return +} +``` +2. `ParamBlock`, i.e.: +```PowerShell +function f { + param([Parameter()]$FirstParam) + return +} +``` + +* For information: in simple scenarios both function definitions above may be considered as equal. Using this rule as-is is more for consistent code-style than functional, but it can be useful in combination with other rules. + +## How to Fix + +Rewrite function so it defines parameters as specified in the rule + +## Example + +### When the rule sets parameters definition kind to 'Inline': +```PowerShell +# Correct +function f([Parameter()]$FirstParam) { + return +} + +# Incorrect +function g { + param([Parameter()]$FirstParam) + return +} +``` + +### When the rule sets parameters definition kind to 'ParamBlock': +```PowerShell +# Inorrect +function f([Parameter()]$FirstParam) { + return +} + +# Correct +function g { + param([Parameter()]$FirstParam) + return +} +``` \ No newline at end of file From b3653a9496f7ee5865a01ebc19ca204775c3eff3 Mon Sep 17 00:00:00 2001 From: Henrique Azevedo Date: Tue, 17 Mar 2026 22:27:59 -0300 Subject: [PATCH 111/130] Fix typos in resource strings and associated C# references (#2163) * Fix typos and grammatical errors in resource strings - Correct misspellings in Strings.resx (e.g., 'equaltiy', 'whitepsace') - Rename misspelled resource keys for consistency - Update C# rule files to reflect new resource key names * Strings: fix grammar and casing in rule descriptions - Combine sentence fragment in AvoidUsingEmptyCatchBlockDescription for better flow - Update 'Whatif' to 'WhatIf' in UseSupportsShouldProcess strings to match PowerShell naming conventions * Strings: improve AvoidUsingEmptyCatchBlockDescription phrasing --- Rules/PossibleIncorrectComparisonWithNull.cs | 2 +- Rules/Strings.resx | 42 +++++++++---------- Rules/UseLiteralInitializerForHashtable.cs | 10 ++--- ...eShouldProcessForStateChangingFunctions.cs | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Rules/PossibleIncorrectComparisonWithNull.cs b/Rules/PossibleIncorrectComparisonWithNull.cs index 3302583ae..692f49f13 100644 --- a/Rules/PossibleIncorrectComparisonWithNull.cs +++ b/Rules/PossibleIncorrectComparisonWithNull.cs @@ -104,7 +104,7 @@ private IEnumerable GetCorrectionExtent(BinaryExpressionAst bi binaryExpressionAst.Extent.EndColumnNumber, $"{binaryExpressionAst.Right.Extent.Text} {binaryExpressionAst.ErrorPosition.Text} {binaryExpressionAst.Left.Extent.Text}", binaryExpressionAst.Extent.File, - Strings.PossibleIncorrectComparisonWithNullSuggesteCorrectionDescription + Strings.PossibleIncorrectComparisonWithNullSuggestedCorrectionDescription ); yield return correction; diff --git a/Rules/Strings.resx b/Rules/Strings.resx index f94c9cceb..67f925ede 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -124,7 +124,7 @@ Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. - Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks. + Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently cause problems, it can, so it should be avoided where possible. To fix a violation of this rule, use Write-Error or throw statements in catch blocks. Avoid Using Empty Catch Block @@ -136,7 +136,7 @@ Avoid Using Invoke-Expression - Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. + Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using named parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. Avoid Using Positional Parameters @@ -178,7 +178,7 @@ No Global Variables - Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. + Checks that $null is on the left side of any equality comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comparison are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. $null should be on the left side of equality comparisons. @@ -618,7 +618,7 @@ Use ShouldProcess For State Changing Functions - + Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'. @@ -703,7 +703,7 @@ PowerShell help file needs to use UTF8 Encoding. - File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file. + File {0} has to use UTF8 instead of {1} encoding because it is a PowerShell help file. UseUTF8EncodingForHelpFile @@ -742,7 +742,7 @@ Misleading Backtick - Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. + Ending a line with an escaped whitespace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. MisleadingBacktick @@ -786,16 +786,16 @@ Replace {0} with {1} - + Create hashtables with literal initializers - + Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default - - Create hashtables with literal initliazers + + Create hashtables with literal initializers - + UseLiteralInitializerForHashtable @@ -874,7 +874,7 @@ The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - Avoid global functiosn and aliases + Avoid global functions and aliases Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems. @@ -979,7 +979,7 @@ Use consistent indentation - Each statement block should have a consistent indenation. + Each statement block should have a consistent indentation. Indentation not consistent @@ -991,7 +991,7 @@ Use whitespaces - Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';') + Check for whitespace between keyword and open paren/curly, around assignment operator ('='), around arithmetic operators and after separators (',' and ';') Use space before open brace. @@ -1015,10 +1015,10 @@ Use SupportsShouldProcess - Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess. + Commands typically provide Confirm and WhatIf parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a command needs Confirm and WhatIf parameters, then it should support ShouldProcess. - Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. + WhatIf and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. AlignAssignmentStatement @@ -1042,10 +1042,10 @@ Use a different variable name - Changing automtic variables might have undesired side effects + Changing automatic variables might have undesired side effects - This automatic variables is built into PowerShell and readonly. + This automatic variable is built into PowerShell and readonly. The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name. @@ -1077,7 +1077,7 @@ PossibleIncorrectUsageOfRedirectionOperator - + Use $null on the left hand side for safe comparison with $null. @@ -1177,7 +1177,7 @@ Avoid multiple type specifiers on parameters - Prameter should not have more than one type specifier. + Parameter should not have more than one type specifier. Parameter '{0}' has more than one type specifier. @@ -1219,7 +1219,7 @@ Avoid sending credentials and secrets over unencrypted connections. - The insecure AllowUsingUnencryptedAuthentication switch was used. This should be avoided except for compatability with legacy systems. + The insecure AllowUnencryptedAuthentication switch was used. This should be avoided except for compatibility with legacy systems. AvoidUsingAllowUnencryptedAuthentication diff --git a/Rules/UseLiteralInitializerForHashtable.cs b/Rules/UseLiteralInitializerForHashtable.cs index 8f59d8332..56a31508f 100644 --- a/Rules/UseLiteralInitializerForHashtable.cs +++ b/Rules/UseLiteralInitializerForHashtable.cs @@ -62,7 +62,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) /// public string GetCommonName() { - return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitilializerForHashtableCommonName); + return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitializerForHashtableCommonName); } /// @@ -70,7 +70,7 @@ public string GetCommonName() /// public string GetDescription() { - return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitilializerForHashtableDescription); + return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitializerForHashtableDescription); } /// @@ -82,7 +82,7 @@ public string GetName() CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), - Strings.UseLiteralInitilializerForHashtableName); + Strings.UseLiteralInitializerForHashtableName); } /// @@ -170,7 +170,7 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio || !HasIgnoreCaseComparerArg(methodCallAst.Arguments)) { var dr = new DiagnosticRecord( - Strings.UseLiteralInitilializerForHashtableDescription, + Strings.UseLiteralInitializerForHashtableDescription, methodCallAst.Extent, GetName(), GetDiagnosticSeverity(), @@ -205,7 +205,7 @@ private void AnalyzeNewObjectCommand(CommandAst commandAst) } var dr = new DiagnosticRecord( - Strings.UseLiteralInitilializerForHashtableDescription, + Strings.UseLiteralInitializerForHashtableDescription, commandAst.Extent, GetName(), GetDiagnosticSeverity(), diff --git a/Rules/UseShouldProcessForStateChangingFunctions.cs b/Rules/UseShouldProcessForStateChangingFunctions.cs index 4448e6693..0d526042f 100644 --- a/Rules/UseShouldProcessForStateChangingFunctions.cs +++ b/Rules/UseShouldProcessForStateChangingFunctions.cs @@ -102,7 +102,7 @@ public string GetCommonName() /// The description of this rule public string GetDescription() { - return string.Format(CultureInfo.CurrentCulture, Strings.UseShouldProcessForStateChangingFunctionsDescrption); + return string.Format(CultureInfo.CurrentCulture, Strings.UseShouldProcessForStateChangingFunctionsDescription); } /// From 474e28bcf9ca93c4480fd3ef54c408e60ec646b3 Mon Sep 17 00:00:00 2001 From: Christoph Bergmeister Date: Wed, 18 Mar 2026 01:29:19 +0000 Subject: [PATCH 112/130] Optimise LINQ queries (#2160) * Initial plan * Optimize LINQ operations for better performance Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Update Rules/AvoidMultipleTypeAttributes.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Rules/AvoidMultipleTypeAttributes.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Engine/Generic/RuleSuppression.cs | 2 +- Engine/ScriptAnalyzer.cs | 8 ++++---- Engine/Settings.cs | 2 +- Rules/AvoidMultipleTypeAttributes.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Engine/Generic/RuleSuppression.cs b/Engine/Generic/RuleSuppression.cs index 279eec58f..d16b356fb 100644 --- a/Engine/Generic/RuleSuppression.cs +++ b/Engine/Generic/RuleSuppression.cs @@ -340,7 +340,7 @@ public static List GetSuppressions(IEnumerable at if (targetAsts != null) { - if (targetAsts.Count() == 0) + if (!targetAsts.Any()) { if (String.IsNullOrWhiteSpace(scopeAst.Extent.File)) { diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index c564dc8fa..46e267fc6 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -267,9 +267,9 @@ internal bool ParseProfile(object profileObject, PathIntrinsics path, IOutputWri return false; } - this.severity = (severityList.Count() == 0) ? null : severityList.ToArray(); - this.includeRule = (includeRuleList.Count() == 0) ? null : includeRuleList.ToArray(); - this.excludeRule = (excludeRuleList.Count() == 0) ? null : excludeRuleList.ToArray(); + this.severity = (severityList.Count == 0) ? null : severityList.ToArray(); + this.includeRule = (includeRuleList.Count == 0) ? null : includeRuleList.ToArray(); + this.excludeRule = (excludeRuleList.Count == 0) ? null : excludeRuleList.ToArray(); if (settings != null && settings.ContainsKey("Rules")) { @@ -609,7 +609,7 @@ private bool ParseProfileString(string profile, PathIntrinsics path, IOutputWrit IEnumerable hashTableAsts = profileAst.FindAll(item => item is HashtableAst, false); // no hashtable, raise warning - if (hashTableAsts.Count() == 0) + if (!hashTableAsts.Any()) { writer.WriteError(new ErrorRecord(new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.InvalidProfile, profile)), Strings.ConfigurationFileHasNoHashTable, ErrorCategory.ResourceUnavailable, profile)); diff --git a/Engine/Settings.cs b/Engine/Settings.cs index 124e5488a..0d37a3a78 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -453,7 +453,7 @@ private void parseSettingsFile(string settingsFilePath) IEnumerable hashTableAsts = profileAst.FindAll(item => item is HashtableAst, false); // no hashtable, raise warning - if (hashTableAsts.Count() == 0) + if (!hashTableAsts.Any()) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.InvalidProfile, settingsFilePath)); } diff --git a/Rules/AvoidMultipleTypeAttributes.cs b/Rules/AvoidMultipleTypeAttributes.cs index 77f63de21..590a058d9 100644 --- a/Rules/AvoidMultipleTypeAttributes.cs +++ b/Rules/AvoidMultipleTypeAttributes.cs @@ -37,7 +37,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // Iterates all ParamAsts and check the number of its types. foreach (ParameterAst paramAst in paramAsts) { - if (paramAst.Attributes.Where(typeAst => typeAst is TypeConstraintAst).Count() > 1) + if (paramAst.Attributes.OfType().Skip(1).Any()) { yield return new DiagnosticRecord( String.Format(CultureInfo.CurrentCulture, Strings.AvoidMultipleTypeAttributesError, paramAst.Name), From 9d8a1fea42b4b8026d11ffe68c34363cf5527479 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 18 Mar 2026 01:31:18 +0000 Subject: [PATCH 113/130] AlignAssignmentStatement overhaul to fix issues and include handing of Enums. (#2132) * Wholesale rewrite of AlignAssignmentStatement to fix issues * Change setting defaults to all enabled * Wholesale rewrite of AlignAssignmentStatement to fix issues * Change setting defaults to all enabled --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Rules/AlignAssignmentStatement.cs | 800 ++++++++++---- .../Rules/AlignAssignmentStatement.tests.ps1 | 974 ++++++++++++++++-- docs/Rules/AlignAssignmentStatement.md | 160 ++- 3 files changed, 1617 insertions(+), 317 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 14cce7d37..5b941924a 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -21,34 +21,45 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif public class AlignAssignmentStatement : ConfigurableRule { - // We keep this switch even though the rule has only one switch (this) as of now, because we want - // to let the rule be expandable in the future to allow formatting assignments even - // in variable assignments. But for now we will stick to only one option. + /// - /// Check if key value pairs in a hashtable are aligned or not. + /// Check the key value pairs of a hashtable, including DSC configurations. /// - /// [ConfigurableRuleProperty(defaultValue: true)] public bool CheckHashtable { get; set; } - private readonly char whitespaceChar = ' '; + /// + /// Whether to include hashtable key-value pairs where there is a comment + /// between the key and the equals sign in alignment. + /// + [ConfigurableRuleProperty(defaultValue: true)] + public bool AlignHashtableKvpWithInterveningComment { get; set; } - private List>> violationFinders - = new List>>(); + /// + /// Check the members of an enum. + /// + [ConfigurableRuleProperty(defaultValue: true)] + public bool CheckEnums { get; set; } /// - /// Sets the configurable properties of this rule. + /// Include enum members without explicit values in the width calculation. /// - /// A dictionary that maps parameter name to it value. Must be non-null - public override void ConfigureRule(IDictionary paramValueMap) - { - base.ConfigureRule(paramValueMap); - if (CheckHashtable) - { - violationFinders.Add(FindHashtableViolations); - } - } + [ConfigurableRuleProperty(defaultValue: true)] + public bool IncludeValuelessEnumMembers { get; set; } + + /// + /// Whether to include enum members where there is a comment + /// between the name and the equals sign in alignment. + /// + [ConfigurableRuleProperty(defaultValue: true)] + public bool AlignEnumMemberWithInterveningComment { get; set; } + /// + /// A mapping of line numbers to the indices of assignment operator + /// tokens on those lines. + /// + private readonly Dictionary> assignmentOperatorIndicesByLine = + new Dictionary>(); /// /// Analyzes the given ast to find if consecutive assignment statements are aligned. @@ -60,277 +71,640 @@ public override IEnumerable AnalyzeScript(Ast ast, string file { if (ast == null) { - throw new ArgumentNullException("ast"); + throw new ArgumentNullException(nameof(ast)); } - // only handles one line assignments - // if the rule encounters assignment statements that are multi-line, the rule will ignore that block - var tokenOps = new TokenOperations(Helper.Instance.Tokens, ast); - foreach (var violationFinder in violationFinders) + // The high-level approach of the rule is to find all of the + // Key-Value pairs in a hashtable, or the members of an enum. + // For all of these assignments, we want to locate where both the + // left-hand-side (LHS) ends and where the equals sign is. + // Looking at all of these assignments for a particular structure, + // we can then decide where the equals sign _should_ be. It should + // be in the column after the longest LHS. + // + // Looking at where it _is_ vs where it _should_ be, we can then + // generate diagnostics and corrections. + + // As an optimisation, we first build a dictionary of all of the + // assignment operators in the script, keyed by line number. We do + // this by doing a single scan of the tokens. This makes it trvially + // fast to find the `Equals` token for a given assignment. + + // Note: In instances where there is a parse error, we do not have + // access to the tokens, so we can't build this dictionary. + // This is relevant for the DSC configuration parsing. + LocateAssignmentOperators(); + + if (CheckHashtable) { - foreach (var diagnosticRecord in violationFinder(tokenOps)) + // Find all hashtables + var hashtableAsts = ast.FindAll( + a => a is HashtableAst, true + ).Cast(); + foreach (var hashtableAst in hashtableAsts) { - yield return diagnosticRecord; + // For each hashtable find all assignment sites that meet + // our criteria for alignment checking + var hashtableAssignmentSites = ParseHashtable(hashtableAst); + + // Check alignment of the assignment sites and emit a + // diagnostic for each misalignment found. + foreach (var diag in CheckAlignment(hashtableAssignmentSites)) + { + yield return diag; + } + } + + // DSC does design time checking of available resource nodes. + // If a resource is not available at design time, the parser + // will error. A DSC Resource definition for a resource which is + // not found will not successfully be parsed and appear in the + // AST as a hashtable. The below is a best-effort attempt to + // find these assignment statements and consistently align them. + + // Find all ConfigurationDefinitionAsts + var dscConfigDefAsts = ast.FindAll( + a => a is ConfigurationDefinitionAst, true + ).Cast(); + foreach (var dscConfigDefAst in dscConfigDefAsts) + { + // Within each ConfigurationDefinitionAst, there can be many + // nested NamedBlocks, each of which can contain many nested + // CommandAsts. The CommandAsts which have 3 command + // elements, with the middle one being an equals sign, are + // the ones we're interested in. `ParseDscConfigDef` will + // emit parsed lists of these CommandAsts that share the + // same parent (and so should be aligned with one another). + foreach (var group in ParseDscConfigDef(dscConfigDefAst, ast)) + { + // Check alignment of the assignment sites and emit a + // diagnostic for each misalignment found. + foreach (var diag in CheckAlignment(group)) + { + yield return diag; + } + } } } - } - /// - /// Retrieves the common name of this rule. - /// - public override string GetCommonName() - { - return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName); - } + if (CheckEnums) + { + // Find all enum TypeDefinitionAsts + var EnumTypeDefAsts = ast.FindAll( + a => a is TypeDefinitionAst t && t.IsEnum, true + ).Cast(); + foreach (var enumTypeDefAst in EnumTypeDefAsts) + { + // For each enum TypeDef find all assignment sites that meet + // our criteria for alignment checking + var enumAssignmentSites = ParseEnums(enumTypeDefAst); - /// - /// Retrieves the description of this rule. - /// - public override string GetDescription() - { - return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription); + // Check alignment of the assignment sites and emit a + // diagnostic for each misalignment found. + foreach (var diag in CheckAlignment(enumAssignmentSites)) + { + yield return diag; + } + } + } } /// - /// Retrieves the name of this rule. + /// Locate all the assignment tokens in the script and store their + /// indices in the assignmentOperatorIndicesByLine dictionary. /// - public override string GetName() + private void LocateAssignmentOperators() { - return string.Format( - CultureInfo.CurrentCulture, - Strings.NameSpaceFormat, - GetSourceName(), - Strings.AlignAssignmentStatementName); - } + // Clear any existing entries + assignmentOperatorIndicesByLine.Clear(); - /// - /// Retrieves the severity of the rule: error, warning or information. - /// - public override RuleSeverity GetSeverity() - { - return RuleSeverity.Warning; + var tokens = Helper.Instance.Tokens; + // Iterate through all tokens, looking for Equals tokens + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i].Kind == TokenKind.Equals) + { + // When an equals token is found, check if the dictionary + // has an entry for this line number, and if not create one. + int lineNumber = tokens[i].Extent.StartLineNumber; + if (!assignmentOperatorIndicesByLine.ContainsKey(lineNumber)) + { + assignmentOperatorIndicesByLine[lineNumber] = new List(); + } + // Add the index of this token to the list for this line + assignmentOperatorIndicesByLine[lineNumber].Add(i); + } + } } /// - /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// Parse a hashtable's key-value pairs into a list of tuples which are + /// later used to verify and correct alignment of assignment operators. /// - /// - public DiagnosticSeverity GetDiagnosticSeverity() + /// The hashtable AST to parse. + /// + /// A list of tuples, where each tuple is a (lhsTokenExtent, equalsExtent) + /// pair representing the extent of the token immediately before the '=' + /// (effectively the key/rightmost key token) and the extent of the '=' itself. + /// Only includes pairs where an '=' token is found on the same line as the key. + /// Implicitly skips line continuations. + /// + private List> ParseHashtable(HashtableAst hashtableAst) { - return DiagnosticSeverity.Warning; - } + var assignmentSites = new List>(); - /// - /// Retrieves the name of the module/assembly the rule is from. - /// - public override string GetSourceName() - { - return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + if (hashtableAst == null) { return assignmentSites; } + + // Enumerate the KeyValuePairs of this hashtable + // Each KVP is a Tuple + foreach (var kvp in hashtableAst.KeyValuePairs) + { + // If the assignmentOperator dictionary has no entry for the + // line that the key ends on, skip this KVP + if (!assignmentOperatorIndicesByLine.ContainsKey(kvp.Item1.Extent.EndLineNumber)) + { + continue; + } + + // Next we need to find the location of the equals sign for this + // Key-Value pair. We know the line it should be on. We can + // search all of the equals signs on that line for the one that + // lives between the end of the key and the start of the value. + + int equalsTokenIndex = -1; + foreach (var index in assignmentOperatorIndicesByLine[kvp.Item1.Extent.EndLineNumber]) + { + if (Helper.Instance.Tokens[index].Extent.StartOffset >= kvp.Item1.Extent.EndOffset && + Helper.Instance.Tokens[index].Extent.EndOffset <= kvp.Item2.Extent.StartOffset + ) + { + equalsTokenIndex = index; + break; + } + } + + // If we didn't find the equals sign - skip this KVP + if (equalsTokenIndex == -1) + { + continue; + } + + // Normally a Key-Value pair looks like: + // + // Key = Value + // + // But the below is also valid: + // + // Key <#Inline Comment#> = Value + // + // We can still use this KVP for alignment - we simply treat + // the end of the token before the equals sign as the Left-Hand + // Side (LHS) of the assignment. We expose a user setting for + // this. + // If the user has not chosen to align such KVPs and the token + // before the equals sign does not end at the same offset as + // the key, we skip this KVP. + if (!AlignHashtableKvpWithInterveningComment && + Helper.Instance.Tokens[equalsTokenIndex - 1].Extent.EndOffset != kvp.Item1.Extent.EndOffset + ) + { + continue; + } + + assignmentSites.Add(new Tuple( + Helper.Instance.Tokens[equalsTokenIndex - 1].Extent, + Helper.Instance.Tokens[equalsTokenIndex].Extent + )); + } + + return assignmentSites; } /// - /// Retrieves the type of the rule, Builtin, Managed or Module. + /// Parse a DSC configuration definition's resource/property blocks into + /// a list of tuples which are later used to verify and correct alignment of + /// assignment operators. /// - public override SourceType GetSourceType() + /// The ConfigurationDefinitionAst to parse. + /// + /// An enumeration of lists of tuples, where each tuple is a (lhsTokenExtent, equalsExtent) + /// pair representing the extent of the token immediately before the '=' + /// (effectively the key/rightmost key token) and the extent of the '=' itself. + /// Only includes pairs where an '=' token is found on the same line as the key. + /// Implicitly skips line continuations. + /// + private IEnumerable>> ParseDscConfigDef( + ConfigurationDefinitionAst configDefAst, + Ast ast + ) { - return SourceType.Builtin; - } - private IEnumerable FindHashtableViolations(TokenOperations tokenOps) - { - var hashtableAsts = tokenOps.Ast.FindAll(ast => ast is HashtableAst, true); - var groups = new List>>(); - if (hashtableAsts != null) + + if (configDefAst == null) { yield break; } + + // Find command asts shaped like: = + var commandAsts = configDefAst.FindAll( + a => + a is CommandAst c && + c.CommandElements.Count == 3 && + c.CommandElements[1].Extent?.Text == "=", + true + ).Cast(); + + // Group by grandparent NamedBlock (commandAst.Parent is PipelineAst) + var grouped = commandAsts.GroupBy( + c => c.Parent?.Parent + ); + + foreach (var group in grouped) { - foreach (var astItem in hashtableAsts) + var assignmentSites = new List>(); + + foreach (var cmd in group) { - groups.Add(GetExtents(tokenOps, (HashtableAst)astItem)); - } - } + var lhs = cmd.CommandElements[0].Extent; + var eq = cmd.CommandElements[1].Extent; - var configAsts = tokenOps.Ast.FindAll(ast => ast is ConfigurationDefinitionAst, true); - if (configAsts != null) - { - // There are probably parse errors caused by an "Undefined DSC resource" - // which prevents the parser from detecting the property value pairs as - // hashtable. Hence, this is a workaround to format configurations which - // have "Undefined DSC resource" parse errors. - - // find all commandAsts of the form "prop" "=" "val" that have the same parent - // and format those pairs. - foreach (var configAst in configAsts) + if (lhs.EndLineNumber != eq.StartLineNumber) + { + // Skip if the key and equals sign are not on the same + // line + continue; + } + + // Note: We can't use the token dictionary here like we do + // for hashtables/enums, as we get here typically + // because there's a parse error. i.e. + // ModuleNotFoundDuringParse and ResourceNotDefined + // Helper.Instance.Tokens is unavailable when there's + // a parse error so we can only use the ast. + + // In lieu of being able to check tokens, we check the + // source text between the end of the lhs and the start of + // the equals sign for non-whitespace characters. + // + // key <#comment#> = value + // ^ ^ + // | | + // ------------- + // | + // We check for non-whitespace characters here + // + // If there are any, we extend the lhs extent to include + // them, so that the alignment is to the end of the + // rightmost non-whitespace characters. + + // We get the text between between lhs and eq, trim it from + // the end (so we keep the right-most non-whitespace + // characters). It's length is how much we need to extend + // the lhs extent by. + var nonWhitespaceLength = + ast.Extent.Text.Substring( + lhs.EndOffset, + eq.StartOffset - lhs.EndOffset + ).TrimEnd().Length; + + // If there's any non-whitespace characters between the + // key and the equals sign, and the user has chosen to + // ignore such cases, skip this KVP. + if (nonWhitespaceLength > 0 && !AlignHashtableKvpWithInterveningComment) + { + continue; + } + + IScriptExtent leftExtent = null; + if (nonWhitespaceLength == 0) + { + // When there is no intervening comment, we use the + // key's extent as the LHS extent. + leftExtent = lhs; + } + else + { + // When there is an intervening comment, we extend + // the key's extent to include it. + leftExtent = new ScriptExtent( + new ScriptPosition( + lhs.File, + lhs.StartLineNumber, + lhs.StartColumnNumber, + null + ), + new ScriptPosition( + lhs.File, + lhs.EndLineNumber, + lhs.EndColumnNumber + nonWhitespaceLength, + null + ) + ); + } + + assignmentSites.Add(new Tuple( + leftExtent, + eq + )); + } + if (assignmentSites.Count > 0) { - groups.AddRange(GetCommandElementExtentGroups(configAst)); + yield return assignmentSites; } } + } - // it is probably much easier have a hashtable writer that formats the hashtable and writes it - // but it makes handling comments hard. So we need to use this approach. - - // This is how the algorithm actually works: - // if each key value pair are on a separate line - // find all the assignment operators - // if all the assignment operators are aligned (check the column number of each assignment operator) - // skip - // else - // find the distance between the assignment operators and their corresponding LHS - // find the longest left expression - // make sure all the assignment operators are in the same column as that of the longest left hand. - foreach (var extentTuples in groups) + /// + /// Parse an enum's members into a list of tuples which are later used to + /// verify and correct alignment of assignment operators. + /// + /// The enum TypeDefinitionAst to parse. + /// + /// A list of tuples, where each tuple is a (lhsTokenExtent, equalsExtent) + /// pair representing the extent of the token immediately before the '=' + /// (effectively the member name) and the extent of the '=' itself. + /// Implicitly skips line continuations. + /// + private List> ParseEnums( + TypeDefinitionAst enumTypeDefAst + ) + { + var assignmentSites = new List>(); + if (enumTypeDefAst == null) { return assignmentSites; } + + // Ensure we're only processing enums + if (!enumTypeDefAst.IsEnum) { return assignmentSites; } + + // Enumerate Enum Members that are PropertyMemberAst + foreach ( + var member in enumTypeDefAst.Members.Where( + m => m is PropertyMemberAst + ).Cast() + ) { - if (!HasPropertiesOnSeparateLines(extentTuples)) + + // Enums can have members with or without explicit values. + + // If InitialValue is null, this member has no explicit + // value and so should have no equals sign. + if (member.InitialValue == null) { + if (!IncludeValuelessEnumMembers) + { + continue; + } + + if (member.Extent.StartLineNumber != member.Extent.EndLineNumber) + { + // This member spans multiple lines - skip it + continue; + } + + // We include this member in the alignment check, but + // with a null equalsExtent. This will be ignored in + // CheckAlignment, but will ensure that this member + // is included in the calculation of the target column. + assignmentSites.Add(new Tuple( + member.Extent, + null + )); continue; } - if (extentTuples == null - || extentTuples.Count == 0 - || !extentTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber)) + // If the assignmentOperator dictionary has no entry for the + // line of the member name - skip this member; it should + // have an explicit value, so must have an equals sign. + // It's possible that the equals sign is on a different + // line thanks to line continuations (`). We skip such + // members. + if (!assignmentOperatorIndicesByLine.ContainsKey(member.Extent.StartLineNumber)) { continue; } - var expectedStartColumnNumber = extentTuples.Max(x => x.Item1.EndColumnNumber) + 1; - foreach (var extentTuple in extentTuples) + // Next we need to find the location of the equals sign for this + // member. We know the line it should be on. We can + // search all of the equals signs on that line. + // + // Unlike hashtables, we don't have an extent for the LHS and + // RHS of the member. We have the extent of the entire + // member, the name of the member, and the extent of the + // InitialValue (RHS). We can use these to find the equals + // sign. We know the equals sign must be after the + // member name, and before the InitialValue. + + int equalsTokenIndex = -1; + foreach (var index in assignmentOperatorIndicesByLine[member.Extent.StartLineNumber]) { - if (extentTuple.Item2.StartColumnNumber != expectedStartColumnNumber) + if (Helper.Instance.Tokens[index].Extent.StartOffset >= (member.Extent.StartColumnNumber + member.Name.Length) && + Helper.Instance.Tokens[index].Extent.EndOffset < member.InitialValue.Extent.StartOffset + ) { - yield return new DiagnosticRecord( - GetError(), - extentTuple.Item2, - GetName(), - GetDiagnosticSeverity(), - extentTuple.Item1.File, - null, - GetHashtableCorrections(extentTuple, expectedStartColumnNumber).ToList()); + equalsTokenIndex = index; + break; } } - } - } - private List>> GetCommandElementExtentGroups(Ast configAst) - { - var result = new List>>(); - var commandAstGroups = GetCommandElementGroups(configAst); - foreach (var commandAstGroup in commandAstGroups) - { - var list = new List>(); - foreach (var commandAst in commandAstGroup) + // If we didn't find the equals sign - skip, it's likely on a + // different line due to line continuations. + if (equalsTokenIndex == -1) { - var elems = commandAst.CommandElements; - list.Add(new Tuple(elems[0].Extent, elems[1].Extent)); + continue; } - result.Add(list); - } + // Normally a member with a value looks like: + // + // Name = Value + // + // But the below is also valid: + // + // Name <#Inline Comment#> = Value + // + // We can still use this member for alignment - we simply treat + // the end of the token before the equals sign as the Left-Hand + // Side (LHS) of the assignment. We expose a user setting for + // this. + // If the user has not chosen to align such members and the + // token before the equals sign is a comment, we skip this + // member. + if (!AlignEnumMemberWithInterveningComment && + Helper.Instance.Tokens[equalsTokenIndex - 1].Kind == TokenKind.Comment + ) + { + continue; + } - return result; + assignmentSites.Add(new Tuple( + Helper.Instance.Tokens[equalsTokenIndex - 1].Extent, + Helper.Instance.Tokens[equalsTokenIndex].Extent + )); + } + return assignmentSites; } - private List> GetCommandElementGroups(Ast configAst) + /// + /// Check alignment of assignment operators in the provided list of + /// (lhsTokenExtent, equalsExtent) tuples, and return diagnostics for + /// any misalignments found. + /// + /// From the lhsTokenExtent, we can determine the target column for + /// alignment (the column after the longest key). We then compare the + /// equalsExtent's start column to the target column, and if they + /// differ, we have a misalignment and return a diagnostic. + /// + /// + /// A list of tuples, where each tuple is a (lhsTokenExtent, equalsExtent) + /// pair representing the extent of the token immediately before the '=' + /// and the extent of the '=' itself. + /// Only includes pairs where an '=' token is found on the same line as + /// the key. + /// + /// + /// An enumerable of DiagnosticRecords, one for each misaligned + /// assignment operator found. + /// + private IEnumerable CheckAlignment( + List> assignmentSites + ) { - var result = new List>(); - var astsFound = configAst.FindAll(ast => IsPropertyValueCommandAst(ast), true); - if (astsFound == null) + if (assignmentSites == null || assignmentSites.Count == 0) { - return result; + yield break; } - var parentChildrenGroup = from ast in astsFound - select (CommandAst)ast into commandAst - group commandAst by commandAst.Parent.Parent; // parent is pipeline and pipeline's parent is namedblockast - foreach (var group in parentChildrenGroup) + // Filter out everything from assignmentSites that is not on + // it's own line. Do this by grouping by the start line number + // of the lhsTokenExtent, and only keeping groups with a count + // of 1. + assignmentSites = assignmentSites + .GroupBy(t => t.Item1.StartLineNumber) + .Where(g => g.Count() == 1) + .Select(g => g.First()) + .ToList(); + + // If, after filtering, we have no assignment sites, exit + if (assignmentSites == null || assignmentSites.Count == 0) { - result.Add(group.ToList()); + yield break; } - return result; + // The target column for this hashtable is longest key plus one + // space. + var targetColumn = assignmentSites + .Max(t => t.Item1.EndColumnNumber) + 1; + + // Check each element of the hashtable to see if it's aligned + foreach (var site in assignmentSites) + { + // If the equalsExtent is null, this is a member without + // an explicit value. We include such members in the + // calculation of the target column, but we don't + // generate diagnostics for them. + if (site.Item2 == null) + { + continue; + } + + // If the equals sign is already at the target column, + // no diagnostic is needed. + if (site.Item2.StartColumnNumber == targetColumn) + { + continue; + } + + yield return new DiagnosticRecord( + string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError), + site.Item2, + GetName(), + DiagnosticSeverity.Warning, + site.Item1.File, + null, + GetCorrectionExtent( + site.Item1, + site.Item2, + targetColumn + ) + ); + } } - private bool IsPropertyValueCommandAst(Ast ast) + /// + /// Generate the correction extent to align the assignment operator + /// to the target column. + /// + /// The extent of the token immediately before the '=' + /// The extent of the '=' token + /// The target column to align to + /// An enumerable of CorrectionExtents, one for each correction + private List GetCorrectionExtent( + IScriptExtent lhsExtent, + IScriptExtent equalsExtent, + int targetColumn + ) { - var commandAst = ast as CommandAst; - return commandAst != null - && commandAst.CommandElements.Count() == 3 - && commandAst.CommandElements[1].Extent.Text.Equals("="); + // We generate a correction extent which replaces the text between + // the end of the lhs and the start of the equals sign with the + // appropriate number of spaces to align the equals sign to the + // target column. + return new List + { + new CorrectionExtent( + lhsExtent.EndLineNumber, + equalsExtent.StartLineNumber, + lhsExtent.EndColumnNumber, + equalsExtent.StartColumnNumber, + new string(' ', targetColumn - lhsExtent.EndColumnNumber), + string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError) + ) + }; } - private IEnumerable GetHashtableCorrections( - Tuple extentTuple, - int expectedStartColumnNumber) + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() { - var equalExtent = extentTuple.Item2; - var lhsExtent = extentTuple.Item1; - var columnDiff = expectedStartColumnNumber - equalExtent.StartColumnNumber; - yield return new CorrectionExtent( - lhsExtent.EndLineNumber, - equalExtent.StartLineNumber, - lhsExtent.EndColumnNumber, - equalExtent.StartColumnNumber, - new String(whitespaceChar, expectedStartColumnNumber - lhsExtent.EndColumnNumber), - GetError()); + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName); } - private string GetError() + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() { - return String.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError); + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription); } - private static List> GetExtents( - TokenOperations tokenOps, - HashtableAst hashtableAst) + /// + /// Retrieves the name of this rule. + /// + public override string GetName() { - var nodeTuples = new List>(); - foreach (var kvp in hashtableAst.KeyValuePairs) - { - var keyStartOffset = kvp.Item1.Extent.StartOffset; - bool keyStartOffSetReached = false; - var keyTokenNode = tokenOps.GetTokenNodes( - token => - { - if (keyStartOffSetReached) - { - return token.Kind == TokenKind.Equals; - } - if (token.Extent.StartOffset == keyStartOffset) - { - keyStartOffSetReached = true; - } - return false; - }).FirstOrDefault(); - if (keyTokenNode == null || keyTokenNode.Value == null) - { - continue; - } - var assignmentToken = keyTokenNode.Value.Extent; - - nodeTuples.Add(new Tuple( - kvp.Item1.Extent, assignmentToken)); - } + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AlignAssignmentStatementName); + } - return nodeTuples; + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; } - private bool HasPropertiesOnSeparateLines(IEnumerable> tuples) + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() { - if (tuples.Count() == 1) - { - // If the hashtable has just a single key-value pair, it does not have properties on separate lines - return false; - } - var lines = new HashSet(); - foreach (var kvp in tuples) - { - if (lines.Contains(kvp.Item1.StartLineNumber)) - { - return false; - } - else - { - lines.Add(kvp.Item1.StartLineNumber); - } - } + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } - return true; + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; } } } diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index f252cccb5..1262acdf8 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -2,162 +2,956 @@ # Licensed under the MIT License. BeforeAll { - $testRootDirectory = Split-Path -Parent $PSScriptRoot - Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") + function New-AlignAssignmentSettings { + [OutputType([hashtable])] + [CmdletBinding()] + param( + [Parameter()] + [bool] + $CheckHashtable = $false, + [Parameter()] + [bool] + $AlignHashtableKvpWithInterveningComment = $false, + [Parameter()] + [bool] + $CheckEnums = $false, + [Parameter()] + [bool] + $IncludeValuelessEnumMembers = $false, + [Parameter()] + [bool] + $AlignEnumMemberWithInterveningComment = $false + ) + return @{ + IncludeRules = @('PSAlignAssignmentStatement') + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $CheckHashtable + AlignHashtableKvpWithInterveningComment = $AlignHashtableKvpWithInterveningComment + CheckEnums = $CheckEnums + IncludeValuelessEnumMembers = $IncludeValuelessEnumMembers + AlignEnumMemberWithInterveningComment = $AlignEnumMemberWithInterveningComment + } + } + } + } - $ruleConfiguration = @{ - Enable = $true - CheckHashtable = $true + function Get-NonParseDiagnostics { + [OutputType([object[]])] + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [object[]] + $Diagnostics + ) + process { + $Diagnostics | Where-Object { + $_.RuleName -eq 'PSAlignAssignmentStatement' + } + } } - $settings = @{ - IncludeRules = @("PSAlignAssignmentStatement") - Rules = @{ - PSAlignAssignmentStatement = $ruleConfiguration + function Apply-Corrections { + [OutputType([string])] + [CmdletBinding()] + param( + [string] + $Original, + [object[]] + $Diagnostics + ) + # Note: This only works to apply the correction extents because all of + # our corrections are simple, single line operations. + $lines = $Original -split "`n" + foreach ($Diagnostic in $Diagnostics) { + if (-not $Diagnostic.SuggestedCorrections) { + continue + } + foreach ($extent in $Diagnostic.SuggestedCorrections) { + $lineIndex = $extent.StartLineNumber - 1 + $prefix = $lines[$lineIndex].Substring( + 0, $extent.StartColumnNumber - 1 + ) + $suffix = $lines[$lineIndex].Substring( + $extent.EndColumnNumber - 1 + ) + $lines[$lineIndex] = $prefix + $extent.Text + $suffix + + } } + return ($lines -join "`n") + } +} + +Describe 'AlignAssignmentStatement' { + + Context 'When checking Hashtables is disabled' { + + It 'Should not find violations in mis-aligned hashtables' { + $def = @' +@{ + 'Key' = 'Value' + 'LongerKey' = 'Value' +} +'@ + $settings = New-AlignAssignmentSettings + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + + } + + It 'Should not find violations in DSC configuration blocks' { + $def = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name = '"RSAT"' + } + } +} +'@ + $settings = New-AlignAssignmentSettings + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + + } -Skip:($IsLinux -or $IsMacOS) + } + + Context 'When Hashtable checking is enabled' { + + It 'Should not find violations in empty single-line hashtable' { + $def = '@{}' + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should not find violations in empty multi-line hashtable' { + $def = @' +@{ + } +'@ + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should not find violation in aligned, single-line, single-kvp hashtable' { + $def = '@{"Key" = "Value"}' + + $settings = New-AlignAssignmentSettings -CheckHashtable $true -Describe "AlignAssignmentStatement" { - Context "When assignment statements are in hashtable" { - It "Should find violation when assignment statements are not aligned (whitespace needs to be added)" { + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + It 'Should find violation in mis-aligned, single-line, single-kvp hashtable' { + $def = '@{"Key" = "Value"}' + $expected = '@{"Key" = "Value"}' + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + + $corrected | Should -BeExactly $expected + } + + It 'Should not find violations in mis-aligned hashtable with multiple kvp on a single line' { + $def = '@{"Key1" = "Value1";"Key2"="Value2"}' + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + It 'Should not find violations in well aligned, multi-line, multi-kvp hashtable' { $def = @' -$hashtable = @{ - property1 = "value" - anotherProperty = "another value" +@{ + 'Key1' = 'Value1' + 'Key2' = 'Value2' + 'Key3' = 'Value3' } '@ - # Expected output after correction should be the following - # $hashtable = @{ - # property1 = "value" - # anotherProperty = "another value" - # } + $settings = New-AlignAssignmentSettings -CheckHashtable $true - $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings - $violations.Count | Should -Be 1 - Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty } - It "Should find violation when assignment statements are not aligned (whitespace needs to be removed)" { + It 'Should find violations in mis-aligned, multi-line, multi-kvp hashtable' { $def = @' -$hashtable = @{ - property1 = "value" - anotherProperty = "another value" +@{ + 'Key1'= 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' +} +'@ + + $expected = @' +@{ + 'Key1' = 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' } '@ - # Expected output should be the following - # $hashtable = @{ - # property1 = "value" - # anotherProperty = "another value" - # } + $settings = New-AlignAssignmentSettings -CheckHashtable $true - $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings - $violations.Count | Should -Be 1 - Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 2 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected } - It "Should not crash if property name reaches further to the right than the longest property name (regression test for issue 1067)" { + It 'Should ignore lines with intervening comments when AlignHashtableKvpWithInterveningComment is false' { $def = @' -$hashtable = @{ property1 = "value" - anotherProperty = "another value" +@{ + 'Key1' <#comment#>= 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' +} +'@ + + $expected = @' +@{ + 'Key1' <#comment#>= 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' } '@ - $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings -ErrorAction Stop | Get-Count | Should -Be 0 + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected } - It "Should ignore if a hashtable is empty" { + It 'Should align lines with intervening comments when AlignHashtableKvpWithInterveningComment is true' { $def = @' -$x = @{ } +@{ + 'Key1' <#comment#>= 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' +} +'@ + + $expected = @' +@{ + 'Key1' <#comment#> = 'Value1' + 'Key12' = 'Value2' + 'Key123' = 'Value3' +} '@ - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 3 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected } - It "Should ignore if a hashtable has a single key-value pair on a single line" { + It 'Should not find violations when intervening comment is already aligned and AlignHashtableKvpWithInterveningComment is true' { $def = @' -$x = @{ 'key'="value" } +@{ + 'Key1' <#comment#> = 'Value1' + 'Key2' = 'Value2' + 'Key3' = 'Value3' +} '@ - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty } - It "Should ignore if a hashtable has a single key-value pair across multiple lines" { + It 'Should not find violations when intervening comment is on right of equals sign and AlignHashtableKvpWithInterveningComment is true' { $def = @' -$x = @{ - 'key'="value" +@{ + 'Key1' = <#comment#> 'Value1' + 'Key2' = 'Value2' + 'Key3' = 'Value3' } '@ - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty } - It "Should ignore if a hashtable has multiple key-value pairs on a single line" { + It 'Should ignore kvp with a line continuation between key and equals sign' { $def = @' -$x = @{ 'key'="value"; 'key2'="value2"; 'key3WithLongerName'="value3" } +@{ + 'LongerKey' ` + = <#comment#> 'Value1' + 'Key1' = 'Value2' + 'Key12' = 'Value3' +} +'@ + + $expected = @' +@{ + 'LongerKey' ` + = <#comment#> 'Value1' + 'Key1' = 'Value2' + 'Key12' = 'Value3' +} '@ - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 0 + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected } + It 'Should correctly align kvp when key is a string containing an equals sign' { + $def = @' +@{ + 'key1=5' = 'Value1' + 'Key1' = 'Value2' + 'Key12' = 'Value3' +} +'@ + + $expected = @' +@{ + 'key1=5' = 'Value1' + 'Key1' = 'Value2' + 'Key12' = 'Value3' +} +'@ + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 3 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should correctly align kvp when key is an expression containing an assignment' { + # Note: `($key='key1')` defines the variable `$key` and sets it's + # value to 'key1'. The entire expression evaluates to 'key1' + # which is then used as the hashtable key. So the first key + # at runtime is equal to the string 'key1'. + $def = @' +@{ + ($key='key1') = 'Value1' + 'Key2' = 'Value2' + 'Key3' = 'Value3' +} +'@ + + $expected = @' +@{ + ($key='key1') = 'Value1' + 'Key2' = 'Value2' + 'Key3' = 'Value3' +} +'@ + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 3 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should correctly align hashtables independantly when nested' { + $def = @' +@{ + 'key1' = 5 + 'key12' = @{ + 'nestedKey1' = 'Value1' + 'nestedKey12'= 'Value2' + 'nestedKey123'= @{ + 'superNestedKey1' = 'Value1' + 'superNestedKey12'='Value2' + } + } + 'key123' = 'Value3' +} +'@ + + $expected = @' +@{ + 'key1' = 5 + 'key12' = @{ + 'nestedKey1' = 'Value1' + 'nestedKey12' = 'Value2' + 'nestedKey123' = @{ + 'superNestedKey1' = 'Value1' + 'superNestedKey12' ='Value2' + } } + 'key123' = 'Value3' +} +'@ - Context "When assignment statements are in DSC Configuration" { - It "Should find violations when assignment statements are not aligned" -skip:($IsLinux -or $IsMacOS) { + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 8 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should not find violations in aligned DSC configuration blocks' { $def = @' -Configuration MyDscConfiguration { +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name = '"RSAT"' + } + } +} +'@ + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty - param( - [string[]]$ComputerName="localhost" - ) - Node $ComputerName { - WindowsFeature MyFeatureInstance { - Ensure = "Present" - Name = "RSAT" + } -Skip:($IsLinux -or $IsMacOS) + + It 'Should find violations in mis-aligned DSC configuration blocks' { + $def = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name = '"RSAT"' } - WindowsFeature My2ndFeatureInstance { - Ensure = "Present" - Name = "Bitlocker" + } +} +'@ + + $expected = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name = '"RSAT"' } } } '@ - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should -Be 2 + + $settings = New-AlignAssignmentSettings -CheckHashtable $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + + } -Skip:($IsLinux -or $IsMacOS) + + It 'Should ignore lines in DSC configuration blocks with intervening comments when AlignHashtableKvpWithInterveningComment is false' { + $def = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name <#asdasd#>= '"RSAT"' + Other = 'Value' } } +} +'@ + + $expected = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name <#asdasd#>= '"RSAT"' + Other = 'Value' + } + } +} +'@ - Context "When assignment statements are in DSC Configuration that has parse errors" { - It "Should find violations when assignment statements are not aligned" -skip:($IsLinux -or $IsMacOS) { + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $false + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } -Skip:($IsLinux -or $IsMacOS) + + It 'Should align lines in DSC configuration blocks with intervening comments when AlignHashtableKvpWithInterveningComment is true' { $def = @' -Configuration Sample_ChangeDescriptionAndPermissions -{ - Import-DscResource -Module NonExistentModule - # A Configuration block can have zero or more Node blocks - Node $NodeName - { - # Next, specify one or more resource blocks +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name <#asdasd#>= '"RSAT"' + Other = 'Value' + } + } +} +'@ + + $expected = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name <#asdasd#> = '"RSAT"' + Other = 'Value' + } + } +} +'@ + + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 3 - NonExistentModule MySMBShare - { - Ensure = "Present" - Name = "MyShare" - Path = "C:\Demo\Temp" - ReadAccess = "author" - FullAccess = "some other author" - Description = "This is an updated description for this share" + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } -Skip:($IsLinux -or $IsMacOS) + + It 'Should ignore lines with a line continuation in DSC configuration blocks' { + $def = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name ` + = '"RSAT"' + Other = 'Value' } } } '@ - # This invocation will throw parse error caused by "Undefined DSC resource" because - # NonExistentModule is not really avaiable to load. Therefore we set erroraction to - # SilentlyContinue - Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings -ErrorAction SilentlyContinue | - Where-Object { $_.Severity -ne "ParseError" } | - Get-Count | - Should -Be 4 + + $expected = @' +Configuration C1 { + Node localhost { + NonExistentResource X { + Ensure = '"Present"' + Name ` + = '"RSAT"' + Other = 'Value' } } } +'@ + + $settings = New-AlignAssignmentSettings -CheckHashtable $true -AlignHashtableKvpWithInterveningComment $false + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } -Skip:($IsLinux -or $IsMacOS) + + } + + Context 'When Enum checking is disabled' { + + It 'Should not find violations in mis-aligned enums' { + $def = @' +enum E1 { + Short = 1 + Longer = 2 + Longest = 3 +} +'@ + $settings = New-AlignAssignmentSettings + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + + } + + } + + Context 'When Enum checking is enabled' { + + It 'Should not find violations in empty single-line enum' { + $def = 'enum E1 {}' + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should not find violations in empty multi-line enum' { + $def = @' +enum E1 { + +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should not find violations in single-member, valueless, single-line enum' { + $def = 'enum E1 { Member }' + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should not find violations in aligned single-member, explicitly valued, single-line enum' { + $def = 'enum E1 { Member = 1 }' + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics | + Should -BeNullOrEmpty + } + + It 'Should find violations in mis-aligned single-member, explicitly valued, single-line enum' { + $def = 'enum E1 { Member = 1 }' + + $expected = 'enum E1 { Member = 1 }' + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should find violations in mis-aligned single-member, explicitly valued, multi-line enum' { + $def = @' +enum E1 { + Member = 1 +} +'@ + + $expected = @' +enum E1 { + Member = 1 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should not find violations in aligned, multi-member enum' { + $def = @' +enum E1 { + Member1 = 1 + Member2 = 2 + Member3 = 3 + Member4 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + It 'Should find violations in mis-aligned, multi-member enum' { + $def = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 = 3 + Member1234 = 4 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 = 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 3 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should find violations in mis-aligned, multi-member, mixed-valued enum' { + $def = @' +enum E1 { + Member1 = 1 + Member12 + Member123 = 3 + Member1234 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 + Member123 = 3 + Member1234 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 1 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should ignore lines with intervening comments when AlignEnumMemberWithInterveningComment is false' { + $def = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 <#Comment#>= 3 + Member1234 = 4 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 <#Comment#>= 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -AlignEnumMemberWithInterveningComment $false + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 2 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should align lines with intervening comments when AlignHashtableKvpWithInterveningComment is true' { + $def = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 <#Comment#>= 3 + Member1234 = 4 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 <#Comment#> = 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -AlignEnumMemberWithInterveningComment $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 4 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should not find violations when intervening comment is already aligned and AlignEnumMemberWithInterveningComment is true' { + $def = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 <#Comment#> = 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -AlignEnumMemberWithInterveningComment $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + It 'Should not find violations when intervening comment is on right of equals sign and AlignEnumMemberWithInterveningComment is true' { + $def = @' +enum E1 { + Member1 = 1 + Member12 = 2 + Member123 = <#Comment#> 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -AlignEnumMemberWithInterveningComment $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + It 'Should ignore member with a line continuation between name and equals sign' { + $def = @' +enum E1 { + Member1 = 1 + Member12 ` + = 2 + Member123 = 3 + Member1234 = 4 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 ` + = 2 + Member123 = 3 + Member1234 = 4 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 2 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should use valueless members for alignment when IncludeValuelessEnumMembers is true' { + $def = @' +enum E1 { + Member1 = 1 + Member12 + Member123 = 3 + Member1234 +} +'@ + + $expected = @' +enum E1 { + Member1 = 1 + Member12 + Member123 = 3 + Member1234 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -IncludeValuelessEnumMembers $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -HaveCount 2 + + $corrected = Apply-Corrections -Original $def -Diagnostics $violations + $corrected | Should -BeExactly $expected + } + + It 'Should not find violations where all members are valueless and IncludeValuelessEnumMembers is true' { + $def = @' +enum E1 { + Member1 + Member12 + Member123 + Member1234 +} +'@ + + $settings = New-AlignAssignmentSettings -CheckEnums $true -IncludeValuelessEnumMembers $true + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | + Get-NonParseDiagnostics + $violations | Should -BeNullOrEmpty + } + + } + +} \ No newline at end of file diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index c2709ac1a..28ca9f47f 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -10,38 +10,55 @@ title: AlignAssignmentStatement ## Description -Consecutive assignment statements are more readable if they are aligned. By aligned, we imply that -the `equal` sign for all the assignment statements should be in the same column. +Consecutive assignment statements are more readable when they're aligned. +Assignments are considered aligned when their `equals` signs line up vertically. -The rule looks for key (property) value pairs in a hashtable (DSC configuration) to check if they -are aligned or not. Consider the following example in which the key value pairs are not aligned. +This rule looks at the key-value pairs in hashtables (including DSC +configurations) as well as enum definitions. + +Consider the following example which has a hashtable and enum which are not +aligned. ```powershell $hashtable = @{ - property1 = 'value' + property = 'value' anotherProperty = 'another value' } + +enum Enum { + member = 1 + anotherMember = 2 +} ``` Alignment in this case would look like the following. ```powershell $hashtable = @{ - property1 = 'value' + property = 'value' anotherProperty = 'another value' } + +enum Enum { + member = 1 + anotherMember = 2 +} ``` -The rule ignores hashtables in which the assignment statements are on the same line. For example, -the rule ignores `$h = {a = 1; b = 2}`. +The rule ignores any assignments within hashtables and enums which are on the +same line as others. For example, the rule ignores `$h = @{a = 1; b = 2}`. ## Configuration ```powershell Rules = @{ PSAlignAssignmentStatement = @{ - Enable = $true - CheckHashtable = $true + Enable = $true + CheckHashtable = $true + AlignHashtableKvpWithInterveningComment = $true + CheckEnum = $true + AlignEnumMemberWithInterveningComment = $true + IncludeValuelessEnumMembers = $true } } ``` @@ -52,8 +69,123 @@ Rules = @{ Enable or disable the rule during ScriptAnalyzer invocation. -#### CheckHashtable: bool (Default value is `$false`) +#### CheckHashtable: bool (Default value is `$true`) + +Enforce alignment of assignment statements in a hashtable and in a DSC +Configuration. There is only one setting for hashtable and DSC configuration +because the property value pairs in a DSC configuration are parsed as key-value +pairs of a hashtable. + +#### AlignHashtableKvpWithInterveningComment: bool (Default value is `$true`) + +Include key-value pairs in the alignment that have an intervening comment - that +is to say a comment between the key name and the equals sign. + +Consider the following: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +With this setting disabled, the line with the comment is ignored, and it would +be aligned like so: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +With it enabled, the comment line is included in alignment: + +```powershell +$hashtable = @{ + property = 'value' + anotherProperty <#A Comment#> = 'another value' + anotherDifferentProperty = 'yet another value' +} +``` + +#### CheckEnum: bool (Default value is `$true`) + +Enforce alignment of assignment statements of an Enum definition. + +#### AlignEnumMemberWithInterveningComment: bool (Default value is `$true`) + +Include enum members in the alignment that have an intervening comment - that +is to say a comment between the member name and the equals sign. -Enforce alignment of assignment statements in a hashtable and in a DSC Configuration. There is only -one switch for hasthable and DSC configuration because the property value pairs in a DSC -configuration are parsed as key-value pairs of a hashtable. +Consider the following: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +With this setting disabled, the line with the comment is ignored, and it would +be aligned like so: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +With it enabled, the comment line is included in alignment: + +```powershell +enum Enum { + member = 1 + anotherMember <#A Comment#> = 2 + anotherDifferentMember = 3 +} +``` + +#### IncludeValuelessEnumMembers: bool (Default value is `$true`) + +Include enum members in the alignment that don't have an initial value - that +is to say they don't have an equals sign. Enum's don't need to be given a value +when they're defined. + +Consider the following: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` + +With this setting disabled the third line which has no value is not considered +when choosing where to align assignments. It would be aligned like so: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` + +With it enabled, the valueless member is included in alignment as if it had a +value: + +```powershell +enum Enum { + member = 1 + anotherMember = 2 + anotherDifferentMember +} +``` \ No newline at end of file From f3e91348de260141dd927dc37a836ab6ef6482e3 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:04:09 -0700 Subject: [PATCH 114/130] Dynamically count expected rules in GetScriptAnalyzerRule test (#2167) Replace hardcoded rule count with dynamic counting of [Export(typeof(I...Rule))] attributes in C# source files. This prevents the test from breaking every time a new rule is added. --- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 8e433d071..422b585bf 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -63,8 +63,13 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 72 - $defaultRules.Count | Should -Be $expectedNumRules + # Dynamically count the expected number of rules from source files + # by finding all C# files with [Export(typeof(I...Rule))] attributes + $rulesRoot = Resolve-Path "$PSScriptRoot/../../Rules" + $expectedNumRules = (Get-ChildItem -Path $rulesRoot -Filter '*.cs' -Recurse | + Select-String -Pattern 'Export\(typeof\s*\(I\w+Rule\)\)' | + Select-Object -ExpandProperty Path -Unique).Count + $defaultRules.Count | Should -Be $expectedNumRules } It "is a positional parameter" { From ed50b9d8b5443e365de7543395f3034192686fbf Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 18 Mar 2026 19:35:06 +0000 Subject: [PATCH 115/130] Add UseConsistentParameterSetName Rule (#2124) * Add UseConsistentParameterSetName Rule * Update GetScriptAnalyzerRule.tests.ps1 to fix test about number of rules * Make rule optional, disabled by default * Update tests to reflect need to enable rule * Update docs to refelct that the rule is not enabeld by default * Fix alignment of README markdown table * Update Rules/UseConsistentParameterSetName.cs Co-authored-by: Christoph Bergmeister * Update expected rule count to 73 for new UseConsistentParameterSetName rule --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Rules/Strings.resx | 27 ++ Rules/UseConsistentParameterSetName.cs | 453 ++++++++++++++++++ .../UseConsistentParameterSetName.tests.ps1 | 399 +++++++++++++++ docs/Rules/README.md | 1 + docs/Rules/UseConsistentParameterSetName.md | 135 ++++++ 5 files changed, 1015 insertions(+) create mode 100644 Rules/UseConsistentParameterSetName.cs create mode 100644 Tests/Rules/UseConsistentParameterSetName.tests.ps1 create mode 100644 docs/Rules/UseConsistentParameterSetName.md diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 67f925ede..482cec69f 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1224,6 +1224,33 @@ AvoidUsingAllowUnencryptedAuthentication + + Use Consistent Parameter Set Name + + + Parameter set names are case-sensitive in PowerShell. This rule checks for case mismatches between DefaultParameterSetName and ParameterSetName values, case mismatches between different ParameterSetName values, and missing DefaultParameterSetName when parameter sets are used. + + + Param block uses parameter sets but does not specify a DefaultParameterSetName. Consider adding DefaultParameterSetName to the CmdletBinding attribute. + + + DefaultParameterSetName '{0}' does not match the case of ParameterSetName '{1}'. Parameter set names are case-sensitive. + + + ParameterSetName '{0}' does not match the case of '{1}'. Parameter set names are case-sensitive and should use consistent casing. + + + Parameter '{0}' is declared in parameter-set '{1}' multiple times. + + + Parameter set names should not contain new lines. + + + Rename ParameterSet '{0}' to '{1}'. + + + UseConsistentParameterSetName + Avoid reserved words as function names diff --git a/Rules/UseConsistentParameterSetName.cs b/Rules/UseConsistentParameterSetName.cs new file mode 100644 index 000000000..bf12f37e2 --- /dev/null +++ b/Rules/UseConsistentParameterSetName.cs @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Linq; +using System.Management.Automation; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// UseConsistentParameterSetName: Check for case-sensitive parameter set + /// name mismatches, missing default parameter set names, and parameter set + /// names containing new lines. + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class UseConsistentParameterSetName : ConfigurableRule + { + + private const string AllParameterSetsName = "__AllParameterSets"; + + /// + /// AnalyzeScript: Check for parameter set name issues. + /// + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException(Strings.NullAstErrorMessage); + } + + var allParameterBlocks = ast + .FindAll(testAst => testAst is ParamBlockAst, true) + .Cast() + .Where(pb => pb.Parameters?.Count > 0); + + foreach (var paramBlock in allParameterBlocks) + { + // If the paramblock has no parameters, skip it + if (paramBlock.Parameters.Count == 0) + { + continue; + } + + // Get the CmdletBinding attribute and default parameter set name + // Or null if not present + var cmdletBindingAttr = Helper.Instance.GetCmdletBindingAttributeAst(paramBlock.Attributes); + var defaultParamSetName = GetNamedArgumentValue(cmdletBindingAttr, "DefaultParameterSetName"); + + // For each parameter block, build up a list of all the parameters + // and the parameter sets in which they appear. + List paramBlockInfo = new List(); + + foreach (var parameter in paramBlock.Parameters) + { + // If the parameter has no attributes, it is part of all + // parameter sets. We can ignore it for these checks. + if (parameter.Attributes.Count == 0) + { + continue; + } + + // For each parameter attribute a parameter has, extract + // the parameter set and add it to our knowledge of the + // param block. + foreach (var attribute in parameter.Attributes.Where(attr => attr is AttributeAst).Cast()) + { + if (string.Equals(attribute.TypeName?.Name, "Parameter", StringComparison.OrdinalIgnoreCase)) + { + var parameterSetName = GetNamedArgumentValue(attribute, "ParameterSetName", AllParameterSetsName); + paramBlockInfo.Add(new ParameterSetInfo(parameter.Name.VariablePath.UserPath, parameterSetName, attribute)); + } + } + } + + // We now have a picture of the parameters and parameterset + // usage of this paramblock. We can make each check. + + // Check 1: Default parameter set name + // ------------------------------------------------------------- + // If we have parameter sets in use and the CmdletBinding + // attribute, but no default specified, warn about this. + if (string.IsNullOrEmpty(defaultParamSetName) && + cmdletBindingAttr != null && + paramBlockInfo.Any(p => p.ParameterSetName != AllParameterSetsName) + ) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameMissingDefaultError), + cmdletBindingAttr?.Extent ?? paramBlock.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + + // Check 2: Parameter Declared Multiple Times in Same Set + // ------------------------------------------------------------- + // If any parameter has more than one parameter attribute for + // the same parameterset, warn about each instance. + // Parameters cannot be declared multiple times in the same set. + // Calling a function that has a parameter declared multiple + // times in the same parameterset is a runtime exception - + // specifically a [System.Management.Automation.MetadataException] + // It'd be better to know before runtime. + // We use the same message text as the MetadataException for + // consistency + var duplicateAttributes = paramBlockInfo + .GroupBy(p => new { p.ParameterName, p.ParameterSetName }) + .Where(g => g.Count() > 1) + .SelectMany(g => g); + + foreach (var duplicate in duplicateAttributes) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameMultipleDeclarationsError, + duplicate.ParameterName, + duplicate.ParameterSetName), + duplicate.ParameterAttributeAst.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + + // Check 3: Validate Default Parameter Set + // ------------------------------------------------------------- + // If a default parameter set is specified and matches one of + // the used parameter set names ignoring case, but not otherwise + // then we should warn about this + if (!string.IsNullOrEmpty(defaultParamSetName)) + { + // Look for an exact (case-sensitive) match + var exactMatch = paramBlockInfo + .FirstOrDefault(p => + string.Equals( + p.ParameterSetName, + defaultParamSetName, + StringComparison.Ordinal + ) + ); + + if (exactMatch == null) + { + // No exact match, look for a case-insensitive match + var caseInsensitiveMatch = paramBlockInfo + .FirstOrDefault(p => + string.Equals( + p.ParameterSetName, + defaultParamSetName, + StringComparison.OrdinalIgnoreCase + ) + ); + + if (caseInsensitiveMatch != null) + { + var defaultParameterSetNameExtents = GetDefaultParameterSetNameValueExtent(cmdletBindingAttr); + + // Emit a diagnostic for the first case-insensitive match + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameCaseMismatchDefaultError, + defaultParamSetName, + caseInsensitiveMatch.ParameterSetName), + defaultParameterSetNameExtents ?? cmdletBindingAttr?.Extent ?? paramBlock.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + } + } + + // Check 4: Parameter Set Name Consistency + // ------------------------------------------------------------- + // If a parameter set name is used in multiple places, it must + // be consistently used across all usages. This means the casing + // must match exactly. We should warn about any inconsistencies + // found. + var paramSetGroups = paramBlockInfo + .GroupBy(p => p.ParameterSetName, StringComparer.OrdinalIgnoreCase) + .Where(g => + g.Select(p => p.ParameterSetName) + .Distinct(StringComparer.Ordinal) + .Skip(1).Any() + ); + + foreach (var group in paramSetGroups) + { + // Take the first instance as the canonical casing + var canonical = group.First(); + foreach (var entry in group.Skip(1)) + { + if (!string.Equals( + entry.ParameterSetName, + canonical.ParameterSetName, + StringComparison.Ordinal + ) + ) + { + var parameterSetNameExtents = GetParameterSetNameValueExtent(entry.ParameterAttributeAst); + + if (parameterSetNameExtents != null) + { + var correction = new CorrectionExtent( + parameterSetNameExtents.StartLineNumber, + parameterSetNameExtents.EndLineNumber, + parameterSetNameExtents.StartColumnNumber, + parameterSetNameExtents.EndColumnNumber, + $"'{canonical.ParameterSetName}'", + fileName, + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameCaseMismatchSuggestedCorrectionDescription, + entry.ParameterSetName, + canonical.ParameterSetName + ) + ); + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameCaseMismatchParameterError, + entry.ParameterSetName, + canonical.ParameterSetName), + parameterSetNameExtents, + GetName(), + DiagnosticSeverity.Warning, + fileName, + null, + new List { correction }); + } + else + { + // If we couldn't find the parameter set name extents, we can't create a correction + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameCaseMismatchParameterError, + entry.ParameterSetName, + canonical.ParameterSetName), + entry.ParameterAttributeAst.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + } + } + } + + // Check 5: Parameter Set Names should not contain New Lines + // ------------------------------------------------------------- + // There is no practical purpose for parameterset names to + // contain a newline + foreach (var entry in paramBlockInfo) + { + if (entry.ParameterSetName.Contains('\n') || entry.ParameterSetName.Contains('\r')) + { + var parameterSetNameExtents = GetParameterSetNameValueExtent(entry.ParameterAttributeAst); + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameNewLineError), + parameterSetNameExtents ?? entry.ParameterAttributeAst.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + } + if (defaultParamSetName != null && + (defaultParamSetName.Contains('\n') || defaultParamSetName.Contains('\r'))) + { + // If the default parameter set name contains new lines, warn about it + var defaultParameterSetNameExtents = GetDefaultParameterSetNameValueExtent(cmdletBindingAttr); + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameNewLineError, + defaultParamSetName), + defaultParameterSetNameExtents ?? cmdletBindingAttr?.Extent ?? paramBlock.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName); + } + + } + } + + /// + /// Retrieves the value of a named argument from an AttributeAst's NamedArguments collection. + /// If the named argument is not found, returns the provided default value. + /// If the argument value is a constant, returns its string representation; otherwise, returns the argument's text. + /// + /// The AttributeAst to search for the named argument. + /// The name of the argument to look for (case-insensitive). + /// The value to return if the named argument is not found. Defaults to null. + /// + /// The value of the named argument as a string if found; otherwise, the default value. + /// + private static string GetNamedArgumentValue(AttributeAst attributeAst, string argumentName, string defaultValue = null) + { + if (attributeAst == null || attributeAst.NamedArguments == null) + { + return defaultValue; + } + + foreach (var namedArg in attributeAst.NamedArguments) + { + if (namedArg?.ArgumentName == null) continue; + + if (string.Equals(namedArg.ArgumentName, argumentName, StringComparison.OrdinalIgnoreCase)) + { + // Try to evaluate the argument value as a constant string + if (namedArg.Argument is ConstantExpressionAst constAst) + { + return constAst.Value?.ToString(); + } + // If not a constant, try to get the string representation + return namedArg.Argument.Extent.Text; + } + } + return defaultValue; + } + + /// + /// Finds the IScriptExtent of the value assigned to the ParameterSetName argument + /// in the given AttributeAst (if it is a [Parameter()] attribute). + /// Returns null if not found. + /// + /// The AttributeAst to search. + /// The IScriptExtent of the ParameterSetName value, or null if not found. + private static IScriptExtent GetParameterSetNameValueExtent(AttributeAst attributeAst) + { + return GetAttributeNamedArgumentValueExtent(attributeAst, "ParameterSetName", "Parameter"); + } + + /// + /// Finds the IScriptExtent of the value assigned to the DefaultParameterSetName argument + /// in the given AttributeAst (if it is a [CmdletBinding()] attribute). + /// Returns null if not found. + /// + /// The AttributeAst to search. + /// The IScriptExtent of the DefaultParameterSetName value, or null if not found. + private static IScriptExtent GetDefaultParameterSetNameValueExtent(AttributeAst attributeAst) + { + return GetAttributeNamedArgumentValueExtent(attributeAst, "DefaultParameterSetName", "CmdletBinding"); + } + + /// + /// Finds the IScriptExtent of the value of a named argument in the given AttributeAst. + /// Returns null if not found. + /// + /// The AttributeAst to search. + /// The name of the argument to find. + /// The expected type name of the attribute. i.e. Parameter (optional). + /// The IScriptExtent of the named argument value, or null if not found. + private static IScriptExtent GetAttributeNamedArgumentValueExtent(AttributeAst attributeAst, string argumentName, string expectedAttributeName = null) + { + if (attributeAst == null || attributeAst.NamedArguments == null) + return null; + + if (!string.IsNullOrEmpty(expectedAttributeName) && + !string.Equals( + attributeAst.TypeName?.Name, + expectedAttributeName, + StringComparison.OrdinalIgnoreCase) + ) + return null; + + foreach (var namedArg in attributeAst.NamedArguments) + { + if (string.Equals(namedArg.ArgumentName, argumentName, StringComparison.OrdinalIgnoreCase)) + { + return namedArg.Argument?.Extent; + } + } + return null; + } + + /// + /// Represents information about a parameter and its parameter set. + /// + private class ParameterSetInfo + { + public string ParameterName { get; } + public string ParameterSetName { get; } + public AttributeAst ParameterAttributeAst { get; } + + public ParameterSetInfo(string parameterName, string parameterSetName, AttributeAst parameterAttributeAst) + { + ParameterName = parameterName; + ParameterSetName = parameterSetName; + ParameterAttributeAst = parameterAttributeAst; + } + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public override string GetName() => string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.UseConsistentParameterSetNameName + ); + + /// + /// GetCommonName: Retrieves the common name of this rule. + /// + /// The common name of this rule + public override string GetCommonName() => string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameCommonName + ); + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public override string GetDescription() => string.Format( + CultureInfo.CurrentCulture, + Strings.UseConsistentParameterSetNameDescription + ); + + /// + /// Method: Retrieves the type of the rule: builtin, managed or module. + /// + public override SourceType GetSourceType() => SourceType.Builtin; + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning of information. + /// + /// + public override RuleSeverity GetSeverity() => RuleSeverity.Warning; + + /// + /// Method: Retrieves the module/assembly name the rule is from. + /// + public override string GetSourceName() => string.Format( + CultureInfo.CurrentCulture, Strings.SourceName + ); + } +} diff --git a/Tests/Rules/UseConsistentParameterSetName.tests.ps1 b/Tests/Rules/UseConsistentParameterSetName.tests.ps1 new file mode 100644 index 000000000..3ca229fb5 --- /dev/null +++ b/Tests/Rules/UseConsistentParameterSetName.tests.ps1 @@ -0,0 +1,399 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $ruleName = 'PSUseConsistentParameterSetName' + + $ruleSettings = @{ + Enable = $true + } + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = $ruleSettings } + } +} + +Describe "UseConsistentParameterSetName" { + Context "When there are case mismatch violations between DefaultParameterSetName and ParameterSetName" { + It "detects case mismatch between DefaultParameterSetName and ParameterSetName" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='setone')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].Message | Should -Match "DefaultParameterSetName 'SetOne' does not match the case of ParameterSetName 'setone'" + } + + It "detects multiple case mismatches with DefaultParameterSetName" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='setone')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SETONE')] + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations | ForEach-Object { $_.Severity | Should -Be 'Warning' } + } + } + + Context "When there are case mismatch violations between ParameterSetName values" { + It "detects case mismatch between different ParameterSetName values" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='setone')] + [string]$Parameter2, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter3 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].Message | Should -Match "ParameterSetName 'setone' does not match the case of 'SetOne'" + } + + It "detects multiple case variations of the same parameter set name" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='setone')] + [string]$Parameter2, + + [Parameter(ParameterSetName='SETONE')] + [string]$Parameter3 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 # Two mismatches with the first occurrence + $violations | ForEach-Object { $_.Severity | Should -Be 'Warning' } + } + } + + Context "When DefaultParameterSetName is missing" { + It "warns when parameter sets are used but DefaultParameterSetName is not specified" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding()] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Severity | Should -Be 'Warning' + $violations[0].Message | Should -Match "uses parameter sets but does not specify a DefaultParameterSetName" + } + } + + Context "When a parameter is declared multiple times in the same parameter set" { + It "detects duplicate parameter declarations in the same parameter set (explicit)" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName='SetOne')] + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations | ForEach-Object { $_.Message | Should -Be "Parameter 'Parameter1' is declared in parameter-set 'SetOne' multiple times." } + } + + It "detects duplicate parameter declarations in the same parameter set (implicit via omitted ParameterSetName)" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter()] + [Parameter()] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations | ForEach-Object { $_.Message | Should -Be "Parameter 'Parameter1' is declared in parameter-set '__AllParameterSets' multiple times." } + } + + It "detects duplicate parameter declarations in explicit and implicit parameter sets" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName='__AllParameterSets')] + [Parameter()] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations | ForEach-Object { $_.Message | Should -Be "Parameter 'Parameter1' is declared in parameter-set '__AllParameterSets' multiple times." } + } + + + } + + Context "When ParameterSetNames contain inadvisable characters" { + It "detects ParameterSetName containing a new line" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName="Set`nOne")] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Match "should not contain new lines" + } + + It "detects ParameterSetName containing a carriage return" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName="Set`rOne")] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Match "should not contain new lines" + } + + It "detects ParameterSetName containing mixed newline types" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName="Set`r`nOne")] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + + It "detects DefaultParameterSetName containing a new line" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName="Set`nOne")] + param( + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Match "should not contain new lines" + } + + It "detects DefaultParameterSetName containing a carriage return" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName="Set`rOne")] + param( + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].Message | Should -Match "should not contain new lines" + } + + # Missing: DefaultParameterSetName with newlines + It "detects DefaultParameterSetName containing mixed newline types" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName="Set`r`nOne")] + param([string]$Parameter1) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 1 + } + + } + + Context "When there are no violations" { + It "does not flag functions without CmdletBinding" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It "does not flag functions without parameter sets" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding()] + param( + [string]$Parameter1, + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It "does not flag when DefaultParameterSetName and ParameterSetName cases match exactly" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It "does not flag when all ParameterSetName cases match exactly" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter2, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter3 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + # This could be a case where the function can be run without any parameters + # in the default set. + It "does not flag when DefaultParameterSetName doesn't match any ParameterSetName" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='Default')] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [Parameter(ParameterSetName='SetTwo')] + [string]$Parameter2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It "handles parameters without attributes correctly" { + $scriptDefinition = @' +function Test-Function { + [CmdletBinding(DefaultParameterSetName='SetOne')] + param( + [Parameter(ParameterSetName='SetOne')] + [string]$Parameter1, + + [string]$CommonParameter # No Parameter attribute + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context "Real-world scenarios" { + It "handles complex parameter set definitions correctly" { + $scriptDefinition = @' +function Test-ComplexFunction { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName', Mandatory)] + [string]$Name, + + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByID')] + [string]$ComputerName, + + [Parameter(ParameterSetName='ByID', Mandatory)] + [int]$ID + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It "detects case issues in complex scenarios" { + $scriptDefinition = @' +function Test-ComplexFunction { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='byname', Mandatory)] + [string]$Name, + + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByID')] + [string]$ComputerName, + + [Parameter(ParameterSetName='byid', Mandatory)] + [int]$ID + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 # 'byname' and 'byid' case mismatches + $violations | ForEach-Object { $_.Severity | Should -Be 'Warning' } + } + } +} diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 51858d0de..e4a298ef2 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -68,6 +68,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [UseCompatibleSyntax](./UseCompatibleSyntax.md) | Warning | No | Yes | | [UseCompatibleTypes](./UseCompatibleTypes.md) | Warning | No | Yes | | [UseConsistentIndentation](./UseConsistentIndentation.md) | Warning | No | Yes | +| [UseConsistentParameterSetName](./UseConsistentParameterSetName.md) | Warning | No | | | [UseConsistentParametersKind](./UseConsistentParametersKind.md) | Warning | No | Yes | | [UseConsistentWhitespace](./UseConsistentWhitespace.md) | Warning | No | Yes | | [UseCorrectCasing](./UseCorrectCasing.md) | Information | No | Yes | diff --git a/docs/Rules/UseConsistentParameterSetName.md b/docs/Rules/UseConsistentParameterSetName.md new file mode 100644 index 000000000..5ad33eb22 --- /dev/null +++ b/docs/Rules/UseConsistentParameterSetName.md @@ -0,0 +1,135 @@ +--- +description: Use consistent parameter set names and proper parameter set configuration. +ms.date: 08/19/2025 +ms.topic: reference +title: UseConsistentParameterSetName +--- + +# UseConsistentParameterSetName + +**Severity Level: Warning** + +## Description + +Parameter set names in PowerShell are case-sensitive, unlike most other PowerShell elements. This rule ensures consistent casing and proper configuration of parameter sets to avoid runtime errors and improve code clarity. + +The rule performs five different checks: + +1. **Missing DefaultParameterSetName** - Warns when parameter sets are used but no default is specified +2. **Multiple parameter declarations** - Detects when a parameter is declared multiple times in the same parameter set. This is ultimately a runtime exception - this check helps catch it sooner. +3. **Case mismatch between DefaultParameterSetName and ParameterSetName** - Ensures consistent casing +4. **Case mismatch between different ParameterSetName values** - Ensures all references to the same parameter set use identical casing +5. **Parameter set names containing newlines** - Warns against using newline characters in parameter set names + +> [!NOTE] +> This rule is not enabled by default. The user needs to enable it through settings. + +## How + +- Use a `DefaultParameterSetName` when defining multiple parameter sets +- Ensure consistent casing between `DefaultParameterSetName` and `ParameterSetName` values +- Use identical casing for all references to the same parameter set name +- Avoid declaring the same parameter multiple times in a single parameter set +- Do not use newline characters in parameter set names + +## Example + +### Wrong + +```powershell +# Missing DefaultParameterSetName +function Get-Data { + [CmdletBinding()] + param( + [Parameter(ParameterSetName='ByName')] + [string]$Name, + + [Parameter(ParameterSetName='ByID')] + [int]$ID + ) +} + +# Case mismatch between DefaultParameterSetName and ParameterSetName +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='byname')] + [string]$Name, + + [Parameter(ParameterSetName='ByID')] + [int]$ID + ) +} + +# Inconsistent casing between ParameterSetName values +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName')] + [string]$Name, + + [Parameter(ParameterSetName='byname')] + [string]$DisplayName + ) +} + +# Multiple parameter declarations in same set +function Get-Data { + param( + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByName')] + [string]$Name + ) +} + +# Parameter set name with newline +function Get-Data { + param( + [Parameter(ParameterSetName="Set`nOne")] + [string]$Name + ) +} +``` + +### Correct + +```powershell +# Proper parameter set configuration +function Get-Data { + [CmdletBinding(DefaultParameterSetName='ByName')] + param( + [Parameter(ParameterSetName='ByName', Mandatory)] + [string]$Name, + + [Parameter(ParameterSetName='ByName')] + [Parameter(ParameterSetName='ByID')] + [string]$ComputerName, + + [Parameter(ParameterSetName='ByID', Mandatory)] + [int]$ID + ) +} +``` + +## Configuration + +```powershell +Rules = @{ + PSUseConsistentParameterSetName = @{ + Enable = $true + } +} +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. + +## Notes + +- Parameter set names are case-sensitive in PowerShell, making this different from most other PowerShell elements +- The first occurrence of a parameter set name in your code is treated as the canonical casing +- Parameters without [Parameter()] attributes are automatically part of all parameter sets +- It's a PowerShell best practice to always specify a DefaultParameterSetName when using parameter sets \ No newline at end of file From ac48fad089275bf17cc1dded7b4ddafd3555ed29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olav=20R=C3=B8nnestad=20Birkeland?= <6450056+o-l-a-v@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:00:17 +0000 Subject: [PATCH 116/130] Added closing bracket test for formatting presets (#2161) * feat(tests): added closing bracket test for formatting presets * Update Tests/Rules/PlaceCloseBrace.tests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Tests/Rules/PlaceCloseBrace.tests.ps1 | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Tests/Rules/PlaceCloseBrace.tests.ps1 b/Tests/Rules/PlaceCloseBrace.tests.ps1 index 85ccb8979..fb741ee2a 100644 --- a/Tests/Rules/PlaceCloseBrace.tests.ps1 +++ b/Tests/Rules/PlaceCloseBrace.tests.ps1 @@ -383,4 +383,49 @@ if ($true) { $violations.Count | Should -Be 0 } } + + Context "When formatting presets handle if/else" { + BeforeAll { + $AllmanDefinition = @" +if (`$true) +{ + 'yes' +} +else +{ + 'no' +} +"@ + $OTBSDefinition = @" +if (`$true) { + 'yes' +} else { + 'no' +} +"@ + $StroustrupDefinition = @" +if (`$true) { + 'yes' +} +else { + 'no' +} +"@ + } + + It "Allman should have all opening and closing braces on a new line" { + Invoke-Formatter -ScriptDefinition $OTBSDefinition -Settings 'CodeFormattingAllman' | Should -Be $AllmanDefinition + Invoke-Formatter -ScriptDefinition $StroustrupDefinition -Settings 'CodeFormattingAllman' | Should -Be $AllmanDefinition + } + + It "OTBS should place else on same line as the if closing bracket" { + Invoke-Formatter -ScriptDefinition $AllmanDefinition -Settings 'CodeFormattingOTBS' | Should -Be $OTBSDefinition + Invoke-Formatter -ScriptDefinition $StroustrupDefinition -Settings 'CodeFormattingOTBS' | Should -Be $OTBSDefinition + } + + It "Stroustrup should place else on a new line after the if closing bracket" { + Invoke-Formatter -ScriptDefinition $AllmanDefinition -Settings 'CodeFormattingStroustrup' | Should -Be $StroustrupDefinition + Invoke-Formatter -ScriptDefinition $OTBSDefinition -Settings 'CodeFormattingStroustrup' | Should -Be $StroustrupDefinition + } + } } From 669c685ba79ec1acdb819b98f9b0ff8b2b7135a8 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 19 Mar 2026 17:31:53 +0000 Subject: [PATCH 117/130] Add UseSingleValueFromPipelineParameter Rule (#2119) * Added Rule UseSingleValueFromPipelineParameter * Correct rule name for consistency and to fix failing docs tests * Update handling of empty AST passed to AnalyzeScript * Fixup built-in rule count test * Make rule not enabled by default * Apply suggestions from code review Co-authored-by: Christoph Bergmeister * Fix merge --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Rules/Strings.resx | 14 +- Rules/UseSingleValueFromPipelineParameter.cs | 188 ++++++++++ ...SingleValueFromPipelineParameter.Tests.ps1 | 335 ++++++++++++++++++ docs/Rules/README.md | 1 + .../UseSingleValueFromPipelineParameter.md | 101 ++++++ 5 files changed, 638 insertions(+), 1 deletion(-) create mode 100644 Rules/UseSingleValueFromPipelineParameter.cs create mode 100644 Tests/Rules/UseSingleValueFromPipelineParameter.Tests.ps1 create mode 100644 docs/Rules/UseSingleValueFromPipelineParameter.md diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 482cec69f..772b8bb0d 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1263,6 +1263,18 @@ The reserved word '{0}' was used as a function name. This should be avoided. + + Use a single ValueFromPipeline parameter per parameter set + + + Use at most a single ValueFromPipeline parameter per parameter set to avoid undefined or unexpected behaviour. + + + Multiple parameters ({0}) in parameter set '{1}' are marked as ValueFromPipeline. Only one parameter per parameter set should accept pipeline input. + + + UseSingleValueFromPipelineParameter + Use correct function parameters definition kind. @@ -1278,4 +1290,4 @@ Use inline parameters definition instead of param() block in function body. - \ No newline at end of file + diff --git a/Rules/UseSingleValueFromPipelineParameter.cs b/Rules/UseSingleValueFromPipelineParameter.cs new file mode 100644 index 000000000..0b69880aa --- /dev/null +++ b/Rules/UseSingleValueFromPipelineParameter.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management.Automation.Language; +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that identifies parameter blocks with multiple parameters in + /// the same parameter set that are marked as ValueFromPipeline=true, which + /// can cause undefined behavior. + /// + public class UseSingleValueFromPipelineParameter : ConfigurableRule + { + private const string AllParameterSetsName = "__AllParameterSets"; + + /// + /// Analyzes the PowerShell AST for parameter sets with multiple ValueFromPipeline parameters. + /// + /// The PowerShell Abstract Syntax Tree to analyze. + /// The name of the file being analyzed (for diagnostic reporting). + /// A collection of diagnostic records for each violating parameter. + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException(Strings.NullAstErrorMessage); + } + // Find all param blocks that have a Parameter attribute with + // ValueFromPipeline set to true. + var paramBlocks = ast.FindAll(testAst => testAst is ParamBlockAst, true) + .Where(paramBlock => paramBlock.FindAll( + attributeAst => attributeAst is AttributeAst attr && + ParameterAttributeAstHasValueFromPipeline(attr), + true + ).Any()); + + foreach (var paramBlock in paramBlocks) + { + // Find all parameter declarations in the current param block + // Convert the generic ast objects into ParameterAst Objects + // For each ParameterAst, find all it's attributes that have + // ValueFromPipeline set to true (either explicitly or + // implicitly). Flatten the results into a single collection of + // Annonymous objects relating the parameter with it's attribute + // and then group them by parameter set name. + // + // + // https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_parameter_sets?#reserved-parameter-set-name + // + // The default parameter set name is '__AllParameterSets'. + // Not specifying a parameter set name and using the parameter + // set name '__AllParameterSets' are equivalent, so we shouldn't + // treat them like they're different just because one is an + // empty string and the other is not. + // + // Filter the list to only keep parameter sets that have more + // than one ValueFromPipeline parameter. + var parameterSetGroups = paramBlock.FindAll(n => n is ParameterAst, true) + .Cast() + .SelectMany(parameter => parameter.FindAll( + a => a is AttributeAst attr && ParameterAttributeAstHasValueFromPipeline(attr), + true + ).Cast().Select(attr => new { Parameter = parameter, Attribute = attr })) + .GroupBy(item => GetParameterSetForAttribute(item.Attribute) ?? AllParameterSetsName) + .Where(group => group.Count() > 1); + + + foreach (var group in parameterSetGroups) + { + // __AllParameterSets being the default name is...obscure. + // Instead we'll show the user "default". It's more than + // likely the user has not specified a parameter set name, + // so default will make sense. If they have used 'default' + // as their parameter set name, then we're still correct. + var parameterSetName = group.Key == AllParameterSetsName ? "default" : group.Key; + + // Create a concatenated string of parameter names that + // conflict in this parameter set + var parameterNames = string.Join(", ", group.Select(item => item.Parameter.Name.VariablePath.UserPath)); + + // We emit a diagnostic record for each offending parameter + // attribute in the parameter set so it's obvious where all the + // occurrences are. + foreach (var item in group) + { + var message = string.Format(CultureInfo.CurrentCulture, + Strings.UseSingleValueFromPipelineParameterError, + parameterNames, + parameterSetName); + + yield return new DiagnosticRecord( + message, + item.Attribute.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + parameterSetName); + } + } + } + } + + /// + /// Returns whether the specified AttributeAst represents a Parameter attribute + /// that has the ValueFromPipeline named argument set to true (either explicitly or + /// implicitly). + /// + /// The Parameter attribute to examine. + /// Whether the attribute has the ValueFromPipeline named argument set to true. + private static bool ParameterAttributeAstHasValueFromPipeline(AttributeAst attributeAst) + { + // Exit quickly if the attribute is null, has no named arguments, or + // is not a parameter attribute. + if (attributeAst?.NamedArguments == null || + !string.Equals(attributeAst.TypeName?.Name, "Parameter", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return attributeAst.NamedArguments + .OfType() + .Any(namedArg => string.Equals( + namedArg?.ArgumentName, + "ValueFromPipeline", + StringComparison.OrdinalIgnoreCase + // Helper.Instance.GetNamedArgumentAttributeValue handles both explicit ($true) + // and implicit (no value specified) ValueFromPipeline declarations + ) && Helper.Instance.GetNamedArgumentAttributeValue(namedArg)); + } + + /// + /// Gets the ParameterSetName value from a Parameter attribute. + /// + /// The Parameter attribute to examine. + /// The parameter set name, or null if not found or empty. + private static string GetParameterSetForAttribute(AttributeAst attributeAst) + { + // Exit quickly if the attribute is null, has no named arguments, or + // is not a parameter attribute. + if (attributeAst?.NamedArguments == null || + !string.Equals(attributeAst.TypeName.Name, "Parameter", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return attributeAst.NamedArguments + .OfType() + .Where(namedArg => string.Equals( + namedArg?.ArgumentName, + "ParameterSetName", + StringComparison.OrdinalIgnoreCase + )) + .Select(namedArg => namedArg?.Argument) + .OfType() + .Select(stringConstAst => stringConstAst?.Value) + .FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); + } + + public override string GetCommonName() => Strings.UseSingleValueFromPipelineParameterCommonName; + + public override string GetDescription() => Strings.UseSingleValueFromPipelineParameterDescription; + + public override string GetName() => string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.UseSingleValueFromPipelineParameterName); + + public override RuleSeverity GetSeverity() => RuleSeverity.Warning; + + public override string GetSourceName() => Strings.SourceName; + + public override SourceType GetSourceType() => SourceType.Builtin; + } +} \ No newline at end of file diff --git a/Tests/Rules/UseSingleValueFromPipelineParameter.Tests.ps1 b/Tests/Rules/UseSingleValueFromPipelineParameter.Tests.ps1 new file mode 100644 index 000000000..945130b9e --- /dev/null +++ b/Tests/Rules/UseSingleValueFromPipelineParameter.Tests.ps1 @@ -0,0 +1,335 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $ruleName = 'PSUseSingleValueFromPipelineParameter' + + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ + $ruleName = @{ + Enable = $true + } + } + } +} + +Describe 'UseSingleValueFromPipelineParameter' { + + Context 'When multiple parameters have ValueFromPipeline in same parameter set' { + + It "Should flag explicit ValueFromPipeline=`$true in default parameter set" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $InputObject, + + [Parameter(ValueFromPipeline=$true)] + $AnotherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations[0].Message | Should -Match "Multiple parameters \(InputObject, AnotherParam\) in parameter set 'default'" + } + + It 'Should flag implicit ValueFromPipeline in default parameter set' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline)] + $InputObject, + + [Parameter(ValueFromPipeline)] + $SecondParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + } + + It 'Should flag mixed explicit and implicit ValueFromPipeline' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $InputObject, + + [Parameter(ValueFromPipeline)] + $SecondParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + } + + It 'Should flag multiple parameters in named parameter set' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true, ParameterSetName='MySet')] + $InputObject, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='MySet')] + $SecondParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations[0].Message | Should -Match "parameter set 'MySet'" + } + + It 'Should flag three parameters in same parameter set' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $First, + + [Parameter(ValueFromPipeline=$true)] + $Second, + + [Parameter(ValueFromPipeline=$true)] + $Third + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 3 + $violations[0].Message | Should -Match 'Multiple parameters \(First, Second, Third\)' + } + } + + Context 'When parameters are in different parameter sets' { + + It 'Should not flag parameters in different parameter sets' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set1')] + $InputObject1, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set2')] + $InputObject2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It 'Should handle mix of named and default parameter sets correctly' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $DefaultSetParam, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='NamedSet')] + $NamedSetParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context 'When only one parameter has ValueFromPipeline' { + + It 'Should not flag single ValueFromPipeline parameter' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $InputObject, + + [Parameter()] + $OtherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context 'When ValueFromPipeline is explicitly set to false' { + + It "Should not flag parameters with ValueFromPipeline=`$false" { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$false)] + $InputObject, + + [Parameter(ValueFromPipeline=$false)] + $AnotherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It 'Should only flag the true ValueFromPipeline parameter' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + $TrueParam, + + [Parameter(ValueFromPipeline=$false)] + $FalseParam, + + [Parameter()] + $NoValueParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context 'When non-Parameter attributes have ValueFromPipeline property' { + + It 'Should not flag custom attributes with ValueFromPipeline property' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + [CustomAttribute(ValueFromPipeline=$true)] + $InputObject, + + [CustomAttribute(ValueFromPipeline=$true)] + $NonPipelineParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It 'Should not flag ValidateSet with ValueFromPipeline property' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true)] + [ValidateSet('Value1', 'Value2', ValueFromPipeline=$true)] + $InputObject, + + [ValidateSet('Value1', 'Value2', ValueFromPipeline=$true)] + $NonPipelineParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context 'When there are no Parameter attributes' { + + It 'Should not flag functions without Parameter attributes' { + $scriptDefinition = @' +function Test-Function { + param( + $InputObject, + $AnotherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + + It 'Should not flag functions with only non-ValueFromPipeline Parameter attributes' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(Mandatory=$true)] + $InputObject, + + [Parameter(Position=0)] + $AnotherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context 'Complex parameter set scenarios' { + + It 'Should flag violations in multiple parameter sets independently' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set1')] + $Set1Param1, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set1')] + $Set1Param2, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set2')] + $Set2Param1, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='Set2')] + $Set2Param2 + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 4 # 2 violations per parameter set, each parameter gets flagged + + # Check that both parameter sets are mentioned in violations + $violationMessages = $violations.Message -join ' ' + $violationMessages | Should -Match "parameter set 'Set1'" + $violationMessages | Should -Match "parameter set 'Set2'" + } + + It 'Should handle __AllParameterSets parameter set name correctly' { + $scriptDefinition = @' +function Test-Function { + param( + [Parameter(ValueFromPipeline=$true, ParameterSetName='__AllParameterSets')] + $ExplicitAllSets, + + [Parameter(ValueFromPipeline=$true)] + $ImplicitAllSets + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $violations[0].Message | Should -Match "parameter set 'default'" + } + } + + Context 'Suppression scenarios' { + + It 'Should be suppressible by parameter set name' { + $scriptDefinition = @' +function Test-Function { + [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'MySet')] + param( + [Parameter(ValueFromPipeline=$true, ParameterSetName='MySet')] + $InputObject, + + [Parameter(ValueFromPipeline=$true, ParameterSetName='MySet')] + $AnotherParam + ) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + } + } +} \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index e4a298ef2..2f8559623 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -78,6 +78,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [UseProcessBlockForPipelineCommand](./UseProcessBlockForPipelineCommand.md) | Warning | Yes | | | [UsePSCredentialType](./UsePSCredentialType.md) | Warning | Yes | | | [UseShouldProcessForStateChangingFunctions](./UseShouldProcessForStateChangingFunctions.md) | Warning | Yes | | +| [UseSingleValueFromPipelineParameter](./UseSingleValueFromPipelineParameter.md) | Warning | No | | | [UseSingularNouns](./UseSingularNouns.md) | Warning | Yes | Yes | | [UseSupportsShouldProcess](./UseSupportsShouldProcess.md) | Warning | Yes | | | [UseToExportFieldsInManifest](./UseToExportFieldsInManifest.md) | Warning | Yes | | diff --git a/docs/Rules/UseSingleValueFromPipelineParameter.md b/docs/Rules/UseSingleValueFromPipelineParameter.md new file mode 100644 index 000000000..bfaa3fe6a --- /dev/null +++ b/docs/Rules/UseSingleValueFromPipelineParameter.md @@ -0,0 +1,101 @@ +--- +description: Use at most a single ValueFromPipeline parameter per parameter set. +ms.date: 08/08/2025 +ms.topic: reference +title: UseSingleValueFromPipelineParameter +--- +# UseSingleValueFromPipelineParameter + +**Severity Level: Warning** + +## Description + +Parameter sets should have at most one parameter marked as +`ValueFromPipeline = true`. + +This rule identifies functions where multiple parameters within the same +parameter set have `ValueFromPipeline` set to `true` (either explicitly or +implicitly). + +## How + +Ensure that only one parameter per parameter set accepts pipeline input by +value. If you need multiple parameters to accept different types of pipeline +input, use separate parameter sets. + +## Example + +### Wrong + +```powershell +function Process-Data { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string] $InputData, + + [Parameter(ValueFromPipeline)] + [string] $ProcessingMode + ) + + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` + + +### Correct + +```powershell +function Process-Data { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string] $InputData, + + [Parameter(Mandatory)] + [string] $ProcessingMode + ) + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` +## Suppression + +To suppress this rule for a specific parameter set, use the `SuppressMessage` +attribute with the parameter set name: + +```powershell +function Process-Data { + [Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'MyParameterSet')] + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] + [string] $InputData, + + [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] + [string] $ProcessingMode + ) + process { + Write-Output "$ProcessingMode`: $InputData" + } +} +``` + +For the default parameter set, use `'default'` as the suppression target: + +```powershell +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseSingleValueFromPipelineParameter', 'default')] +``` + +## Notes + +- This rule applies to both explicit `ValueFromPipeline = $true` and implicit + `ValueFromPipeline` (which is the same as using `= $true`) +- Parameters with `ValueFromPipeline=$false` are not flagged by this rule +- The rule correctly handles the default parameter set (`__AllParameterSets`) + and named parameter sets +- Different parameter sets can each have their own single `ValueFromPipeline` + parameter without triggering this rule From 45e3e6cc6594876979b2dca3ccbe1921076c86ed Mon Sep 17 00:00:00 2001 From: Adam Erickson <5751456+admercs@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:00:03 -0700 Subject: [PATCH 118/130] Added user-friendly installation instructions (#2141) * added user-friendly installation instructions * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Christoph Bergmeister Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d038ec756..24f2704ff 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,11 @@ documentation for the latest release is published on on learn.microsoft.com. ## Installation To install **PSScriptAnalyzer** from the PowerShell Gallery, see -[Installing PSScriptAnalyzer](https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/overview#installing-psscriptanalyzer). +[Installing PSScriptAnalyzer](https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/overview#installing-psscriptanalyzer) or simply open PowerShell in a Terminal and run: + +```powershell +Install-Module -Name PSScriptAnalyzer +``` To install **PSScriptAnalyzer** from source code: From 68836181f570abd561d5ad1b1fa74efcd8bb3689 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:17:42 +0000 Subject: [PATCH 119/130] Update .NET SDK, PowerShell SDK dependencies (#2168) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: andyleejordan <2226434+andyleejordan@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- Engine/PSScriptAnalyzer.psm1 | 3 ++- global.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f5afcab86..bd8565ee8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,8 +9,8 @@ - - + + diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index acd9daf76..7e2ca8f31 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -9,7 +9,8 @@ $PSModuleRoot = $PSModule.ModuleBase # Import the appropriate nested binary module based on the current PowerShell version $binaryModuleRoot = $PSModuleRoot -[Version] $minimumPowerShellCoreVersion = '7.4.13' +# This should be the equivalent of 7.4.x i.e. latest major/minor supported but lowest patch. +[Version] $minimumPowerShellCoreVersion = '7.4.6' if ($PSVersionTable.PSVersion.Major -ge 6) { $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" # Minimum PowerShell Core version given by PowerShell Core support itself and diff --git a/global.json b/global.json index 875b92095..de3e16bd7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.416", + "version": "8.0.419", "rollForward": "latestFeature" } } From 6cfc26064f2d2f6d3d6c7f0fa5d5537715c72fd5 Mon Sep 17 00:00:00 2001 From: Josh Corrick Date: Fri, 20 Mar 2026 10:16:19 -0700 Subject: [PATCH 120/130] Add Optional PSUseConstrainedLanguageMode rule (#2165) * Add UseConstrainedLanguageMode rule and tests * Enhance CLM rule: restrict types, COM objects, add tests * Enhance detection of disallowed types in CLM rule * Detect and flag class definitions in Constrained Language * Enhance CLM rule to check module manifests for wildcards/.ps1 * Differentiate CLM checks for signed vs unsigned scripts * Improve dot-sourcing detection and expand CLM rule tests * Add IgnoreSignatures option to CLM rule and improve type checks * Add documentation for PSUseConstrainedLanguageMode rule * Increase severity of UseConstrainedLanguageMode to Warning for optional rule * Update test to expect 'Warning' severity instead of 'Info' * Making copilot suggested edits * Detect and flag [PSCustomObject]@{} in CLM scripts * Add UseConstrainedLanguageMode rule to README.md * Update CLM rule: tighten types, docs, and add error string * Add platform checks to CLM tests for cross-platform support * Add missing end tag after merge conflict from main * Add CLM rule ScriptsToProcess; Fix wildcard and dotsource bug --- Rules/Strings.resx | 54 + Rules/UseConstrainedLanguageMode.cs | 1101 +++++++++++++++++ .../UseConstrainedLanguageMode.tests.ps1 | 864 +++++++++++++ docs/Rules/README.md | 1 + docs/Rules/UseConstrainedLanguageMode.md | 378 ++++++ 5 files changed, 2398 insertions(+) create mode 100644 Rules/UseConstrainedLanguageMode.cs create mode 100644 Tests/Rules/UseConstrainedLanguageMode.tests.ps1 create mode 100644 docs/Rules/UseConstrainedLanguageMode.md diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 772b8bb0d..2a04fd759 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1263,6 +1263,60 @@ The reserved word '{0}' was used as a function name. This should be avoided. + + UseConstrainedLanguageMode + + + Consider Constrained Language Mode Restrictions + + + Identifies script patterns that are restricted in Constrained Language Mode. Constrained Language Mode limits the types, cmdlets, and .NET methods that can be used to help secure PowerShell in environments requiring additional restrictions. + + + Add-Type is not permitted in Constrained Language Mode. Consider alternative approaches if this script will run in a restricted environment. + + + New-Object with the COM object '{0}' is not permitted in Constrained Language Mode. Consider alternative approaches if this script will run in a restricted environment. + + + XAML usage is not permitted in Constrained Language Mode. Consider alternative approaches if this script will run in a restricted environment. + + + Dot-sourcing may be restricted in Constrained Language Mode depending on the source location. Ensure scripts are from trusted locations if running in a restricted environment. + + + Invoke-Expression is restricted in Constrained Language Mode. Consider alternative approaches if this script will run in a restricted environment. + + + New-Object with type '{0}' is not permitted in Constrained Language Mode. Consider using an allowed type. + + + Type constraint [{0}] is not permitted in Constrained Language Mode. Consider using an allowed type. + + + Type expression [{0}] is not permitted in Constrained Language Mode. Consider using an allowed type. + + + Type cast [{0}] is not permitted in Constrained Language Mode. Consider using an allowed type. + + + Member '{1}' accessed on type [{0}] which is not permitted in Constrained Language Mode. Consider using an allowed type. + + + PowerShell class '{0}' is not permitted in Constrained Language Mode. Consider using alternative approaches such as hashtables or PSCustomObject. + + + Module manifest field '{0}' uses wildcard ('*') which is not recommended for Constrained Language Mode. Explicitly list exported items instead. + + + Module manifest field '{0}' contains script file '{1}' (.ps1). Use a module file (.psm1) or a binary module (.dll) instead for Constrained Language Mode compatibility. + + + Module manifest field 'ScriptsToProcess' contains script file '{0}' (.ps1). Scripts in ScriptsToProcess run in the caller's session state and are restricted in Constrained Language Mode. Consider moving this logic to module initialization code + + + [PSCustomObject]@{{}} syntax is not permitted in Constrained Language Mode. Use New-Object PSObject -Property @{{}} or plain hashtables instead. + Use a single ValueFromPipeline parameter per parameter set diff --git a/Rules/UseConstrainedLanguageMode.cs b/Rules/UseConstrainedLanguageMode.cs new file mode 100644 index 000000000..7d73dd5eb --- /dev/null +++ b/Rules/UseConstrainedLanguageMode.cs @@ -0,0 +1,1101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System.Management.Automation; + +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// UseConstrainedLanguageMode: Checks for patterns that indicate Constrained Language Mode should be considered. + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class UseConstrainedLanguageMode : ConfigurableRule + { + // Allowed COM objects in Constrained Language Mode + private static readonly HashSet AllowedComObjects = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Scripting.Dictionary", + "Scripting.FileSystemObject", + "VBScript.RegExp" + }; + + // Allowed types in Constrained Language Mode (type accelerators and common types) + private static readonly HashSet AllowedTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "adsi", "adsisearcher", "Alias", "AllowEmptyCollection", "AllowEmptyString", + "AllowNull", "ArgumentCompleter", "ArgumentCompletions", "array", "bigint", + "bool", "byte", "char", "cimclass", "cimconverter", "ciminstance", "CimSession", + "cimtype", "CmdletBinding", "cultureinfo", "datetime", "decimal", "double", + "DscLocalConfigurationManager", "DscProperty", "DscResource", "ExperimentAction", + "Experimental", "ExperimentalFeature", "float", "guid", "hashtable", "int", + "int16", "int32", "int64", "ipaddress", "IPEndpoint", "long", "mailaddress", + "Microsoft.PowerShell.Commands.ModuleSpecification", "NoRunspaceAffinity", + "NullString", "Object", "ObjectSecurity", "ordered", "OutputType", "Parameter", + "PhysicalAddress", "pscredential", "pscustomobject", "PSDefaultValue", + "pslistmodifier", "psobject", "psprimitivedictionary", "PSTypeNameAttribute", + "regex", "sbyte", "securestring", "semver", "short", "single", "string", + "SupportsWildcards", "switch", "timespan", "uint", "uint16", "uint32", "uint64", + "ulong", "uri", "ushort", "ValidateCount", "ValidateDrive", "ValidateLength", + "ValidateNotNull", "ValidateNotNullOrEmpty", "ValidateNotNullOrWhiteSpace", + "ValidatePattern", "ValidateRange", "ValidateScript", "ValidateSet", + "ValidateTrustedData", "ValidateUserDrive", "version", "void", "WildcardPattern", + "wmi", "wmiclass", "wmisearcher", "X500DistinguishedName", "X509Certificate", "xml", + // Full type names for common allowed types + "System.Object", "System.String", "System.Int32", "System.Boolean", "System.Byte", + "System.Collections.Hashtable", "System.DateTime", "System.Version", "System.Uri", + "System.Guid", "System.TimeSpan", "System.Management.Automation.PSCredential", + "System.Management.Automation.PSObject", "System.Security.SecureString", + "System.Text.RegularExpressions.Regex", "System.Xml.XmlDocument", + "System.Collections.ArrayList", "System.Collections.Generic.List", + "System.Net.IPAddress", "System.Net.Mail.MailAddress" + }; + + /// + /// Cache for typed variable assignments per scope to avoid O(N*M) performance issues. + /// Key: Scope AST (FunctionDefinitionAst or ScriptBlockAst) + /// Value: Dictionary mapping variable names to their type names + /// + private Dictionary> _typedVariableCache; + + /// + /// When True, ignores the presence of script signature blocks and runs all CLM checks + /// regardless of whether a script appears to be signed. + /// When False (default), scripts that contain a PowerShell signature block (for example, + /// one starting with '# SIG # Begin signature block') are treated as having elevated + /// permissions for this rule and only critical checks (dot-sourcing, parameter types, + /// manifests) are performed. No cryptographic validation or trust evaluation of the + /// signature is performed. + /// + [ConfigurableRuleProperty(defaultValue: false)] + public bool IgnoreSignatures { get; set; } + + public UseConstrainedLanguageMode() + { + // This rule is disabled by default - users must explicitly enable it + Enable = false; + + // IgnoreSignatures defaults to false (respects signatures) + IgnoreSignatures = false; + } + + /// + /// Checks if a type name is allowed in Constrained Language Mode + /// + private bool IsTypeAllowed(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + { + return true; // Can't determine, so don't flag + } + + // Handle array types (e.g., string[], System.String[], int[][]) + // Strip array brackets and check the base type + string baseTypeName = typeName; + + + // Handle multi-dimensional or jagged arrays by removing all brackets + while (baseTypeName.EndsWith("[]", StringComparison.Ordinal)) + { + baseTypeName = baseTypeName.Substring(0, baseTypeName.Length - 2); + } + + + // Check exact match first + if (AllowedTypes.Contains(baseTypeName)) + { + return true; + } + + // Check simple name (last part after last dot) + if (baseTypeName.Contains('.')) + { + var simpleTypeName = baseTypeName.Substring(baseTypeName.LastIndexOf('.') + 1); + if (AllowedTypes.Contains(simpleTypeName)) + { + return true; + } + } + + return false; + } + + /// + /// Analyzes the script to check for patterns that may require Constrained Language Mode. + /// + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException(nameof(ast)); + } + + // Initialize cache for this analysis to avoid O(N*M) performance issues + _typedVariableCache = new Dictionary>(); + + var diagnosticRecords = new List(); + + // Check if the file is signed (via signature block detection) + bool isFileSigned = IgnoreSignatures ? false : IsScriptSigned(fileName); + + // Note: If IgnoreSignatures is true, isFileSigned will always be false, + // causing all CLM checks to run regardless of actual signature status + + // Check if this is a module manifest (.psd1 file) + bool isModuleManifest = fileName != null && fileName.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase); + + if (isModuleManifest) + { + // Perform PSD1-specific checks + // These checks are ALWAYS enforced, even for signed scripts + CheckModuleManifest(ast, fileName, diagnosticRecords); + } + + // For signed scripts, only check specific patterns that are still restricted + // (unless IgnoreSignatures is true, then this block is skipped) + if (isFileSigned) + { + // Even signed scripts have these restrictions in CLM: + + // 1. Check for dot-sourcing (still restricted in CLM even for signed scripts) + CheckDotSourcing(ast, fileName, diagnosticRecords); + + // 2. Check for type constraints on parameters (still need to be validated) + CheckParameterTypeConstraints(ast, fileName, diagnosticRecords); + + return diagnosticRecords; + } + + // For unsigned scripts (or when IgnoreSignatures is true), perform all CLM checks + CheckAllClmRestrictions(ast, fileName, diagnosticRecords); + + return diagnosticRecords; + } + + /// + /// Checks if a PowerShell script file appears to be digitally signed. + /// Note: This performs a simple text check for the signature block marker. + /// It does NOT validate signature authenticity, certificate trust, or file integrity. + /// For production use, PowerShell's execution policy and Get-AuthenticodeSignature + /// should be used to properly validate signatures. + /// + private bool IsScriptSigned(string fileName) + { + if (string.IsNullOrEmpty(fileName) || !System.IO.File.Exists(fileName)) + { + return false; + } + + // Only check .ps1, .psm1, and .psd1 files + string extension = System.IO.Path.GetExtension(fileName); + if (!extension.Equals(".ps1", StringComparison.OrdinalIgnoreCase) && + !extension.Equals(".psm1", StringComparison.OrdinalIgnoreCase) && + !extension.Equals(".psd1", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + try + { + // Read the file content + string content = System.IO.File.ReadAllText(fileName); + + // Check for signature block marker + // A signed PowerShell script contains a signature block that starts with: + // # SIG # Begin signature block + // + // IMPORTANT: This is a simple text check only. It does NOT validate: + // - Signature authenticity + // - Certificate validity or trust + // - File integrity (hash matching) + // - Certificate expiration + // + // This check assumes that if a signature block is present, the script + // was intended to be signed. Actual signature validation is performed + // by PowerShell at execution time based on execution policy. + return content.IndexOf("# SIG # Begin signature block", StringComparison.OrdinalIgnoreCase) >= 0; + } + catch + { + // If we can't read the file, assume it's not signed + return false; + } + } + + /// + /// Performs all CLM restriction checks (for unsigned scripts). + /// + private void CheckAllClmRestrictions(Ast ast, string fileName, List diagnosticRecords) + { + var addTypeCommands = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdAst.GetCommandName() != null && + cmdAst.GetCommandName().Equals("Add-Type", StringComparison.OrdinalIgnoreCase), + true); + + foreach (CommandAst cmd in addTypeCommands) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeAddTypeError), + cmd.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + + // Check for New-Object with COM objects and TypeName (only specific ones are allowed in CLM) + var newObjectCommands = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdAst.GetCommandName() != null && + cmdAst.GetCommandName().Equals("New-Object", StringComparison.OrdinalIgnoreCase), + true); + + foreach (CommandAst cmd in newObjectCommands) + { + // Use StaticParameterBinder to reliably get parameter values + var bindingResult = StaticParameterBinder.BindCommand(cmd, true); + + // Check for -ComObject parameter + if (bindingResult.BoundParameters.ContainsKey("ComObject")) + { + string comObjectValue = null; + + // Try to get the value from the AST directly first + if (bindingResult.BoundParameters["ComObject"].Value is StringConstantExpressionAst strAst) + { + comObjectValue = strAst.Value; + } + else + { + // Fall back to ConstantValue + comObjectValue = bindingResult.BoundParameters["ComObject"].ConstantValue as string; + } + + // Only flag if COM object name was found AND it's not in the allowed list + if (!string.IsNullOrWhiteSpace(comObjectValue) && !AllowedComObjects.Contains(comObjectValue)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeComObjectError, + comObjectValue), + cmd.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + + // Check for -TypeName parameter + if (bindingResult.BoundParameters.ContainsKey("TypeName")) + { + var typeNameValue = bindingResult.BoundParameters["TypeName"].ConstantValue as string; + + // If ConstantValue is null, try to extract from the AST Value + if (typeNameValue == null && bindingResult.BoundParameters["TypeName"].Value is StringConstantExpressionAst typeStrAst) + { + typeNameValue = typeStrAst.Value; + } + + // Only flag if type name was found AND it's not in the allowed list + if (!string.IsNullOrWhiteSpace(typeNameValue) && !IsTypeAllowed(typeNameValue)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeNewObjectError, + typeNameValue), + cmd.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + + // Check for XAML usage (not allowed in Constrained Language Mode) + var xamlPatterns = ast.FindAll(testAst => + testAst is StringConstantExpressionAst strAst && + strAst.Value.Contains("<") && strAst.Value.Contains("xmlns"), + true); + + foreach (StringConstantExpressionAst xamlAst in xamlPatterns) + { + if (xamlAst.Value.Contains("http://schemas.microsoft.com/winfx")) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeXamlError), + xamlAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + + // Check for dot-sourcing (also called separately for signed scripts) + CheckDotSourcing(ast, fileName, diagnosticRecords); + + // Check for Invoke-Expression usage (restricted in Constrained Language Mode) + var invokeExpressionCommands = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdAst.GetCommandName() != null && + cmdAst.GetCommandName().Equals("Invoke-Expression", StringComparison.OrdinalIgnoreCase), + true); + + foreach (CommandAst cmd in invokeExpressionCommands) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeInvokeExpressionError), + cmd.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + + // Check for class definitions (not allowed in Constrained Language Mode) + var classDefinitions = ast.FindAll(testAst => + testAst is TypeDefinitionAst typeAst && typeAst.IsClass, + true); + + foreach (TypeDefinitionAst classDef in classDefinitions) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeClassError, + classDef.Name), + classDef.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + + // Check for parameter type constraints (also called separately for signed scripts) + CheckParameterTypeConstraints(ast, fileName, diagnosticRecords); + + // Check for disallowed type constraints on variables (e.g., [System.Net.WebClient]$client) + var typeConstraints = ast.FindAll(testAst => + testAst is TypeConstraintAst typeConstraint && + !(typeConstraint.Parent is ParameterAst), // Exclude parameters - handled above + true); + + foreach (TypeConstraintAst typeConstraint in typeConstraints) + { + var typeName = typeConstraint.TypeName.FullName; + if (!IsTypeAllowed(typeName)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeConstrainedTypeError, + typeName), + typeConstraint.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + + // Check for disallowed type expressions and casts (e.g., [System.Net.WebClient]::new() or $x -as [Type]) + var typeExpressions = ast.FindAll(testAst => testAst is TypeExpressionAst, true); + foreach (TypeExpressionAst typeExpr in typeExpressions) + { + var typeName = typeExpr.TypeName.FullName; + if (!IsTypeAllowed(typeName)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeTypeExpressionError, + typeName), + typeExpr.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + + // Check for convert expressions (e.g., $x = [System.Net.WebClient]$value) + var convertExpressions = ast.FindAll(testAst => testAst is ConvertExpressionAst, true); + foreach (ConvertExpressionAst convertExpr in convertExpressions) + { + var typeName = convertExpr.Type.TypeName.FullName; + + // Special case: [PSCustomObject]@{} is not allowed in CLM + // Even though PSCustomObject is an allowed type for parameters, + // the type cast syntax with hashtable literal is blocked in CLM + if (typeName.Equals("PSCustomObject", StringComparison.OrdinalIgnoreCase) && + convertExpr.Child is HashtableAst) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModePSCustomObjectError), + convertExpr.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + continue; // Already flagged, skip general type check + } + + if (!IsTypeAllowed(typeName)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeConvertExpressionError, + typeName), + convertExpr.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + + // Check for member invocations on disallowed types + // This includes method calls and property access on variables with type constraints + var memberInvocations = ast.FindAll(testAst => + testAst is InvokeMemberExpressionAst || testAst is MemberExpressionAst, true); + + foreach (Ast memberAst in memberInvocations) + { + // Skip static member access - already handled by TypeExpressionAst check + if (memberAst is InvokeMemberExpressionAst invokeAst && invokeAst.Static) + { + continue; + } + + if (memberAst is MemberExpressionAst memAst && memAst.Static) + { + continue; + } + + // Get the expression being invoked on (e.g., the variable in $var.Method()) + ExpressionAst targetExpr = memberAst is InvokeMemberExpressionAst invExpr + ? invExpr.Expression + : ((MemberExpressionAst)memberAst).Expression; + + // Check if the target has a type constraint + string constrainedType = GetTypeConstraintFromExpression(targetExpr); + if (!string.IsNullOrWhiteSpace(constrainedType) && !IsTypeAllowed(constrainedType)) + { + string memberName = memberAst is InvokeMemberExpressionAst inv + ? (inv.Member as StringConstantExpressionAst)?.Value ?? "" + : ((memberAst as MemberExpressionAst).Member as StringConstantExpressionAst)?.Value ?? ""; + + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeMemberAccessError, + constrainedType, + memberName), + memberAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + + /// + /// Checks for dot-sourcing patterns which are restricted in CLM even for signed scripts. + /// + private void CheckDotSourcing(Ast ast, string fileName, List diagnosticRecords) + { + // Dot-sourcing is detected by looking for commands where the extent text starts with a dot + // Example: . $PSScriptRoot\Helper.ps1 + // Example: . .\script.ps1 + // PowerShell doesn't have a specific DotSourceExpressionAst, so we check the command extent + var commands = ast.FindAll(testAst => testAst is CommandAst, true); + + foreach (CommandAst cmdAst in commands) + { + // Check if the command extent starts with a dot followed by whitespace + // This indicates dot-sourcing + string extentText = cmdAst.Extent.Text.TrimStart(); + if (extentText.StartsWith(".") && extentText.Length > 1 && char.IsWhiteSpace(extentText[1])) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeDotSourceError), + cmdAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + + /// + /// Checks parameter type constraints which need validation even for signed scripts. + /// + private void CheckParameterTypeConstraints(Ast ast, string fileName, List diagnosticRecords) + { + // Find all parameter definitions + var parameters = ast.FindAll(testAst => testAst is ParameterAst, true); + + foreach (ParameterAst param in parameters) + { + // Check for type constraints on parameters + var typeConstraints = param.Attributes.OfType(); + + foreach (var typeConstraint in typeConstraints) + { + var typeName = typeConstraint.TypeName.FullName; + if (!IsTypeAllowed(typeName)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeConstrainedTypeError, + typeName), + typeConstraint.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + } + + /// + /// Attempts to determine if an expression has a type constraint. + /// Returns the type name if found, otherwise null. + /// + private string GetTypeConstraintFromExpression(ExpressionAst expr) + { + if (expr == null) + { + return null; + } + + // Check if this is a convert expression with a type (e.g., [Type]$var) + if (expr is ConvertExpressionAst convertExpr) + { + return convertExpr.Type.TypeName.FullName; + } + + // Check if this is a variable expression + if (expr is VariableExpressionAst varExpr) + { + // Walk up the AST to find if this variable has a type constraint in a parameter + var parameterAst = FindParameterForVariable(varExpr); + if (parameterAst != null) + { + // Get the first type constraint attribute + var typeConstraint = parameterAst.Attributes + .OfType() + .FirstOrDefault(); + + if (typeConstraint != null) + { + return typeConstraint.TypeName.FullName; + } + } + + // Check if the variable was declared with a type constraint elsewhere + // Look for assignment statements with type constraints + var assignmentWithType = FindTypedAssignment(varExpr); + if (assignmentWithType != null) + { + return assignmentWithType; + } + } + + // Check if this is a member expression that might have a known return type + // For now, we'll be conservative and only check direct type constraints + + return null; + } + + /// + /// Finds the parameter AST for a given variable expression, if it exists. + /// + private ParameterAst FindParameterForVariable(VariableExpressionAst varExpr) + { + if (varExpr == null) + { + return null; + } + + var varName = varExpr.VariablePath.UserPath; + + // Walk up to find the containing function or script block + Ast current = varExpr.Parent; + while (current != null) + { + if (current is FunctionDefinitionAst funcAst) + { + // Check parameters in the param block + var paramBlock = funcAst.Body?.ParamBlock; + if (paramBlock?.Parameters != null) + { + foreach (var param in paramBlock.Parameters) + { + if (string.Equals(param.Name.VariablePath.UserPath, varName, StringComparison.OrdinalIgnoreCase)) + { + return param; + } + } + } + + // Check function parameters (for functions with parameters outside param block) + if (funcAst.Parameters != null) + { + foreach (var param in funcAst.Parameters) + { + if (string.Equals(param.Name.VariablePath.UserPath, varName, StringComparison.OrdinalIgnoreCase)) + { + return param; + } + } + } + + break; // Don't check outer function scopes + } + + if (current is ScriptBlockAst scriptAst) + { + var paramBlock = scriptAst.ParamBlock; + if (paramBlock?.Parameters != null) + { + foreach (var param in paramBlock.Parameters) + { + if (string.Equals(param.Name.VariablePath.UserPath, varName, StringComparison.OrdinalIgnoreCase)) + { + return param; + } + } + } + break; // Don't check outer script block scopes + } + + current = current.Parent; + } + + return null; + } + + /// + /// Builds and caches typed variable assignments for a given scope. + /// This is called once per scope to avoid O(N*M) performance issues. + /// + private Dictionary GetOrBuildTypedVariableCache(Ast scope) + { + if (scope == null) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // Check if we already have cached results for this scope + if (_typedVariableCache.TryGetValue(scope, out var cachedResults)) + { + return cachedResults; + } + + // Build the cache for this scope + var typedVariables = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Find all assignment statements in this scope + var assignments = scope.FindAll(testAst => testAst is AssignmentStatementAst, true); + + foreach (AssignmentStatementAst assignment in assignments) + { + // Check if the left side is a convert expression with a variable + if (assignment.Left is ConvertExpressionAst convertExpr && + convertExpr.Child is VariableExpressionAst assignedVar) + { + var varName = assignedVar.VariablePath.UserPath; + var typeName = convertExpr.Type.TypeName.FullName; + + // Store in cache (first assignment wins) + if (!typedVariables.ContainsKey(varName)) + { + typedVariables[varName] = typeName; + } + } + } + + // Cache the results + _typedVariableCache[scope] = typedVariables; + return typedVariables; + } + + /// + /// Looks for a typed assignment to a variable using cached results. + /// + private string FindTypedAssignment(VariableExpressionAst varExpr) + { + if (varExpr == null) + { + return null; + } + + var varName = varExpr.VariablePath.UserPath; + + // Walk up to find the containing function or script block + Ast searchScope = varExpr.Parent; + while (searchScope != null && + !(searchScope is FunctionDefinitionAst) && + !(searchScope is ScriptBlockAst)) + { + searchScope = searchScope.Parent; + } + + if (searchScope == null) + { + return null; + } + + // Use cached results instead of re-scanning the entire scope + var typedVariables = GetOrBuildTypedVariableCache(searchScope); + + if (typedVariables.TryGetValue(varName, out string typeName)) + { + return typeName; + } + + return null; + } + + /// + /// Checks module manifest (.psd1) files for CLM compatibility issues. + /// + private void CheckModuleManifest(Ast ast, string fileName, List diagnosticRecords) + { + // Find the hashtable in the manifest + var hashtableAst = ast.Find(x => x is HashtableAst, false) as HashtableAst; + + if (hashtableAst == null) + { + return; + } + + // Check for wildcard exports in FunctionsToExport, CmdletsToExport, AliasesToExport + CheckWildcardExports(hashtableAst, fileName, diagnosticRecords); + + // Check for .ps1 files in RootModule, NestedModules, and ScriptsToProcess + CheckScriptModules(hashtableAst, fileName, diagnosticRecords); + } + + /// + /// Checks for wildcard ('*') in export fields which are not allowed in CLM. + /// + private void CheckWildcardExports(HashtableAst hashtableAst, string fileName, List diagnosticRecords) + { + //AliasesToExport and VariablesToExport can use wildcards in CLM, but it is not recommended for performance reasons. + string[] exportFields = { "FunctionsToExport", "CmdletsToExport"}; + + foreach (var kvp in hashtableAst.KeyValuePairs) + { + if (kvp.Item1 is StringConstantExpressionAst keyAst) + { + string keyName = keyAst.Value; + + if (exportFields.Contains(keyName, StringComparer.OrdinalIgnoreCase)) + { + // Check if the value contains a wildcard + bool hasWildcard = false; + IScriptExtent wildcardExtent = null; + + // The value in a hashtable is a StatementAst, need to extract the expression + var valueExpr = GetExpressionFromStatement(kvp.Item2); + + if (valueExpr is StringConstantExpressionAst stringValue) + { + if (stringValue.Value == "*") + { + hasWildcard = true; + wildcardExtent = stringValue.Extent; + } + } + else if (valueExpr is ArrayLiteralAst arrayValue) + { + foreach (var element in arrayValue.Elements) + { + if (element is StringConstantExpressionAst strElement && strElement.Value == "*") + { + hasWildcard = true; + wildcardExtent = strElement.Extent; + break; + } + } + } + else if (valueExpr is ArrayExpressionAst arrayExpr) + { + // Array expressions like @('a', 'b') have a SubExpression inside + if (arrayExpr.SubExpression?.Statements != null) + { + foreach (var stmt in arrayExpr.SubExpression.Statements) + { + var expr = GetExpressionFromStatement(stmt); + if (expr is ArrayLiteralAst arrayLiteral) + { + foreach (var element in arrayLiteral.Elements) + { + if (element is StringConstantExpressionAst strElement && strElement.Value == "*") + { + hasWildcard = true; + wildcardExtent = strElement.Extent; + break; + } + } + } + else if (expr is StringConstantExpressionAst strElement && strElement.Value == "*") + { + // Handle single-item array expressions like @('*') + hasWildcard = true; + wildcardExtent = strElement.Extent; + break; + } + if (hasWildcard) break; + } + } + } + + if (hasWildcard && wildcardExtent != null) + { + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeWildcardExportError, + keyName), + wildcardExtent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + } + } + + /// + /// Checks for .ps1 files in RootModule, NestedModules, and ScriptsToProcess which are not recommended for CLM. + /// + private void CheckScriptModules(HashtableAst hashtableAst, string fileName, List diagnosticRecords) + { + string[] moduleFields = { "RootModule", "NestedModules", "ScriptsToProcess" }; + + foreach (var kvp in hashtableAst.KeyValuePairs) + { + if (kvp.Item1 is StringConstantExpressionAst keyAst) + { + string keyName = keyAst.Value; + + if (moduleFields.Contains(keyName, StringComparer.OrdinalIgnoreCase)) + { + var valueExpr = GetExpressionFromStatement(kvp.Item2); + CheckForPs1Files(valueExpr, keyName, fileName, diagnosticRecords); + } + } + } + } + + /// + /// Extracts an ExpressionAst from a StatementAst (typically from hashtable values). + /// + private ExpressionAst GetExpressionFromStatement(StatementAst statement) + { + if (statement is PipelineAst pipeline && pipeline.PipelineElements.Count == 1) + { + if (pipeline.PipelineElements[0] is CommandExpressionAst commandExpr) + { + return commandExpr.Expression; + } + } + return null; + } + + /// + /// Helper method to get the appropriate error message for .ps1 file usage in module manifests. + /// + private string GetPs1FileErrorMessage(string fieldName, string scriptFileName) + { + if (fieldName.Equals("ScriptsToProcess", StringComparison.OrdinalIgnoreCase)) + { + return String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeScriptsToProcessError, + scriptFileName); + } + else + { + return String.Format(CultureInfo.CurrentCulture, + Strings.UseConstrainedLanguageModeScriptModuleError, + fieldName, + scriptFileName); + } + } + + /// + /// Helper method to check if an expression contains .ps1 file references. + /// + private void CheckForPs1Files(ExpressionAst valueAst, string fieldName, string fileName, List diagnosticRecords) + { + if (valueAst is StringConstantExpressionAst stringValue) + { + if (stringValue.Value != null && stringValue.Value.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + GetPs1FileErrorMessage(fieldName, stringValue.Value), + stringValue.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + else if (valueAst is ArrayLiteralAst arrayValue) + { + foreach (var element in arrayValue.Elements) + { + if (element is StringConstantExpressionAst strElement && + strElement.Value != null && + strElement.Value.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + GetPs1FileErrorMessage(fieldName, strElement.Value), + strElement.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + else if (valueAst is ArrayExpressionAst arrayExpr) + { + // Array expressions like @('a', 'b') have a SubExpression inside + if (arrayExpr.SubExpression?.Statements != null) + { + foreach (var stmt in arrayExpr.SubExpression.Statements) + { + var expr = GetExpressionFromStatement(stmt); + if (expr is ArrayLiteralAst arrayLiteral) + { + foreach (var element in arrayLiteral.Elements) + { + if (element is StringConstantExpressionAst strElement && + strElement.Value != null && + strElement.Value.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + GetPs1FileErrorMessage(fieldName, strElement.Value), + strElement.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + else if (expr is StringConstantExpressionAst strElement && + strElement.Value != null && + strElement.Value.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + { + diagnosticRecords.Add( + new DiagnosticRecord( + GetPs1FileErrorMessage(fieldName, strElement.Value), + strElement.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName + )); + } + } + } + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseConstrainedLanguageModeDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.UseConstrainedLanguageModeName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} diff --git a/Tests/Rules/UseConstrainedLanguageMode.tests.ps1 b/Tests/Rules/UseConstrainedLanguageMode.tests.ps1 new file mode 100644 index 000000000..02de6b9ed --- /dev/null +++ b/Tests/Rules/UseConstrainedLanguageMode.tests.ps1 @@ -0,0 +1,864 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# Tests for UseConstrainedLanguageMode rule +# +# Some tests are Windows-specific (COM objects, XAML) and will be skipped on non-Windows platforms. + +BeforeDiscovery { + # Detect OS for platform-specific tests + $script:IsWindowsOS = $true + $script:IsLinuxOS = $false + $script:IsMacOSOS = $false + + if ($PSVersionTable.PSVersion.Major -ge 6) { + # PowerShell Core has built-in OS detection variables + $script:IsWindowsOS = $IsWindows + $script:IsLinuxOS = $IsLinux + $script:IsMacOSOS = $IsMacOS + } +} +BeforeAll { + $testRootDirectory = Split-Path -Parent $PSScriptRoot + Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") + + $violationName = "PSUseConstrainedLanguageMode" + $ruleName = $violationName + + # The rule is disabled by default, so we need to enable it + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ + $ruleName = @{ + Enable = $true + } + } + } +} + +Describe "UseConstrainedLanguageMode" { +Context "When Add-Type is used" { + It "Should detect Add-Type usage" { + $def = @' +Add-Type -TypeDefinition @" + public class TestType { + public static string Test() { return "test"; } + } +"@ +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 1 + $violations[0].RuleName | Should -Be $violationName + $violations[0].Message | Should -BeLike "*Add-Type*" + } + + It "Should not flag other commands" { + $def = 'Get-Process | Where-Object { $_.Name -eq "powershell" }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + } + + Context "When New-Object with COM is used" { + It "Should detect disallowed New-Object -ComObject usage" -Skip:(-not $script:IsWindowsOS) { + $def = 'New-Object -ComObject "Excel.Application"' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*COM object*" + } + + It "Should NOT flag allowed COM objects - Scripting.Dictionary" -Skip:(-not $script:IsWindowsOS) { + $def = 'New-Object -ComObject "Scripting.Dictionary"' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag allowed COM objects - Scripting.FileSystemObject" -Skip:(-not $script:IsWindowsOS) { + $def = 'New-Object -ComObject "Scripting.FileSystemObject"' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag allowed COM objects - VBScript.RegExp" -Skip:(-not $script:IsWindowsOS) { + $def = 'New-Object -ComObject "VBScript.RegExp"' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag New-Object with allowed TypeName" { + $def = 'New-Object -TypeName System.Collections.ArrayList' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should flag New-Object with disallowed TypeName" { + $def = 'New-Object -TypeName System.IO.File' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*not permitted*" + } + } + + Context "When XAML is used" { + It "Should detect XAML usage" -Skip:(-not $script:IsWindowsOS) { + $def = @' +$xaml = @" + + + +"@ +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*XAML*" + } + } + + Context "When Invoke-Expression is used" { + It "Should detect Invoke-Expression usage" { + $def = 'Invoke-Expression "Get-Process"' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*Invoke-Expression*" + } + } + + Context "When dot-sourcing is used" { + It "Should detect dot-sourcing in unsigned scripts" { + $def = '. $PSScriptRoot\Helper.ps1' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*dot*" + } + } + + Context "When PowerShell classes are used" { + It "Should detect class definition" { + $def = @' +class MyClass { + [string]$Name + [int]$Value + + MyClass([string]$name) { + $this.Name = $name + } +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*class*MyClass*" + } + + It "Should detect multiple class definitions" { + $def = @' +class FirstClass { + [string]$Name +} + +class SecondClass { + [int]$Value +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 2 + } + + It "Should not flag enum definitions" { + $def = @' +enum MyEnum { + Value1 + Value2 +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + # Enums are allowed, so no class-specific violations + # (though we may still flag other issues if present) + $classViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*class*" + } + $classViolations | Should -BeNullOrEmpty + } + } + + Context "When type expressions are used" { + It "Should flag static type reference with new()" { + $def = '$instance = [System.IO.Directory]::new()' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*System.IO.Directory*" + } + + It "Should flag static method call on disallowed type" { + $def = '[System.IO.File]::ReadAllText("C:\test.txt")' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*" + } + + It "Should NOT flag static reference to allowed type" { + $def = '[string]::Empty' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $typeExprViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*type expression*string*" + } + $typeExprViolations | Should -BeNullOrEmpty + } + } + + Context "When module manifests (.psd1) are analyzed" { + BeforeAll { + $tempPath = Join-Path $TestDrive "TestManifests" + New-Item -Path $tempPath -ItemType Directory -Force | Out-Null + } + + It "Should flag wildcard in FunctionsToExport" { + $manifestPath = Join-Path $tempPath "WildcardFunctions.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + FunctionsToExport = '*' +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*FunctionsToExport*wildcard*" + } + + It "Should flag wildcard array in FunctionsToExport" { + $manifestPath = Join-Path $tempPath "WildcardFunctions.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + FunctionsToExport = @('*') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*FunctionsToExport*wildcard*" + } + + It "Should flag wildcard in CmdletsToExport" { + $manifestPath = Join-Path $tempPath "WildcardCmdlets.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + CmdletsToExport = '*' +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*CmdletsToExport*wildcard*" + } + + It "Should NOT flag explicit list of exports" { + $manifestPath = Join-Path $tempPath "ExplicitExports.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + FunctionsToExport = @('Get-MyFunction', 'Set-MyFunction') + CmdletsToExport = @('Get-MyCmdlet') + AliasesToExport = @() +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $wildcardViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*wildcard*" + } + $wildcardViolations | Should -BeNullOrEmpty + } + + It "Should flag .ps1 file in RootModule" { + $manifestPath = Join-Path $tempPath "ScriptRootModule.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + RootModule = 'MyModule.ps1' +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*RootModule*MyModule.ps1*" + } + + It "Should flag .ps1 file in NestedModules" { + $manifestPath = Join-Path $tempPath "ScriptNestedModule.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + NestedModules = @('Helper.ps1', 'Utility.psm1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*NestedModules*Helper.ps1*" + } + + It "Should NOT flag .psm1 or .dll modules" { + $manifestPath = Join-Path $tempPath "BinaryModules.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + RootModule = 'MyModule.psm1' + NestedModules = @('Helper.dll', 'Utility.psm1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $scriptModuleViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*.ps1*" + } + $scriptModuleViolations | Should -BeNullOrEmpty + } + + It "Should flag .ps1 file in ScriptsToProcess" { + $manifestPath = Join-Path $tempPath "ScriptsToProcess.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + ScriptsToProcess = @('Init.ps1', 'Setup.ps1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*ScriptsToProcess*Init.ps1*" + } + + It "Should use different error message for ScriptsToProcess" { + $manifestPath = Join-Path $tempPath "ScriptsToProcessMessage.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + ScriptsToProcess = 'Init.ps1' +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + # ScriptsToProcess should have a specific message about caller's session state + $matchingViolations[0].Message | Should -BeLike "*caller*session state*" + $matchingViolations[0].Message | Should -BeLike "*Init.ps1*" + } + + It "Should flag single-item array in ScriptsToProcess" { + $manifestPath = Join-Path $tempPath "ScriptsToProcessArray.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + ScriptsToProcess = @('Init.ps1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*ScriptsToProcess*Init.ps1*" + } + + It "Should NOT flag .psm1 files in ScriptsToProcess" { + $manifestPath = Join-Path $tempPath "ScriptsToProcessPsm1.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + ScriptsToProcess = @('Init.psm1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $scriptViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*ScriptsToProcess*" + } + $scriptViolations | Should -BeNullOrEmpty + } + + It "Should flag both wildcard and .ps1 issues in same manifest" { + $manifestPath = Join-Path $tempPath "MultipleIssues.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + RootModule = 'MyModule.ps1' + FunctionsToExport = '*' + CmdletsToExport = '*' +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should have at least 3 violations: RootModule .ps1, FunctionsToExport *, CmdletsToExport * + $matchingViolations.Count | Should -BeGreaterOrEqual 3 + } + + It "Should flag ScriptsToProcess and RootModule with different messages" { + $manifestPath = Join-Path $tempPath "MixedScriptFields.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + RootModule = 'MyModule.ps1' + ScriptsToProcess = @('Init.ps1') +} +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 2 + + # Check that we have both types of messages + $scriptsToProcessMsg = $matchingViolations | Where-Object { $_.Message -like "*caller*session state*" } + $rootModuleMsg = $matchingViolations | Where-Object { $_.Message -like "*RootModule*" -and $_.Message -notlike "*caller*session state*" } + + $scriptsToProcessMsg.Count | Should -Be 1 + $rootModuleMsg.Count | Should -Be 1 + + # Verify the specific field names are mentioned + $scriptsToProcessMsg[0].Message | Should -BeLike "*Init.ps1*" + $rootModuleMsg[0].Message | Should -BeLike "*MyModule.ps1*" + } + } + + Context "Rule severity" { + It "Should have Warning severity" { + $def = 'Add-Type -AssemblyName System.Windows.Forms' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations[0].Severity | Should -Be 'Warning' + } + } + + Context "When type constraints are used" { + It "Should flag disallowed type constraint on parameter" { + $def = 'function Test { param([System.IO.File]$FileHelper) }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*not permitted*" + } + + It "Should flag disallowed type constraint on variable declaration" { + $def = '[System.IO.File]$fileHelper = $null' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*not permitted*" + } + + It "Should flag disallowed type cast on variable assignment" { + $def = '$fileHelper = [System.IO.File]$value' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*not permitted*" + } + + It "Should NOT flag allowed type constraint" { + $def = 'function Test { param([string]$Name, [int]$Count, [hashtable]$Data) }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag allowed type cast on variable" { + $def = '[string]$name = $null' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should flag multiple type issues in same script" { + $def = @' +function Test { + param([System.IO.File]$FileHelper) + [System.IO.Directory]$dirHelper = $null + $pathHelper = [System.IO.Path]::new() +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag: 1) param type constraint, 2) variable type constraint, 3) type expression + # Note: May also flag member access if methods/properties are called on typed variables + $matchingViolations.Count | Should -BeGreaterOrEqual 3 + } + } + + Context "When PSCustomObject type cast is used" { + It "Should flag [PSCustomObject]@{} syntax" { + $def = '$obj = [PSCustomObject]@{ Name = "Test"; Value = 42 }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -BeGreaterThan 0 + $matchingViolations[0].Message | Should -BeLike "*PSCustomObject*" + } + + It "Should flag multiple [PSCustomObject]@{} instances" { + $def = @' +$obj1 = [PSCustomObject]@{ Name = "Test1" } +$obj2 = [PSCustomObject]@{ Name = "Test2" } +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + $matchingViolations.Count | Should -Be 2 + } + + It "Should NOT flag PSCustomObject as parameter type" { + $def = 'function Test { param([PSCustomObject]$InputObject) }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag New-Object PSObject" { + $def = '$obj = New-Object PSObject -Property @{ Name = "Test" }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag plain hashtables" { + $def = '$obj = @{ Name = "Test"; Value = 42 }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag [PSCustomObject] with variable (not hashtable literal)" { + $def = '$hash = @{}; $obj = [PSCustomObject]$hash' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # This is a type cast but not the @{} literal pattern + # Since PSCustomObject is in allowed list, this won't be flagged + $matchingViolations | Should -BeNullOrEmpty + } + + } + + Context "When instance methods are invoked on disallowed types" { + It "Should flag method invocation on parameter with disallowed type constraint" { + $def = @' +function Read-File { + param([System.IO.File]$FileHelper, [string]$Path) + $FileHelper.ReadAllText($Path) +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag both the type constraint AND the member access + $matchingViolations.Count | Should -BeGreaterThan 1 + # At least one violation should mention ReadAllText + ($matchingViolations.Message | Where-Object { $_ -like "*ReadAllText*" }).Count | Should -BeGreaterThan 0 + } + + It "Should flag property access on variable with disallowed type constraint" { + $def = @' +function Test { + param([System.IO.FileInfo]$FileHelper) + $fullPath = $FileHelper.FullName +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag both the type constraint AND the member access + $matchingViolations.Count | Should -BeGreaterThan 1 + # At least one violation should mention FullName + ($matchingViolations.Message | Where-Object { $_ -like "*FullName*" }).Count | Should -BeGreaterThan 0 + } + + It "Should flag method invocation on typed variable assignment" { + $def = @' +[System.IO.File]$fileHelper = $null +$result = $fileHelper.ReadAllText("C:\test.txt") +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag both the type constraint AND the member access + $matchingViolations.Count | Should -BeGreaterThan 1 + # At least one violation should mention ReadAllText + ($matchingViolations.Message | Where-Object { $_ -like "*ReadAllText*" }).Count | Should -BeGreaterThan 0 + } + + It "Should NOT flag method invocation on allowed types" { + $def = @' +function Test { + param([string]$Text) + $upper = $Text.ToUpper() + $length = $Text.Length +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty + } + + It "Should NOT flag static method calls on disallowed types (already caught by type expression check)" { + $def = '[System.IO.File]::Exists("test.txt")' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should only flag once for the type expression, not for member access + $matchingViolations.Count | Should -Be 1 + $matchingViolations[0].Message | Should -BeLike "*System.IO.File*" + } + + It "Should flag chained method calls on disallowed types" { + $def = @' +function Test { + param([System.IO.FileInfo]$FileHelper) + $result = $FileHelper.OpenText().ReadToEnd() +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag the type constraint and at least the first member access + $matchingViolations.Count | Should -BeGreaterThan 1 + } + + It "Should handle complex scenarios with multiple typed variables" { + $def = @' +function Complex-Test { + param( + [System.IO.File]$FileHelper, + [System.IO.Directory]$DirHelper, + [string]$SafeString + ) + + $data = $FileHelper.ReadAllBytes("C:\test.bin") + $DirHelper.GetFiles("C:\temp") + $upper = $SafeString.ToUpper() +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + # Should flag: 2 type constraints + 2 method invocations (not SafeString) + $matchingViolations.Count | Should -BeGreaterThan 2 + } + } + + Context "When scripts are digitally signed" { + BeforeAll { + $tempPath = Join-Path $TestDrive "SignedScripts" + New-Item -Path $tempPath -ItemType Directory -Force | Out-Null + } + + It "Should NOT flag Add-Type in signed scripts" { + $scriptPath = Join-Path $tempPath "SignedWithAddType.ps1" + $scriptContent = @' +Add-Type -TypeDefinition "public class Test { }" + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $scriptPath -Value $scriptContent + $violations = Invoke-ScriptAnalyzer -Path $scriptPath -Settings $settings + $addTypeViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*Add-Type*" + } + $addTypeViolations | Should -BeNullOrEmpty + } + + It "Should NOT flag disallowed types in signed scripts" { + $scriptPath = Join-Path $tempPath "SignedWithDisallowedType.ps1" + $scriptContent = @' +$fileHelper = New-Object System.IO.FileInfo("C:\test.txt") +$data = $fileHelper.OpenText() + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $scriptPath -Value $scriptContent + $violations = Invoke-ScriptAnalyzer -Path $scriptPath -Settings $settings + $typeViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*FileInfo*type*" + } + $typeViolations | Should -BeNullOrEmpty + } + + It "Should NOT flag classes in signed scripts" { + $scriptPath = Join-Path $tempPath "SignedWithClass.ps1" + $scriptContent = @' +class MyClass { + [string]$Name +} + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $scriptPath -Value $scriptContent + $violations = Invoke-ScriptAnalyzer -Path $scriptPath -Settings $settings + $classViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*class*MyClass*" + } + $classViolations | Should -BeNullOrEmpty + } + + It "Should STILL flag dot-sourcing in signed scripts" { + $scriptPath = Join-Path $tempPath "SignedWithDotSource.ps1" + $scriptContent = @' +. .\Helper.ps1 +. .\U tility.ps1 + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $scriptPath -Value $scriptContent + $violations = Invoke-ScriptAnalyzer -Path $scriptPath -Settings $settings + $dotSourceViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*dot*" + } + # Dot-sourcing should still be flagged even in signed scripts + $dotSourceViolations.Count | Should -BeGreaterThan 0 + } + + It "Should STILL flag disallowed parameter types in signed scripts" { + $scriptPath = Join-Path $tempPath "SignedWithBadParam.ps1" + $scriptContent = @' +function Test { + param([System.IO.File]$FileHelper) + Write-Output "Test" +} + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $scriptPath -Value $scriptContent + $violations = Invoke-ScriptAnalyzer -Path $scriptPath -Settings $settings + $paramViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*File*" + } + # Parameter type constraints should still be flagged + $paramViolations.Count | Should -BeGreaterThan 0 + } + + It "Should STILL flag wildcard exports in signed manifests" { + $manifestPath = Join-Path $tempPath "SignedManifest.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + FunctionsToExport = '*' +} + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $wildcardViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*wildcard*" + } + # Wildcard exports should still be flagged + $wildcardViolations.Count | Should -BeGreaterThan 0 + } + + It "Should STILL flag .ps1 modules in signed manifests" { + $manifestPath = Join-Path $tempPath "SignedManifestWithScript.psd1" + $manifestContent = @' +@{ + ModuleVersion = '1.0.0' + GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + RootModule = 'MyModule.ps1' +} + +# SIG # Begin signature block +# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ... +# SIG # End signature block +'@ + Set-Content -Path $manifestPath -Value $manifestContent + $violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings + $scriptModuleViolations = $violations | Where-Object { + $_.RuleName -eq $violationName -and $_.Message -like "*.ps1*" + } + # Script modules should still be flagged + $scriptModuleViolations.Count | Should -BeGreaterThan 0 + } + } + + Context "Performance with large scripts" { + It "Should handle scripts with many typed variables and member invocations efficiently" { + # This test verifies the O(N+M) cache optimization + # Without caching, this would be O(N*M) and very slow + + # Build a script with many typed variables and member invocations + $scriptBuilder = [System.Text.StringBuilder]::new() + [void]$scriptBuilder.AppendLine('function Test-Performance {') + [void]$scriptBuilder.AppendLine(' param([string]$Path)') + + # Add 30 typed variable assignments + for ($i = 1; $i -le 30; $i++) { + [void]$scriptBuilder.AppendLine(" [System.IO.File]`$file$i = `$null") + } + + # Add 50 member invocations (testing cache reuse) + for ($i = 1; $i -le 50; $i++) { + $varNum = ($i % 30) + 1 + [void]$scriptBuilder.AppendLine(" `$result$i = `$file$varNum.ReadAllText(`$Path)") + } + + [void]$scriptBuilder.AppendLine('}') + $def = $scriptBuilder.ToString() + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + + # Should detect violations (30 type constraints + 50 member accesses = 80+) + $matchingViolations.Count | Should -BeGreaterThan 50 + } + + It "Should cache results per scope correctly" { + # Test that cache is scoped properly and doesn't leak between functions + $def = @' +function Function1 { + [System.IO.File]$file1 = $null + $result1 = $file1.ReadAllText("C:\test1.txt") +} + +function Function2 { + [System.IO.Directory]$file1 = $null + $result2 = $file1.GetFiles("C:\temp") +} +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName } + + # Should detect violations in both functions + # Each function has: 1 type constraint + 1 member access = 2 violations each + $matchingViolations.Count | Should -BeGreaterOrEqual 4 + + # Verify both File and Directory are mentioned + $messages = $matchingViolations.Message -join ' ' + $messages | Should -BeLike "*File*" + $messages | Should -BeLike "*Directory*" + } + } +} diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 2f8559623..5df834708 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -71,6 +71,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [UseConsistentParameterSetName](./UseConsistentParameterSetName.md) | Warning | No | | | [UseConsistentParametersKind](./UseConsistentParametersKind.md) | Warning | No | Yes | | [UseConsistentWhitespace](./UseConsistentWhitespace.md) | Warning | No | Yes | +| [UseConstrainedLanguageMode](./UseConstrainedLanguageMode.md) | Warning | No | Yes | | [UseCorrectCasing](./UseCorrectCasing.md) | Information | No | Yes | | [UseDeclaredVarsMoreThanAssignments](./UseDeclaredVarsMoreThanAssignments.md) | Warning | Yes | | | [UseLiteralInitializerForHashtable](./UseLiteralInitializerForHashtable.md) | Warning | Yes | | diff --git a/docs/Rules/UseConstrainedLanguageMode.md b/docs/Rules/UseConstrainedLanguageMode.md new file mode 100644 index 000000000..50ccfd0e2 --- /dev/null +++ b/docs/Rules/UseConstrainedLanguageMode.md @@ -0,0 +1,378 @@ +--- +description: Use patterns compatible with Constrained Language Mode +ms.date: 03/17/2026 +ms.topic: reference +title: UseConstrainedLanguageMode +--- +# UseConstrainedLanguageMode + +**Severity Level: Warning** + +## Description + +This rule identifies PowerShell patterns that are restricted or not permitted in Constrained Language Mode (CLM). + +Constrained Language Mode is a PowerShell security feature that restricts: +- .NET types that can be used +- COM objects that can be instantiated +- Commands that can be executed +- Language features that can be used + +CLM is commonly used in: +- Application Control environments (Application Control for Business, AppLocker) +- Just Enough Administration (JEA) endpoints +- Secure environments requiring additional PowerShell restrictions + +**Signed Script Behavior**: Digitally signed scripts from trusted publishers execute in Full Language Mode (FLM) even in CLM environments. The rule detects signature blocks (`# SIG # Begin signature block`) and adjusts checks accordingly - most restrictions don't apply to signed scripts, but certain checks (dot-sourcing, parameter types, manifest best practices) are always enforced. + +**Important**: The rule performs a simple text check for signature blocks and does NOT validate signature authenticity or certificate trust. Actual signature validation is performed by PowerShell at runtime. + +## Constrained Language Mode Restrictions + +### Unsigned Scripts (Full CLM Checking) + +The following are flagged for unsigned scripts: + +1. **Add-Type** - Code compilation not permitted +2. **Disallowed COM Objects** - Only Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp allowed +3. **Disallowed .NET Types** - Only ~70 allowed types (string, int, hashtable, pscredential, etc.) +4. **Type Constraints** - On parameters and variables +5. **Type Expressions** - Static type references like `[Type]::Method()` +6. **Type Casts** - Converting to disallowed types +7. **Member Invocations** - Methods/properties on disallowed types +8. **PowerShell Classes** - `class` keyword not permitted +9. **XAML/WPF** - Not permitted +10. **Invoke-Expression** - Restricted +11. **Dot-Sourcing** - May be restricted depending on the file being sourced +12. **Module Manifest Wildcards** - Wildcard exports not recommended +13. **Module Manifest .ps1 Files** - Script modules ending with .ps1 not allowed + +Always enforced, even for signed scripts + +### Signed Scripts (Selective Checking) + +For scripts with signature blocks, only these are checked: +- Dot-sourcing +- Parameter type constraints +- Module manifest wildcards (.psd1 files) +- Module manifest script modules (.psd1 files) + +## Configuration + +### Basic Configuration + +```powershell +@{ + Rules = @{ + PSUseConstrainedLanguageMode = @{ + Enable = $true + } + } +} +``` + +### Parameters + +#### Enable: bool (Default value is `$false`) + +Enable or disable the rule during ScriptAnalyzer invocation. This rule is disabled by default because not all scripts need CLM compatibility. + +#### IgnoreSignatures: bool (Default value is `$false`) + +Control signature detection behavior: + +- `$false` (default): Automatically detect signatures. Signed scripts get selective checking, unsigned get full checking. +- `$true`: Bypass signature detection. ALL scripts get full CLM checking regardless of signature status. + +```powershell +@{ + Rules = @{ + PSUseConstrainedLanguageMode = @{ + Enable = $true + IgnoreSignatures = $true # Enforce full CLM compliance for all scripts + } + } +} +``` + +**Use `IgnoreSignatures = $true` when:** +- Auditing signed scripts for complete CLM compatibility +- Preparing scripts for untrusted environments +- Enforcing strict CLM compliance organization-wide +- Development/testing to see all potential issues + +## How to Fix + +### Replace Add-Type + +Use allowed cmdlets or pre-compile assemblies. + +### Replace Disallowed COM Objects + +Use only allowed COM objects (Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp) or PowerShell cmdlets. + +### Replace Disallowed Types + +Use allowed type accelerators (`[string]`, `[int]`, `[hashtable]`, etc.) or allowed cmdlets instead of disallowed .NET types. + +### Replace PowerShell Classes + +Use `New-Object PSObject` with `Add-Member` or hashtables instead of classes. + +**Important**: `[PSCustomObject]@{}` syntax is NOT allowed in CLM because it uses type casting. + +### Avoid XAML + +Don't use WPF/XAML in CLM-compatible scripts. + +### Replace Invoke-Expression + +Use direct execution (`&`) or safer alternatives. + +### Replace Dot-Sourcing + +Use modules with Import-Module instead of dot-sourcing when possible. + +### Fix Module Manifests + +- Replace wildcard exports (`*`) with explicit lists +- Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules +- Don't use ScriptsToProcess as it loads in the caller's scope and will be blocked. + +## Examples + +### Example 1: Add-Type + +#### Wrong + +```powershell +Add-Type -TypeDefinition @" + public class Helper { + public static string DoWork() { return "Done"; } + } +"@ +``` + +#### Correct + +```powershell + # Code sign your scripts/modules using proper signing tools + # (for example, Set-AuthenticodeSignature or external signing processes) + # Use allowed cmdlets instead of Add-Type-defined types where possible + # Or pre-compile, sign, and load the assembly (for example, via Add-Type -Path) +``` + +### Example 2: COM Objects + +#### Wrong + +```powershell +$excel = New-Object -ComObject Excel.Application +``` + +#### Correct + +```powershell +# Use allowed COM object +$dict = New-Object -ComObject Scripting.Dictionary + +# Or use PowerShell cmdlets +Import-Excel -Path $file # From ImportExcel module +``` + +### Example 3: Disallowed Types + +#### Wrong + +```powershell +# Type constraint and member invocation flagged +function Download-File { + param([System.Net.WebClient]$Client) + $Client.DownloadString($url) +} + +# Type cast and method call flagged +[System.Net.WebClient]$client = New-Object System.Net.WebClient +$data = $client.DownloadData($url) +``` + +#### Correct + +```powershell +# Use allowed cmdlets +function Download-File { + param([string]$Url) + Invoke-WebRequest -Uri $Url +} + +# Use allowed types +function Process-Text { + param([string]$Text) + $upper = $Text.ToUpper() # String methods are allowed +} +``` + +### Example 4: PowerShell Classes + +#### Wrong + +```powershell +class MyClass { + [string]$Name + + [string]GetInfo() { + return $this.Name + } +} + +# Also wrong - uses type cast +$obj = [PSCustomObject]@{ + Name = "Test" +} +``` + +#### Correct + +```powershell +# Option 1: New-Object PSObject with Add-Member +$obj = New-Object PSObject -Property @{ + Name = "Test" +} + +$obj | Add-Member -MemberType ScriptMethod -Name GetInfo -Value { + return $this.Name +} + +Add-Member -InputObject $obj -NotePropertyMembers @{"Number" = 42} + +# Option 2: Hashtable +$obj = @{ + Name = "Test" + Number = 42 +} +``` + +### Example 5: Module Manifests + +#### Wrong + +```powershell +@{ + ModuleVersion = '1.0.0' + RootModule = 'MyModule.ps1' # .ps1 not recommended + FunctionsToExport = '*' # Wildcard not recommended + CmdletsToExport = '*' +} +``` + +#### Correct + +```powershell +@{ + ModuleVersion = '1.0.0' + RootModule = 'MyModule.psm1' # Use .psm1 or .dll + FunctionsToExport = @( # Explicit list + 'Get-MyFunction' + 'Set-MyFunction' + ) + CmdletsToExport = @() +} +``` + +### Example 6: Array Types + +#### Wrong + +```powershell +# Disallowed type in array +param([System.Net.WebClient[]]$Clients) +``` + +#### Correct + +```powershell +# Allowed types in arrays are fine +param([string[]]$Names) +param([int[]]$Numbers) +param([hashtable[]]$Configuration) +``` + +## Detailed Restrictions + +### 1. Add-Type +`Add-Type` allows compiling arbitrary C# code and is not permitted in CLM. + +**Enforced For**: Unsigned scripts only + +### 2. COM Objects +Only three COM objects are allowed: +- `Scripting.Dictionary` +- `Scripting.FileSystemObject` +- `VBScript.RegExp` + +All others (Excel.Application, WScript.Shell, etc.) are flagged. + +**Enforced For**: Unsigned scripts only + +### 3. .NET Types +Only ~70 allowed types including: +- Primitives: `string`, `int`, `bool`, `byte`, `char`, `datetime`, `decimal`, `double`, etc. +- Collections: `hashtable`, `array`, `arraylist` +- PowerShell: `pscredential`, `psobject`, `securestring` +- Utilities: `regex`, `guid`, `version`, `uri`, `xml` +- Arrays: `string[]`, `int[][]`, etc. (array of any allowed type) + +The rule checks type usage in: +- Parameter type constraints (**always enforced, even for signed scripts**) +- Variable type constraints +- New-Object -TypeName +- Type expressions (`[Type]::Method()`) +- Type casts (`[Type]$variable`) +- Member invocations on typed variables + +**Enforced For**: Parameter constraints always; others unsigned only + +### 4. PowerShell Classes +The `class` keyword is not permitted. Use `New-Object PSObject` with `Add-Member` or hashtables. + +**Note**: `[PSCustomObject]@{}` is also not allowed because it uses type casting. + +**Enforced For**: Unsigned scripts only + +### 5. XAML/WPF +XAML and WPF are not permitted in CLM. + +**Enforced For**: Unsigned scripts only + +### 6. Invoke-Expression +`Invoke-Expression` is restricted in CLM. + +**Enforced For**: Unsigned scripts only + +### 7. Dot-Sourcing +Dot-sourcing (`. $PSScriptRoot\script.ps1`) may be restricted depending on source location. + +**Enforced For**: ALL scripts (unsigned and signed) + +### 8. Module Manifest Best Practices + +#### Wildcard Exports +Don't use `*` in: `FunctionsToExport`, `CmdletsToExport`, `AliasesToExport`, `VariablesToExport` + +Use explicit lists for security and clarity. + +**Enforced For**: ALL .psd1 files (unsigned and signed) + +#### Script Module Files +Don't use `.ps1` files in: `RootModule`, `ModuleToProcess`, `NestedModules` + +Use `.psm1` (script modules) or `.dll` (binary modules) for better performance and compatibility. + +**Enforced For**: ALL .psd1 files (unsigned and signed) + +## More Information + +- [About Language Modes](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes) +- [PowerShell Constrained Language Mode](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/) +- [PowerShell Module Function Export in Constrained Language](https://devblogs.microsoft.com/powershell/powershell-module-function-export-in-constrained-language/) +- [PowerShell Constrained Language Mode and the Dot-Source Operator](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode-and-the-dot-source-operator/) From f05704df81b2aca17dc027ee39b3fce106d418fc Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:54:19 -0700 Subject: [PATCH 121/130] v1.25.0: Thanks to all the new contributors! (#2169) * v1.25.0: Thanks to all the new contributors! * Add internal metadata --- CHANGELOG.MD | 41 ++++++++++++++++++++++++++++++++ Directory.Build.props | 2 +- docs/Cmdlets/PSScriptAnalyzer.md | 2 +- es-metadata.yml | 12 ++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 es-metadata.yml diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 76352e7c7..16dc799c1 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,46 @@ # CHANGELOG +## [1.25.0](https://github.com/PowerShell/PSScriptAnalyzer/releases/tag/1.25.0) + +### What's Changed +* Add configuration instructions for UseCorrectCasing (again) by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/2090 +* Fix for PSUseConsistantWhiteSpace when using statement is present by @AlexandraDorey in https://github.com/PowerShell/PSScriptAnalyzer/pull/2091 +* Exclude PSNativeCommandArgumentPassing variable by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2093 +* Update version check in PSScriptAnalyzer.psm1 to align with SMA version for PowerShell 7 by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2107 +* Change CommandInfo lookup for commands in the form `module\cmdletName` by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2125 +* PSAvoidDefaultValueForMandatoryParameter: Fix param block and parameter set handling by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2121 +* Add AvoidReservedWordsAsFunctionNames Rule by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2128 +* Update .NET SDK, PowerShell SDK, Newtonsoft.Json and codeowners by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2143 +* Preserve braced member access in `UseConsistentWhitespace` by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2140 +* Update docs and diagnostic message for UseCorrectCasing by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2138 +* Fix SuppressMessage CustomRule by @HeyItsGilbert in https://github.com/PowerShell/PSScriptAnalyzer/pull/2142 +* Sync rules docs from docs repo by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/2144 +* Allow contributors to run CI manually by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2153 +* Remove Conditional Compilation and Runtime Checks for v3 or v4 by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2150 +* Add UseConsistentParametersKind rule by @Haimasker in https://github.com/PowerShell/PSScriptAnalyzer/pull/2149 +* Fix typos in resource strings and associated C# references by @casuffitsharp in https://github.com/PowerShell/PSScriptAnalyzer/pull/2163 +* Optimise LINQ queries by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2160 +* AlignAssignmentStatement overhaul to fix issues and include handing of Enums. by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2132 +* Dynamically count expected rules in GetScriptAnalyzerRule test by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2167 +* Add UseConsistentParameterSetName Rule by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2124 +* feat(tests): added closing bracket test for formatting presets by @o-l-a-v in https://github.com/PowerShell/PSScriptAnalyzer/pull/2161 +* Add UseSingleValueFromPipelineParameter Rule by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2119 +* Added user-friendly installation instructions by @admercs in https://github.com/PowerShell/PSScriptAnalyzer/pull/2141 +* Update .NET SDK, PowerShell SDK dependencies by @Copilot in https://github.com/PowerShell/PSScriptAnalyzer/pull/2168 +* Add Optional PSUseConstrainedLanguageMode rule by @joshcorr in https://github.com/PowerShell/PSScriptAnalyzer/pull/2165 + +### New Contributors +* @AlexandraDorey made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2091 +* @HeyItsGilbert made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2142 +* @Haimasker made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2149 +* @casuffitsharp made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2163 +* @o-l-a-v made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2161 +* @admercs made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2141 +* @Copilot made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2168 +* @joshcorr made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2165 + +**Full Changelog**: https://github.com/PowerShell/PSScriptAnalyzer/compare/1.24.0...1.25.0 + ## [1.24.0](https://github.com/PowerShell/PSScriptAnalyzer/releases/tag/1.24.0) ### What's Changed diff --git a/Directory.Build.props b/Directory.Build.props index f7d809c1d..a9ac6cc61 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.24.0 + 1.25.0 true diff --git a/docs/Cmdlets/PSScriptAnalyzer.md b/docs/Cmdlets/PSScriptAnalyzer.md index 0af4bf2b5..3a5bc1d0a 100644 --- a/docs/Cmdlets/PSScriptAnalyzer.md +++ b/docs/Cmdlets/PSScriptAnalyzer.md @@ -1,6 +1,6 @@ --- Download Help Link: https://aka.ms/ps-modules-help -Help Version: 1.24.0 +Help Version: 1.25.0 Locale: en-US Module Guid: d6245802-193d-4068-a631-8863a4342a18 Module Name: PSScriptAnalyzer diff --git a/es-metadata.yml b/es-metadata.yml new file mode 100644 index 000000000..e4dc3924a --- /dev/null +++ b/es-metadata.yml @@ -0,0 +1,12 @@ +schemaVersion: 1.0.0 +providers: +- provider: InventoryAsCode + version: 1.0.0 + metadata: + isProduction: true + accountableOwners: + service: cef1de07-99d6-45df-b907-77d0066032ec + routing: + defaultAreaPath: + org: msazure + path: One\MGMT\Compute\Powershell\Powershell From 9b55ac29a91e6de07defa93bdf9f417b1468f66b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:51:29 -0700 Subject: [PATCH 122/130] Update copyright year (#2171) --- Engine/PSScriptAnalyzer.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 49fb93227..993677254 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -20,7 +20,7 @@ GUID = 'd6245802-193d-4068-a631-8863a4342a18' CompanyName = 'Microsoft Corporation' # Copyright statement for this module -Copyright = '(c) Microsoft Corporation 2025. All rights reserved.' +Copyright = '(c) Microsoft Corporation 2026. All rights reserved.' # Description of the functionality provided by this module Description = 'PSScriptAnalyzer provides script analysis and checks for potential code defects in the scripts by applying a group of built-in or customized rules on the scripts being analyzed.' From a143b9ff8b54ccbae1224c593bd78b6f46579b14 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Thu, 2 Apr 2026 11:00:32 -0500 Subject: [PATCH 123/130] Sync docs edits from docs repo (#2170) * Sync docs edits from docs repo * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/Rules/AlignAssignmentStatement.md | 50 ++++----- docs/Rules/AvoidLongLines.md | 6 +- docs/Rules/README.md | 2 +- docs/Rules/UseConsistentParameterSetName.md | 41 ++++--- docs/Rules/UseConsistentParametersKind.md | 54 ++++++---- docs/Rules/UseConstrainedLanguageMode.md | 100 ++++++++++++------ .../UseSingleValueFromPipelineParameter.md | 40 ++++--- 7 files changed, 171 insertions(+), 122 deletions(-) diff --git a/docs/Rules/AlignAssignmentStatement.md b/docs/Rules/AlignAssignmentStatement.md index 28ca9f47f..573b54b68 100644 --- a/docs/Rules/AlignAssignmentStatement.md +++ b/docs/Rules/AlignAssignmentStatement.md @@ -1,6 +1,6 @@ --- description: Align assignment statement -ms.date: 06/28/2023 +ms.date: 03/20/2026 ms.topic: reference title: AlignAssignmentStatement --- @@ -10,14 +10,13 @@ title: AlignAssignmentStatement ## Description -Consecutive assignment statements are more readable when they're aligned. -Assignments are considered aligned when their `equals` signs line up vertically. +Consecutive assignment statements are more readable when they're aligned. Assignments are considered +aligned when their `equals` signs line up vertically. -This rule looks at the key-value pairs in hashtables (including DSC -configurations) as well as enum definitions. +This rule looks at the key-value pairs in hashtables (including DSC configurations) as well as enum +definitions. -Consider the following example which has a hashtable and enum which are not -aligned. +Consider the following example with a hashtable and enum that isn't aligned. ```powershell $hashtable = @{ @@ -45,8 +44,8 @@ enum Enum { } ``` -The rule ignores any assignments within hashtables and enums which are on the -same line as others. For example, the rule ignores `$h = @{a = 1; b = 2}`. +The rule ignores any assignments within hashtables and enums which are on the same line as others. +For example, the rule ignores `$h = @{a = 1; b = 2}`. ## Configuration @@ -71,15 +70,14 @@ Enable or disable the rule during ScriptAnalyzer invocation. #### CheckHashtable: bool (Default value is `$true`) -Enforce alignment of assignment statements in a hashtable and in a DSC -Configuration. There is only one setting for hashtable and DSC configuration -because the property value pairs in a DSC configuration are parsed as key-value -pairs of a hashtable. +Enforce alignment of assignment statements in a hashtable and in a DSC Configuration. There is only +one setting for hashtable and DSC configuration because the property value pairs in a DSC +configuration are parsed as key-value pairs of a hashtable. #### AlignHashtableKvpWithInterveningComment: bool (Default value is `$true`) -Include key-value pairs in the alignment that have an intervening comment - that -is to say a comment between the key name and the equals sign. +Include key-value pairs in the alignment that have an intervening comment - that is to say a comment +between the key name and the equals sign. Consider the following: @@ -91,8 +89,7 @@ $hashtable = @{ } ``` -With this setting disabled, the line with the comment is ignored, and it would -be aligned like so: +With this setting disabled, the line with the comment is ignored, and it would be aligned like so: ```powershell $hashtable = @{ @@ -118,8 +115,8 @@ Enforce alignment of assignment statements of an Enum definition. #### AlignEnumMemberWithInterveningComment: bool (Default value is `$true`) -Include enum members in the alignment that have an intervening comment - that -is to say a comment between the member name and the equals sign. +Include enum members in the alignment that have an intervening comment - that is to say a comment +between the member name and the equals sign. Consider the following: @@ -131,8 +128,7 @@ enum Enum { } ``` -With this setting disabled, the line with the comment is ignored, and it would -be aligned like so: +With this setting disabled, the line with the comment is ignored, and it would be aligned like so: ```powershell enum Enum { @@ -154,9 +150,8 @@ enum Enum { #### IncludeValuelessEnumMembers: bool (Default value is `$true`) -Include enum members in the alignment that don't have an initial value - that -is to say they don't have an equals sign. Enum's don't need to be given a value -when they're defined. +Include enum members in the alignment that don't have an explicitly assigned value. Enums don't +need to be given a value when they're defined. Consider the following: @@ -168,8 +163,8 @@ enum Enum { } ``` -With this setting disabled the third line which has no value is not considered -when choosing where to align assignments. It would be aligned like so: +With this setting disabled, the third line, which has no value, isn't considered when choosing where +to align assignments. It would be aligned like so: ```powershell enum Enum { @@ -179,8 +174,7 @@ enum Enum { } ``` -With it enabled, the valueless member is included in alignment as if it had a -value: +With it enabled, the valueless member is included in alignment as if it had a value: ```powershell enum Enum { diff --git a/docs/Rules/AvoidLongLines.md b/docs/Rules/AvoidLongLines.md index c36daaa86..89474c8b8 100644 --- a/docs/Rules/AvoidLongLines.md +++ b/docs/Rules/AvoidLongLines.md @@ -1,6 +1,6 @@ --- description: Avoid long lines -ms.date: 04/29/2025 +ms.date: 03/20/2026 ms.topic: reference title: AvoidLongLines --- @@ -10,8 +10,8 @@ title: AvoidLongLines ## Description -The length of lines, including leading spaces (indentation), should be less than the configured number -of characters. The default length is 120 characters. +The length of lines, including leading spaces (indentation), should be less than the configured +number of characters. The default length is 120 characters. > [!NOTE] > This rule isn't enabled by default. The user needs to enable it through settings. diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 5df834708..fca031e33 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -1,6 +1,6 @@ --- description: List of PSScriptAnalyzer rules -ms.date: 03/27/2024 +ms.date: 03/20/2026 ms.topic: reference title: List of PSScriptAnalyzer rules --- diff --git a/docs/Rules/UseConsistentParameterSetName.md b/docs/Rules/UseConsistentParameterSetName.md index 5ad33eb22..6e9a4598f 100644 --- a/docs/Rules/UseConsistentParameterSetName.md +++ b/docs/Rules/UseConsistentParameterSetName.md @@ -1,6 +1,6 @@ --- description: Use consistent parameter set names and proper parameter set configuration. -ms.date: 08/19/2025 +ms.date: 03/20/2026 ms.topic: reference title: UseConsistentParameterSetName --- @@ -11,18 +11,25 @@ title: UseConsistentParameterSetName ## Description -Parameter set names in PowerShell are case-sensitive, unlike most other PowerShell elements. This rule ensures consistent casing and proper configuration of parameter sets to avoid runtime errors and improve code clarity. +Parameter set names in PowerShell are case-sensitive, unlike most other PowerShell elements. This +rule ensures consistent casing and proper configuration of parameter sets to avoid runtime errors +and improve code clarity. The rule performs five different checks: -1. **Missing DefaultParameterSetName** - Warns when parameter sets are used but no default is specified -2. **Multiple parameter declarations** - Detects when a parameter is declared multiple times in the same parameter set. This is ultimately a runtime exception - this check helps catch it sooner. -3. **Case mismatch between DefaultParameterSetName and ParameterSetName** - Ensures consistent casing -4. **Case mismatch between different ParameterSetName values** - Ensures all references to the same parameter set use identical casing -5. **Parameter set names containing newlines** - Warns against using newline characters in parameter set names +1. **Missing DefaultParameterSetName** - Warns when parameter sets are used but no default is + specified +1. **Multiple parameter declarations** - Detects when a parameter is declared multiple times in the + same parameter set. This is ultimately a runtime exception - this check helps catch it sooner. +1. **Case mismatch between DefaultParameterSetName and ParameterSetName** - Ensures consistent + casing +1. **Case mismatch between different ParameterSetName values** - Ensures all references to the same + parameter set use identical casing +1. **Parameter set names containing newlines** - Warns against using newline characters in parameter + set names > [!NOTE] -> This rule is not enabled by default. The user needs to enable it through settings. +> This rule isn't enabled by default. The user needs to enable it through settings. ## How @@ -43,7 +50,7 @@ function Get-Data { param( [Parameter(ParameterSetName='ByName')] [string]$Name, - + [Parameter(ParameterSetName='ByID')] [int]$ID ) @@ -55,7 +62,7 @@ function Get-Data { param( [Parameter(ParameterSetName='byname')] [string]$Name, - + [Parameter(ParameterSetName='ByID')] [int]$ID ) @@ -67,7 +74,7 @@ function Get-Data { param( [Parameter(ParameterSetName='ByName')] [string]$Name, - + [Parameter(ParameterSetName='byname')] [string]$DisplayName ) @@ -100,11 +107,11 @@ function Get-Data { param( [Parameter(ParameterSetName='ByName', Mandatory)] [string]$Name, - + [Parameter(ParameterSetName='ByName')] [Parameter(ParameterSetName='ByID')] [string]$ComputerName, - + [Parameter(ParameterSetName='ByID', Mandatory)] [int]$ID ) @@ -129,7 +136,9 @@ Rules = @{ ## Notes -- Parameter set names are case-sensitive in PowerShell, making this different from most other PowerShell elements +- Parameter set names are case-sensitive in PowerShell, making this different from most other + PowerShell elements - The first occurrence of a parameter set name in your code is treated as the canonical casing -- Parameters without [Parameter()] attributes are automatically part of all parameter sets -- It's a PowerShell best practice to always specify a DefaultParameterSetName when using parameter sets \ No newline at end of file +- Parameters without `[Parameter()]` attributes are automatically part of all parameter sets +- It's a PowerShell best practice to always specify a `DefaultParameterSetName` when using parameter + sets \ No newline at end of file diff --git a/docs/Rules/UseConsistentParametersKind.md b/docs/Rules/UseConsistentParametersKind.md index 04a323b3d..20a470f30 100644 --- a/docs/Rules/UseConsistentParametersKind.md +++ b/docs/Rules/UseConsistentParametersKind.md @@ -1,26 +1,36 @@ +--- +description: Use the same pattern when defining parameters. +ms.date: 03/20/2026 +ms.topic: reference +title: UseConsistentParametersKind +--- # UseConsistentParametersKind **Severity Level: Warning** ## Description -All functions should have same parameters definition kind specified in the rule. -Possible kinds are: -1. `Inline`, i.e.: -```PowerShell -function f([Parameter()]$FirstParam) { - return -} -``` -2. `ParamBlock`, i.e.: -```PowerShell -function f { - param([Parameter()]$FirstParam) - return -} -``` +All functions should use the same pattern when defining parameters. Possible pattern types are: -* For information: in simple scenarios both function definitions above may be considered as equal. Using this rule as-is is more for consistent code-style than functional, but it can be useful in combination with other rules. +1. `Inline` + + ```powershell + function f([Parameter()]$FirstParam) { + return + } + ``` + +1. `ParamBlock` + + ```powershell + function f { + param([Parameter()]$FirstParam) + return + } + ``` + +In simple scenarios, both function definitions shown are considered to be equal. The purpose of this +rule is to enforce consistent code style across the codebase. ## How to Fix @@ -28,8 +38,9 @@ Rewrite function so it defines parameters as specified in the rule ## Example -### When the rule sets parameters definition kind to 'Inline': -```PowerShell +When the rule sets parameters definition kind to `Inline`: + +```powershell # Correct function f([Parameter()]$FirstParam) { return @@ -42,9 +53,10 @@ function g { } ``` -### When the rule sets parameters definition kind to 'ParamBlock': -```PowerShell -# Inorrect +When the rule sets parameters definition kind to `ParamBlock`: + +```powershell +# Incorrect function f([Parameter()]$FirstParam) { return } diff --git a/docs/Rules/UseConstrainedLanguageMode.md b/docs/Rules/UseConstrainedLanguageMode.md index 50ccfd0e2..e12476531 100644 --- a/docs/Rules/UseConstrainedLanguageMode.md +++ b/docs/Rules/UseConstrainedLanguageMode.md @@ -1,6 +1,6 @@ --- description: Use patterns compatible with Constrained Language Mode -ms.date: 03/17/2026 +ms.date: 03/20/2026 ms.topic: reference title: UseConstrainedLanguageMode --- @@ -10,22 +10,31 @@ title: UseConstrainedLanguageMode ## Description -This rule identifies PowerShell patterns that are restricted or not permitted in Constrained Language Mode (CLM). +This rule identifies PowerShell patterns that are restricted or not permitted in Constrained +Language Mode (CLM). Constrained Language Mode is a PowerShell security feature that restricts: + - .NET types that can be used - COM objects that can be instantiated - Commands that can be executed - Language features that can be used CLM is commonly used in: + - Application Control environments (Application Control for Business, AppLocker) - Just Enough Administration (JEA) endpoints - Secure environments requiring additional PowerShell restrictions -**Signed Script Behavior**: Digitally signed scripts from trusted publishers execute in Full Language Mode (FLM) even in CLM environments. The rule detects signature blocks (`# SIG # Begin signature block`) and adjusts checks accordingly - most restrictions don't apply to signed scripts, but certain checks (dot-sourcing, parameter types, manifest best practices) are always enforced. +Digitally signed scripts from trusted publishers execute in Full Language Mode (FLM) even in CLM +environments. The rule detects signature blocks (`# SIG # Begin signature block`) and adjusts checks +accordingly. Most restrictions don't apply to signed scripts, but certain checks (dot-sourcing, +parameter types, manifest best practices) are always enforced. -**Important**: The rule performs a simple text check for signature blocks and does NOT validate signature authenticity or certificate trust. Actual signature validation is performed by PowerShell at runtime. +> [!IMPORTANT] +> The rule performs a simple text check for signature blocks and does NOT validate signature +> authenticity or certificate trust. Actual signature validation is performed by PowerShell at +> runtime. ## Constrained Language Mode Restrictions @@ -34,24 +43,26 @@ CLM is commonly used in: The following are flagged for unsigned scripts: 1. **Add-Type** - Code compilation not permitted -2. **Disallowed COM Objects** - Only Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp allowed -3. **Disallowed .NET Types** - Only ~70 allowed types (string, int, hashtable, pscredential, etc.) -4. **Type Constraints** - On parameters and variables -5. **Type Expressions** - Static type references like `[Type]::Method()` -6. **Type Casts** - Converting to disallowed types -7. **Member Invocations** - Methods/properties on disallowed types -8. **PowerShell Classes** - `class` keyword not permitted -9. **XAML/WPF** - Not permitted -10. **Invoke-Expression** - Restricted -11. **Dot-Sourcing** - May be restricted depending on the file being sourced -12. **Module Manifest Wildcards** - Wildcard exports not recommended -13. **Module Manifest .ps1 Files** - Script modules ending with .ps1 not allowed +1. **Disallowed COM Objects** - Only Scripting.Dictionary, Scripting.FileSystemObject, + VBScript.RegExp allowed +1. **Disallowed .NET Types** - Only ~70 allowed types (string, int, hashtable, pscredential, etc.) +1. **Type Constraints** - On parameters and variables +1. **Type Expressions** - Static type references like `[Type]::Method()` +1. **Type Casts** - Converting to disallowed types +1. **Member Invocations** - Methods/properties on disallowed types +1. **PowerShell Classes** - `class` keyword not permitted +1. **XAML/WPF** - Not permitted +1. **Invoke-Expression** - Restricted +1. **Dot-Sourcing** - May be restricted depending on the file being sourced +1. **Module Manifest Wildcards** - Wildcard exports not recommended +1. **Module Manifest .ps1 Files** - Script modules ending with .ps1 not allowed Always enforced, even for signed scripts ### Signed Scripts (Selective Checking) For scripts with signature blocks, only these are checked: + - Dot-sourcing - Parameter type constraints - Module manifest wildcards (.psd1 files) @@ -75,14 +86,17 @@ For scripts with signature blocks, only these are checked: #### Enable: bool (Default value is `$false`) -Enable or disable the rule during ScriptAnalyzer invocation. This rule is disabled by default because not all scripts need CLM compatibility. +Enable or disable the rule during ScriptAnalyzer invocation. This rule is disabled by default +because not all scripts need CLM compatibility. #### IgnoreSignatures: bool (Default value is `$false`) Control signature detection behavior: -- `$false` (default): Automatically detect signatures. Signed scripts get selective checking, unsigned get full checking. -- `$true`: Bypass signature detection. ALL scripts get full CLM checking regardless of signature status. +- `$false` (default): Automatically detect signatures. Signed scripts get selective checking, + unsigned get full checking. +- `$true`: Bypass signature detection. ALL scripts get full CLM checking regardless of signature + status. ```powershell @{ @@ -95,7 +109,8 @@ Control signature detection behavior: } ``` -**Use `IgnoreSignatures = $true` when:** +Use `IgnoreSignatures = $true` when: + - Auditing signed scripts for complete CLM compatibility - Preparing scripts for untrusted environments - Enforcing strict CLM compliance organization-wide @@ -109,17 +124,20 @@ Use allowed cmdlets or pre-compile assemblies. ### Replace Disallowed COM Objects -Use only allowed COM objects (Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp) or PowerShell cmdlets. +Use only allowed COM objects (Scripting.Dictionary, Scripting.FileSystemObject, VBScript.RegExp) or +PowerShell cmdlets. ### Replace Disallowed Types -Use allowed type accelerators (`[string]`, `[int]`, `[hashtable]`, etc.) or allowed cmdlets instead of disallowed .NET types. +Use allowed type accelerators (`[string]`, `[int]`, `[hashtable]`, etc.) or allowed cmdlets instead +of disallowed .NET types. ### Replace PowerShell Classes Use `New-Object PSObject` with `Add-Member` or hashtables instead of classes. -**Important**: `[PSCustomObject]@{}` syntax is NOT allowed in CLM because it uses type casting. +> [!IMPORTANT] +> `[PSCustomObject]@{}` syntax is NOT allowed in CLM because it uses type casting. ### Avoid XAML @@ -135,9 +153,9 @@ Use modules with Import-Module instead of dot-sourcing when possible. ### Fix Module Manifests -- Replace wildcard exports (`*`) with explicit lists -- Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules -- Don't use ScriptsToProcess as it loads in the caller's scope and will be blocked. +- Replace wildcard exports (`*`) with explicit lists. +- Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules. +- Don't use `ScriptsToProcess`. These scripts are loaded in the caller's scope and are blocked. ## Examples @@ -219,7 +237,7 @@ function Process-Text { ```powershell class MyClass { [string]$Name - + [string]GetInfo() { return $this.Name } @@ -300,12 +318,15 @@ param([hashtable[]]$Configuration) ## Detailed Restrictions ### 1. Add-Type -`Add-Type` allows compiling arbitrary C# code and is not permitted in CLM. + +`Add-Type` allows compiling arbitrary C# code and isn't permitted in CLM. **Enforced For**: Unsigned scripts only ### 2. COM Objects + Only three COM objects are allowed: + - `Scripting.Dictionary` - `Scripting.FileSystemObject` - `VBScript.RegExp` @@ -315,7 +336,9 @@ All others (Excel.Application, WScript.Shell, etc.) are flagged. **Enforced For**: Unsigned scripts only ### 3. .NET Types + Only ~70 allowed types including: + - Primitives: `string`, `int`, `bool`, `byte`, `char`, `datetime`, `decimal`, `double`, etc. - Collections: `hashtable`, `array`, `arraylist` - PowerShell: `pscredential`, `psobject`, `securestring` @@ -323,6 +346,7 @@ Only ~70 allowed types including: - Arrays: `string[]`, `int[][]`, etc. (array of any allowed type) The rule checks type usage in: + - Parameter type constraints (**always enforced, even for signed scripts**) - Variable type constraints - New-Object -TypeName @@ -333,6 +357,7 @@ The rule checks type usage in: **Enforced For**: Parameter constraints always; others unsigned only ### 4. PowerShell Classes + The `class` keyword is not permitted. Use `New-Object PSObject` with `Add-Member` or hashtables. **Note**: `[PSCustomObject]@{}` is also not allowed because it uses type casting. @@ -340,16 +365,19 @@ The `class` keyword is not permitted. Use `New-Object PSObject` with `Add-Member **Enforced For**: Unsigned scripts only ### 5. XAML/WPF + XAML and WPF are not permitted in CLM. **Enforced For**: Unsigned scripts only ### 6. Invoke-Expression + `Invoke-Expression` is restricted in CLM. **Enforced For**: Unsigned scripts only ### 7. Dot-Sourcing + Dot-sourcing (`. $PSScriptRoot\script.ps1`) may be restricted depending on source location. **Enforced For**: ALL scripts (unsigned and signed) @@ -357,6 +385,7 @@ Dot-sourcing (`. $PSScriptRoot\script.ps1`) may be restricted depending on sourc ### 8. Module Manifest Best Practices #### Wildcard Exports + Don't use `*` in: `FunctionsToExport`, `CmdletsToExport`, `AliasesToExport`, `VariablesToExport` Use explicit lists for security and clarity. @@ -364,6 +393,7 @@ Use explicit lists for security and clarity. **Enforced For**: ALL .psd1 files (unsigned and signed) #### Script Module Files + Don't use `.ps1` files in: `RootModule`, `ModuleToProcess`, `NestedModules` Use `.psm1` (script modules) or `.dll` (binary modules) for better performance and compatibility. @@ -372,7 +402,13 @@ Use `.psm1` (script modules) or `.dll` (binary modules) for better performance a ## More Information -- [About Language Modes](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes) -- [PowerShell Constrained Language Mode](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/) -- [PowerShell Module Function Export in Constrained Language](https://devblogs.microsoft.com/powershell/powershell-module-function-export-in-constrained-language/) -- [PowerShell Constrained Language Mode and the Dot-Source Operator](https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode-and-the-dot-source-operator/) +- [About Language Modes][01] +- [PowerShell Constrained Language Mode][03] +- [PowerShell Module Function Export in Constrained Language][04] +- [PowerShell Constrained Language Mode and the Dot-Source Operator][02] + + +[01]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes +[02]: https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode-and-the-dot-source-operator/ +[03]: https://devblogs.microsoft.com/powershell/powershell-constrained-language-mode/ +[04]: https://devblogs.microsoft.com/powershell/powershell-module-function-export-in-constrained-language/ diff --git a/docs/Rules/UseSingleValueFromPipelineParameter.md b/docs/Rules/UseSingleValueFromPipelineParameter.md index bfaa3fe6a..92c350eb4 100644 --- a/docs/Rules/UseSingleValueFromPipelineParameter.md +++ b/docs/Rules/UseSingleValueFromPipelineParameter.md @@ -1,6 +1,6 @@ --- description: Use at most a single ValueFromPipeline parameter per parameter set. -ms.date: 08/08/2025 +ms.date: 03/20/2026 ms.topic: reference title: UseSingleValueFromPipelineParameter --- @@ -10,18 +10,15 @@ title: UseSingleValueFromPipelineParameter ## Description -Parameter sets should have at most one parameter marked as -`ValueFromPipeline = true`. +Parameter sets should have at most one parameter marked as `ValueFromPipeline = true`. -This rule identifies functions where multiple parameters within the same -parameter set have `ValueFromPipeline` set to `true` (either explicitly or -implicitly). +This rule identifies functions where multiple parameters within the same parameter set have +`ValueFromPipeline` set to `true` (either explicitly or implicitly). ## How -Ensure that only one parameter per parameter set accepts pipeline input by -value. If you need multiple parameters to accept different types of pipeline -input, use separate parameter sets. +Ensure that only one parameter per parameter set accepts pipeline input by value. If you need +multiple parameters to accept different types of pipeline input, use separate parameter sets. ## Example @@ -33,11 +30,11 @@ function Process-Data { param( [Parameter(ValueFromPipeline)] [string] $InputData, - + [Parameter(ValueFromPipeline)] [string] $ProcessingMode ) - + process { Write-Output "$ProcessingMode`: $InputData" } @@ -53,7 +50,7 @@ function Process-Data { param( [Parameter(ValueFromPipeline)] [string] $InputData, - + [Parameter(Mandatory)] [string] $ProcessingMode ) @@ -62,10 +59,11 @@ function Process-Data { } } ``` + ## Suppression -To suppress this rule for a specific parameter set, use the `SuppressMessage` -attribute with the parameter set name: +To suppress this rule for a specific parameter set, use the `SuppressMessage` attribute with the +parameter set name: ```powershell function Process-Data { @@ -74,7 +72,7 @@ function Process-Data { param( [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] [string] $InputData, - + [Parameter(ValueFromPipeline, ParameterSetName='MyParameterSet')] [string] $ProcessingMode ) @@ -92,10 +90,10 @@ For the default parameter set, use `'default'` as the suppression target: ## Notes -- This rule applies to both explicit `ValueFromPipeline = $true` and implicit - `ValueFromPipeline` (which is the same as using `= $true`) +- This rule applies to both explicit `ValueFromPipeline = $true` and implicit `ValueFromPipeline` + (which is the same as using `= $true`) - Parameters with `ValueFromPipeline=$false` are not flagged by this rule -- The rule correctly handles the default parameter set (`__AllParameterSets`) - and named parameter sets -- Different parameter sets can each have their own single `ValueFromPipeline` - parameter without triggering this rule +- The rule correctly handles the default parameter set (`__AllParameterSets`) and named parameter + sets +- Different parameter sets can each have their own single `ValueFromPipeline` parameter without + triggering this rule From d97ddd4862eaa0e5981d10fcfec7df957232e9bc Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Mon, 4 May 2026 21:24:36 +0100 Subject: [PATCH 124/130] Improve pipeline indentation handling in UseConsistentIndentation rule (#2173) * Improve pipeline indentation handling in UseConsistentIndentation rule * Fix over-indentation when multiple openers appear on the same line --- Rules/UseConsistentIndentation.cs | 185 +++++-- .../Rules/UseConsistentIndentation.tests.ps1 | 480 ++++++++++++++++++ 2 files changed, 626 insertions(+), 39 deletions(-) diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index 41aa4ef4d..2c77787c6 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -130,16 +130,27 @@ public override IEnumerable AnalyzeScript(Ast ast, string file var tokens = Helper.Instance.Tokens; var diagnosticRecords = new List(); var indentationLevel = 0; - var currentIndenationLevelIncreaseDueToPipelines = 0; var onNewLine = true; var pipelineAsts = ast.FindAll(testAst => testAst is PipelineAst && (testAst as PipelineAst).PipelineElements.Count > 1, true).ToList(); - /* - When an LParen and LBrace are on the same line, it can lead to too much de-indentation. - In order to prevent the RParen code from de-indenting too much, we keep a stack of when we skipped the indentation - caused by tokens that require a closing RParen (which are LParen, AtParen and DollarParen). - */ - var lParenSkippedIndentation = new Stack(); - + // Sort by end position so that inner (nested) pipelines appear before outer ones. + // This is required by MatchingPipelineAstEnd, whose early-break optimization + // would otherwise skip nested pipelines that end before their outer pipeline. + pipelineAsts.Sort((a, b) => + { + int lineCmp = a.Extent.EndScriptPosition.LineNumber.CompareTo(b.Extent.EndScriptPosition.LineNumber); + return lineCmp != 0 ? lineCmp : a.Extent.EndScriptPosition.ColumnNumber.CompareTo(b.Extent.EndScriptPosition.ColumnNumber); + }); + // Track pipeline indentation increases per PipelineAst instead of as a single + // flat counter. A flat counter caused all accumulated pipeline indentation to be + // subtracted when *any* pipeline ended, instead of only the contribution from + // that specific pipeline - leading to runaway indentation with nested pipelines. + var pipelineIndentationIncreases = new Dictionary(); + // When multiple openers appear on the same line (e.g. ({ or @(@{), + // only the last unclosed opener should affect indentation. We + // track, for every opener, whether its indentation increment was + // skipped so that the matching closer knows not to decrement. + var openerSkippedIndentation = new Stack(); + for (int tokenIndex = 0; tokenIndex < tokens.Length; tokenIndex++) { var token = tokens[tokenIndex]; @@ -153,27 +164,39 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do { case TokenKind.AtCurly: case TokenKind.LCurly: - AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - break; - case TokenKind.DollarParen: case TokenKind.AtParen: - lParenSkippedIndentation.Push(false); - AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); + if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex)) + { + openerSkippedIndentation.Push(true); + } + else + { + indentationLevel++; + openerSkippedIndentation.Push(false); + } break; case TokenKind.LParen: AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); - // When a line starts with a parenthesis and it is not the last non-comment token of that line, - // then indentation does not need to be increased. + // When a line starts with a parenthesis and it is not the + // last non-comment token of that line, indentation does + // not need to be increased. if ((tokenIndex == 0 || tokens[tokenIndex - 1].Kind == TokenKind.NewLine) && NextTokenIgnoringComments(tokens, tokenIndex)?.Kind != TokenKind.NewLine) { - onNewLine = false; - lParenSkippedIndentation.Push(true); + openerSkippedIndentation.Push(true); break; } - lParenSkippedIndentation.Push(false); + // General case: skip when another opener follows so that + // only the last unclosed opener on a line is indent-affecting. + if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex)) + { + openerSkippedIndentation.Push(true); + break; + } + openerSkippedIndentation.Push(false); indentationLevel++; break; @@ -188,40 +211,50 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline) { AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - currentIndenationLevelIncreaseDueToPipelines++; + // Attribute this increase to the innermost pipeline containing + // this pipe token so it is only reversed when that specific + // pipeline ends, not when an unrelated outer pipeline ends. + PipelineAst containingPipeline = FindInnermostContainingPipeline(pipelineAsts, token); + if (containingPipeline != null) + { + if (!pipelineIndentationIncreases.ContainsKey(containingPipeline)) + pipelineIndentationIncreases[containingPipeline] = 0; + pipelineIndentationIncreases[containingPipeline]++; + } break; } if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline) { - bool isFirstPipeInPipeline = pipelineAsts.Any(pipelineAst => - PositionIsEqual(LastPipeOnFirstLineWithPipeUsage((PipelineAst)pipelineAst).Extent.EndScriptPosition, - tokens[tokenIndex - 1].Extent.EndScriptPosition)); - if (isFirstPipeInPipeline) + // Capture which specific PipelineAst this is the first pipe for, + // so the indentation increase is attributed to that pipeline only. + PipelineAst firstPipePipeline = pipelineAsts + .Cast() + .FirstOrDefault(pipelineAst => + PositionIsEqual(LastPipeOnFirstLineWithPipeUsage(pipelineAst).Extent.EndScriptPosition, + tokens[tokenIndex - 1].Extent.EndScriptPosition)); + if (firstPipePipeline != null) { AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - currentIndenationLevelIncreaseDueToPipelines++; + if (!pipelineIndentationIncreases.ContainsKey(firstPipePipeline)) + pipelineIndentationIncreases[firstPipePipeline] = 0; + pipelineIndentationIncreases[firstPipePipeline]++; } } break; case TokenKind.RParen: - bool matchingLParenIncreasedIndentation = false; - if (lParenSkippedIndentation.Count > 0) + case TokenKind.RCurly: + if (openerSkippedIndentation.Count > 0 && openerSkippedIndentation.Pop()) { - matchingLParenIncreasedIndentation = lParenSkippedIndentation.Pop(); + // The matching opener skipped its increment, so we + // skip the decrement but still enforce indentation. + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); } - if (matchingLParenIncreasedIndentation) + else { - onNewLine = false; - break; + indentationLevel = ClipNegative(indentationLevel - 1); + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); } - indentationLevel = ClipNegative(indentationLevel - 1); - AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); - break; - - case TokenKind.RCurly: - indentationLevel = ClipNegative(indentationLevel - 1); - AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); break; case TokenKind.NewLine: @@ -290,14 +323,62 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline || pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline) { - indentationLevel = ClipNegative(indentationLevel - currentIndenationLevelIncreaseDueToPipelines); - currentIndenationLevelIncreaseDueToPipelines = 0; + // Only subtract the indentation contributed by this specific pipeline, + // leaving contributions from outer/unrelated pipelines intact. + if (pipelineIndentationIncreases.TryGetValue(matchingPipeLineAstEnd, out int contribution)) + { + indentationLevel = ClipNegative(indentationLevel - contribution); + pipelineIndentationIncreases.Remove(matchingPipeLineAstEnd); + } } } return diagnosticRecords; } + /// + /// Scans forward from the current opener to the end of the line. + /// Returns true if there is at least one unclosed opener when + /// the line ends, meaning the current opener should skip its + /// indentation increment. If the current opener's own closer + /// is found on the same line (depth drops below zero), returns + /// false so that it indents normally. + /// + private static bool HasUnclosedOpenerBeforeLineEnd(Token[] tokens, int currentIndex) + { + int depth = 0; + for (int i = currentIndex + 1; i < tokens.Length; i++) + { + switch (tokens[i].Kind) + { + case TokenKind.NewLine: + case TokenKind.LineContinuation: + case TokenKind.EndOfInput: + return depth > 0; + + case TokenKind.LCurly: + case TokenKind.AtCurly: + case TokenKind.LParen: + case TokenKind.AtParen: + case TokenKind.DollarParen: + depth++; + break; + + case TokenKind.RCurly: + case TokenKind.RParen: + depth--; + if (depth < 0) + { + // Our own closer was found on this line. + return false; + } + break; + } + } + + return depth > 0; + } + private static Token NextTokenIgnoringComments(Token[] tokens, int startIndex) { if (startIndex >= tokens.Length - 1) @@ -432,6 +513,32 @@ private static PipelineAst MatchingPipelineAstEnd(List pipelineAsts, Token return matchingPipeLineAstEnd; } + /// + /// Finds the innermost (smallest) PipelineAst whose extent fully contains the given token. + /// Used to attribute pipeline indentation increases to the correct pipeline when + /// using IncreaseIndentationAfterEveryPipeline. + /// + private static PipelineAst FindInnermostContainingPipeline(List pipelineAsts, Token token) + { + PipelineAst best = null; + int bestSize = int.MaxValue; + foreach (var ast in pipelineAsts) + { + var pipeline = (PipelineAst)ast; + int pipelineStart = pipeline.Extent.StartOffset; + int pipelineEnd = pipeline.Extent.EndOffset; + int pipelineSize = pipelineEnd - pipelineStart; + if (pipelineStart <= token.Extent.StartOffset && + token.Extent.EndOffset <= pipelineEnd && + pipelineSize < bestSize) + { + best = pipeline; + bestSize = pipelineSize; + } + } + return best; + } + private static bool PositionIsEqual(IScriptPosition position1, IScriptPosition position2) { return position1.ColumnNumber == position2.ColumnNumber && diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index 0d26ff39d..6bf241116 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -549,6 +549,486 @@ foo | } } + Context "When a nested multi-line pipeline is inside a pipelined script block" { + + It "Should preserve indentation with nested pipeline using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | +ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should recover indentation after nested pipeline block using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle multiple sequential nested pipeline blocks using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle inner pipeline with 3+ elements using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | +ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle outer pipeline on same line as command using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle deeply nested pipelines (3 levels) using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$a | +ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle single-line inner pipeline inside multi-line outer pipeline using " -TestCases @( + @{ PipelineIndentation = 'IncreaseIndentationForFirstPipeline' } + @{ PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' } + @{ PipelineIndentation = 'NoIndentation' } + @{ PipelineIndentation = 'None' } + ) { + param ($PipelineIndentation) + + $idempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | Select-Object -Last 1 +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + } + + Context "When multiple openers appear on the same line" { + It "Should not double-indent for paren-then-brace: .foreach({" { + $def = @' +@('a', 'b').foreach({ + $_.ToUpper() + }) +'@ + $expected = @' +@('a', 'b').foreach({ + $_.ToUpper() +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should not double-indent for brace-then-paren: {(" { + $def = @' +@('a', 'b').foreach({( + $_.ToUpper() + )}) +'@ + $expected = @' +@('a', 'b').foreach({( + $_.ToUpper() +)}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should not double-indent for array-then-hashtable on same line: @(@{" { + $idempotentScriptDefinition = @' +$x = @(@{ + key = 'value' +}) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should not double-indent when non-opener tokens separate openers: ([PSCustomObject]@{" { + $def = @' +$list.Add([PSCustomObject]@{ + Name = "Test" + Value = 123 + }) +'@ + $expected = @' +$list.Add([PSCustomObject]@{ + Name = "Test" + Value = 123 +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should indent normally when all openers are closed on the same line" { + $idempotentScriptDefinition = @' +$list.Add([PSCustomObject]@{Name = "Test"; Value = 123}) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should handle closing brace and paren on separate lines" { + $def = @' +@('a', 'b').foreach({ + $_.ToUpper() + } + ) +'@ + $expected = @' +@('a', 'b').foreach({ + $_.ToUpper() +} +) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should handle nested .foreach({ }) calls" { + $def = @' +@(1, 2).foreach({ +@('a', 'b').foreach({ +"$_ and $_" +}) +}) +'@ + $expected = @' +@(1, 2).foreach({ + @('a', 'b').foreach({ + "$_ and $_" + }) +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should still indent each opener separately when on different lines" { + $idempotentScriptDefinition = @' +$x = @( + @{ + key = 'value' + } +) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should still indent normally for sub-expressions" { + $idempotentScriptDefinition = @' +$( + Get-Process +) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + } + Context "When tabs instead of spaces are used for indentation" { BeforeEach { $settings.Rules.PSUseConsistentIndentation.Kind = 'tab' From 940024a12dfc1f9240984104aaab8fd38c195817 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 7 May 2026 21:48:51 +0100 Subject: [PATCH 125/130] Prevent duplicate rule ID suppression errors in `-Fix` and DSC scenarios (#2181) * Enhance suppression error handling in ScriptAnalyzer and add tests for unapplied suppression errors * Skip new -Fix suppression test under Library Usage harness The `Library Usage` describe block in `LibraryUsage.tests.ps1` (only active on Windows PowerShell 5.1, since it's gated `-Skip:$IsCoreCLR`) re-runs `RuleSuppression.tests.ps1` against a hand-rolled `Invoke-ScriptAnalyzer` wrapper that drives the analyzer as a .NET library. That wrapper plugs in `PesterTestOutputWriter`, whose `WriteError` is intentionally a no-op: public void WriteError(ErrorRecord error) { // We don't write errors to avoid misleading // error messages in test output } So the unapplied-suppression `ErrorRecord` we now emit during the final `-Fix` pass never reaches `-ErrorVariable`, and `$fixErr | Should -HaveCount 1` fails with "Expected a collection with size 1, but got an empty collection". The behaviour itself is correct - the assertion is just unobservable through this test harness. Mark the new `It` block `-Skip:$testingLibraryUsage`, matching the existing pattern already used by the `Bad Rule Suppression` and `External Rule Suppression` contexts in the same file for the same reason. The regular pwsh and WinPS runs of `RuleSuppression.tests.ps1` (which `[+]` in the failing CI log) continue to exercise the assertion. The new `UseDSCResourceFunctions.tests.ps1` test isn't dot-sourced by `LibraryUsage.tests.ps1`, so it doesn't need the same guard. Drafted by Copilot (Claude Opus 4.7). * Add copilot review suggestions --------- Co-authored-by: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> --- Engine/ScriptAnalyzer.cs | 74 +++++++++++++++---- Tests/Engine/RuleSuppression.tests.ps1 | 24 ++++++ Tests/Rules/UseDSCResourceFunctions.tests.ps1 | 30 ++++++++ 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 46e267fc6..adc81f2d3 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1447,10 +1447,24 @@ public IEnumerable AnalyzeAndFixPath(string path, Func /// The script to be analyzed. /// Parsed AST of . - /// Parsed tokens of + /// Parsed tokens of . /// Whether variable analysis can be skipped (applicable if rules do not use variable analysis APIs). /// public List AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false) + { + return AnalyzeScriptDefinition(scriptDefinition, out scriptAst, out scriptTokens, skipVariableAnalysis, emitSuppressionErrors: true); + } + + /// + /// Analyzes a script definition in the form of a string input. + /// + /// The script to be analysed. + /// Parsed AST of . + /// Parsed tokens of . + /// Whether variable analysis can be skipped (applicable if rules do not use variable analysis APIs). + /// Whether to emit errors for unapplied rule suppression IDs. + /// A list of diagnostics found by rules. + public List AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis, bool emitSuppressionErrors) { scriptAst = null; scriptTokens = null; @@ -1490,7 +1504,7 @@ public List AnalyzeScriptDefinition(string scriptDefinition, o } // now, analyze the script definition - diagnosticRecords.AddRange(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, null, skipVariableAnalysis)); + diagnosticRecords.AddRange(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, null, skipVariableAnalysis, emitSuppressionErrors)); return diagnosticRecords; } @@ -1549,11 +1563,11 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra IEnumerable records; if (skipParsing && previousUnusedCorrections == 0) { - records = AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty, skipVariableAnalysis); + records = AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty, skipVariableAnalysis, emitSuppressionErrors: false); } else { - records = AnalyzeScriptDefinition(text.ToString(), out scriptAst, out scriptTokens, skipVariableAnalysis); + records = AnalyzeScriptDefinition(text.ToString(), out scriptAst, out scriptTokens, skipVariableAnalysis, emitSuppressionErrors: false); } var corrections = records .Select(r => r.SuggestedCorrections) @@ -1986,7 +2000,8 @@ bool IsRuleAllowed(IRule rule) private Tuple, List> SuppressRule( string ruleName, Dictionary> ruleSuppressions, - List ruleDiagnosticRecords) + List ruleDiagnosticRecords, + bool emitSuppressionErrors = true) { List suppressRuleErrors; var records = Helper.Instance.SuppressRule( @@ -1994,9 +2009,12 @@ private Tuple, List> SuppressRule( ruleSuppressions, ruleDiagnosticRecords, out suppressRuleErrors); - foreach (var error in suppressRuleErrors) + if (emitSuppressionErrors) { - this.outputWriter.WriteError(error); + foreach (var error in suppressRuleErrors) + { + this.outputWriter.WriteError(error); + } } return records; } @@ -2014,13 +2032,15 @@ private Tuple, List> SuppressRule( /// Returns a tuple of suppressed and diagnostic records private Tuple, List> SuppressRule( Dictionary> ruleSuppressions, - DiagnosticRecord ruleDiagnosticRecord + DiagnosticRecord ruleDiagnosticRecord, + bool emitSuppressionErrors = true ) { return SuppressRule( ruleDiagnosticRecord.RuleName, ruleSuppressions, - new List { ruleDiagnosticRecord }); + new List { ruleDiagnosticRecord }, + emitSuppressionErrors); } /// @@ -2038,6 +2058,27 @@ public IEnumerable AnalyzeSyntaxTree( Token[] scriptTokens, string filePath, bool skipVariableAnalysis = false) + { + return AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath, skipVariableAnalysis, emitSuppressionErrors: true); + } + + /// + /// Analyzes the syntax tree of a script file that has already been parsed. + /// + /// The ScriptBlockAst from the parsed script. + /// The tokens found in the script. + /// The path to the file that was parsed. + /// If AnalyzeSyntaxTree is called from an AST obtained via ParseInput, this field will be String.Empty. + /// + /// Whether to skip variable analysis. + /// Whether to emit errors for unapplied rule suppression IDs. + /// An enumeration of DiagnosticRecords found by rules. + public IEnumerable AnalyzeSyntaxTree( + ScriptBlockAst scriptAst, + Token[] scriptTokens, + string filePath, + bool skipVariableAnalysis, + bool emitSuppressionErrors) { Dictionary> ruleSuppressions = new Dictionary>(); ConcurrentBag diagnostics = new ConcurrentBag(); @@ -2117,7 +2158,10 @@ public IEnumerable AnalyzeSyntaxTree( ruleSuppressions, ruleRecords, out suppressRuleErrors); - result.AddRange(suppressRuleErrors); + if (emitSuppressionErrors) + { + result.AddRange(suppressRuleErrors); + } foreach (var record in records.Item2) { diagnostics.Add(record); @@ -2177,7 +2221,7 @@ public IEnumerable AnalyzeSyntaxTree( try { var ruleRecords = tokenRule.AnalyzeTokens(scriptTokens, filePath).ToList(); - var records = SuppressRule(tokenRule.GetName(), ruleSuppressions, ruleRecords); + var records = SuppressRule(tokenRule.GetName(), ruleSuppressions, ruleRecords, emitSuppressionErrors); foreach (var record in records.Item2) { diagnostics.Add(record); @@ -2215,7 +2259,7 @@ public IEnumerable AnalyzeSyntaxTree( try { var ruleRecords = dscResourceRule.AnalyzeDSCClass(scriptAst, filePath).ToList(); - var records = SuppressRule(dscResourceRule.GetName(), ruleSuppressions, ruleRecords); + var records = SuppressRule(dscResourceRule.GetName(), ruleSuppressions, ruleRecords, emitSuppressionErrors); foreach (var record in records.Item2) { diagnostics.Add(record); @@ -2234,7 +2278,7 @@ public IEnumerable AnalyzeSyntaxTree( } // Check if the supplied artifact is indeed part of the DSC resource - if (!filePathIsNullOrWhiteSpace && Helper.Instance.IsDscResourceModule(filePath)) + else if (!filePathIsNullOrWhiteSpace && Helper.Instance.IsDscResourceModule(filePath)) { // Run all DSC Rules foreach (IDSCResourceRule dscResourceRule in this.DSCResourceRules) @@ -2248,7 +2292,7 @@ public IEnumerable AnalyzeSyntaxTree( try { var ruleRecords = dscResourceRule.AnalyzeDSCResource(scriptAst, filePath).ToList(); - var records = SuppressRule(dscResourceRule.GetName(), ruleSuppressions, ruleRecords); + var records = SuppressRule(dscResourceRule.GetName(), ruleSuppressions, ruleRecords, emitSuppressionErrors); foreach (var record in records.Item2) { diagnostics.Add(record); @@ -2297,7 +2341,7 @@ public IEnumerable AnalyzeSyntaxTree( foreach (var ruleRecord in this.GetExternalRecord(scriptAst, scriptTokens, exRules.ToArray(), filePath)) { - var records = SuppressRule(ruleSuppressions, ruleRecord); + var records = SuppressRule(ruleSuppressions, ruleRecord, emitSuppressionErrors); foreach (var record in records.Item2) { diagnostics.Add(record); diff --git a/Tests/Engine/RuleSuppression.tests.ps1 b/Tests/Engine/RuleSuppression.tests.ps1 index 2d31a6ddf..26ca9df78 100644 --- a/Tests/Engine/RuleSuppression.tests.ps1 +++ b/Tests/Engine/RuleSuppression.tests.ps1 @@ -244,6 +244,30 @@ function MyFunc $suppErr.TargetObject.RuleSuppressionID | Should -BeExactly "banana" } } + + It "Issues one unapplied suppression error when -Fix reanalyzes a file" -Skip:$testingLibraryUsage { + $scriptPath = Join-Path $TestDrive 'SuppressionFix.ps1' + $script = @( + 'function Test-Function1 {' + " [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingWriteHost','NonExistentID123')]" + ' param() ; Write-Host ''x''' + '}' + ) -join "`n" + + [System.IO.File]::WriteAllText($scriptPath, $script + "`n") + + $diagnostics = Invoke-ScriptAnalyzer ` + -Path $scriptPath ` + -Fix ` + -ErrorVariable fixErr ` + -ErrorAction SilentlyContinue + + $diagnostics | Should -HaveCount 1 + $diagnostics[0].RuleName | Should -BeExactly 'PSAvoidUsingWriteHost' + $fixErr | Should -HaveCount 1 + $fixErr[0].TargetObject.RuleName | Should -BeExactly 'PSAvoidUsingWriteHost' + $fixErr[0].TargetObject.RuleSuppressionID | Should -BeExactly 'NonExistentID123' + } } Context "RuleSuppressionID with named arguments" { diff --git a/Tests/Rules/UseDSCResourceFunctions.tests.ps1 b/Tests/Rules/UseDSCResourceFunctions.tests.ps1 index 9112d6e22..1ebda2d44 100644 --- a/Tests/Rules/UseDSCResourceFunctions.tests.ps1 +++ b/Tests/Rules/UseDSCResourceFunctions.tests.ps1 @@ -46,4 +46,34 @@ Describe "StandardDSCFunctionsInClass" { $noClassViolations.Count | Should -Be 0 } } + + Context "When a class-based DSC resource is also in DSC resource module layout" { + It "does not duplicate the unapplied suppression error" { + $resourceRoot = Join-Path $TestDrive 'DSCResources' + $resourceDir = Join-Path $resourceRoot 'MyRes' + $resourcePath = Join-Path $resourceDir 'MyRes.psm1' + $schemaPath = Join-Path $resourceDir 'MyRes.schema.mof' + + New-Item -ItemType Directory -Path $resourceDir -Force | Out-Null + [System.IO.File]::WriteAllText($resourcePath, @' +[System.Diagnostics.CodeAnalysis.SuppressMessage('PSDSCStandardDSCFunctionsInResource', 'BadDscId', Scope='Class', Target='MyRes')] +[DscResource()] +class MyRes { + [DscProperty(Key)] [string] $Name + [MyRes] Get() { return $this } +} +'@.TrimStart() + "`n") + Set-Content -Path $schemaPath -Value '' + + Invoke-ScriptAnalyzer ` + -Path $resourcePath ` + -ErrorVariable dscErr ` + -ErrorAction SilentlyContinue | + Out-Null + + $dscErr | Should -HaveCount 1 + $dscErr[0].TargetObject.RuleName | Should -BeExactly $violationName + $dscErr[0].TargetObject.RuleSuppressionID | Should -BeExactly 'BadDscId' + } + } } From 9c04a44e36d6ba3ab1dc5fb0431abef592e87054 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 7 May 2026 21:57:23 +0100 Subject: [PATCH 126/130] Settings file creation and validation (#2176) * Add RuleOptionInfo class and update RuleInfo to include options for configurable rules * Add Test and New Cmdlets for PSScriptAnalyzer Settings Management - Implemented `New-ScriptAnalyzerSettingsFile` cmdlet to create a new PSScriptAnalyzer settings file, with options for presets and overwriting existing files. - Added `Test-ScriptAnalyzerSettingsFile` cmdlet to validate settings files, checking for parseability, rule existence, and valid options. - Created comprehensive tests for both cmdlets to ensure functionality and error handling. - Updated module manifest to export the new cmdlets. - Added documentation for both cmdlets, including usage examples and parameter descriptions. - Enhanced error messages in the strings resource file for better clarity during validation failures. * Enhance ScriptAnalyzer settings file validation and documentation - Update Helper.cs to return null for empty output paths instead of an empty array. - Add new error message for invalid option types in Strings.resx. - Extend tests for New-ScriptAnalyzerSettingsFile to check for new keys: CustomRulePath, IncludeDefaultRules, and RecurseCustomRulePath. - Modify Test-ScriptAnalyzerSettingsFile tests to validate output and error handling for various scenarios, including type mismatches and invalid values. - Improve documentation for New-ScriptAnalyzerSettingsFile and Test-ScriptAnalyzerSettingsFile to clarify behavior and parameters, including handling of custom rules and output format. * Apply docs suggestions from Sean's review Co-authored-by: Sean Wheeler * Add Copilot review suggestions --------- Co-authored-by: Sean Wheeler --- .../Commands/GetScriptAnalyzerRuleCommand.cs | 6 +- .../NewScriptAnalyzerSettingsFileCommand.cs | 537 +++++++++++++++ .../TestScriptAnalyzerSettingsFileCommand.cs | 635 ++++++++++++++++++ Engine/Generic/RuleInfo.cs | 35 + Engine/Generic/RuleOptionInfo.cs | 131 ++++ Engine/Helper.cs | 2 +- Engine/PSScriptAnalyzer.psd1 | 2 +- Engine/PSScriptAnalyzer.psm1 | 4 + Engine/Strings.resx | 36 + Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 30 + .../NewScriptAnalyzerSettingsFile.tests.ps1 | 283 ++++++++ .../TestScriptAnalyzerSettingsFile.tests.ps1 | 424 ++++++++++++ .../Cmdlets/New-ScriptAnalyzerSettingsFile.md | 187 ++++++ .../Test-ScriptAnalyzerSettingsFile.md | 151 +++++ 14 files changed, 2460 insertions(+), 3 deletions(-) create mode 100644 Engine/Commands/NewScriptAnalyzerSettingsFileCommand.cs create mode 100644 Engine/Commands/TestScriptAnalyzerSettingsFileCommand.cs create mode 100644 Engine/Generic/RuleOptionInfo.cs create mode 100644 Tests/Engine/NewScriptAnalyzerSettingsFile.tests.ps1 create mode 100644 Tests/Engine/TestScriptAnalyzerSettingsFile.tests.ps1 create mode 100644 docs/Cmdlets/New-ScriptAnalyzerSettingsFile.md create mode 100644 docs/Cmdlets/Test-ScriptAnalyzerSettingsFile.md diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs index 3219affa7..9a2782a45 100644 --- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs +++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs @@ -114,8 +114,12 @@ protected override void ProcessRecord() foreach (IRule rule in rules) { + var ruleOptions = rule is ConfigurableRule + ? RuleOptionInfo.GetRuleOptions(rule) + : null; + WriteObject(new RuleInfo(rule.GetName(), rule.GetCommonName(), rule.GetDescription(), - rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType())); + rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType(), ruleOptions)); } } } diff --git a/Engine/Commands/NewScriptAnalyzerSettingsFileCommand.cs b/Engine/Commands/NewScriptAnalyzerSettingsFileCommand.cs new file mode 100644 index 000000000..3f2b36844 --- /dev/null +++ b/Engine/Commands/NewScriptAnalyzerSettingsFileCommand.cs @@ -0,0 +1,537 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands +{ + /// + /// Creates a new PSScriptAnalyzer settings file. + /// The emitted file is always named PSScriptAnalyzerSettings.psd1 so that automatic + /// settings discovery works when the file is placed in a project directory. + /// + [Cmdlet(VerbsCommon.New, "ScriptAnalyzerSettingsFile", SupportsShouldProcess = true, + HelpUri = "https://github.com/PowerShell/PSScriptAnalyzer")] + public class NewScriptAnalyzerSettingsFileCommand : PSCmdlet, IOutputWriter + { + private const string SettingsFileName = "PSScriptAnalyzerSettings.psd1"; + + #region Parameters + + /// + /// The directory where the settings file will be created. + /// Defaults to the current working directory. + /// + [Parameter(Mandatory = false, Position = 0)] + [ValidateNotNullOrEmpty] + public string Path { get; set; } + + /// + /// The name of a built-in preset to use as the basis for the + /// generated settings file. When omitted, all rules and their default + /// configurable options are included. Valid values are resolved dynamically + /// from the shipped preset files and tab-completed via an argument completer + /// registered in PSScriptAnalyzer.psm1. + /// + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string BaseOnPreset { get; set; } + + /// + /// Overwrite an existing settings file at the target path. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + #endregion Parameters + + #region Overrides + + /// + /// Initialise the analyser engine so that rule metadata is available. + /// + protected override void BeginProcessing() + { + Helper.Instance = new Helper(SessionState.InvokeCommand); + Helper.Instance.Initialize(); + + ScriptAnalyzer.Instance.Initialize(this, null, null, null, null, true); + } + + /// + /// Generate and write the settings file. + /// + protected override void ProcessRecord() + { + // Validate -BaseOnPreset against the dynamically discovered presets. + if (!string.IsNullOrEmpty(BaseOnPreset)) + { + var validPresets = Settings.GetSettingPresets().ToList(); + if (!validPresets.Contains(BaseOnPreset, StringComparer.OrdinalIgnoreCase)) + { + ThrowTerminatingError( + new ErrorRecord( + new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Strings.InvalidPresetName, + BaseOnPreset, + string.Join(", ", validPresets) + ) + ), + "InvalidPresetName", + ErrorCategory.InvalidArgument, + BaseOnPreset + ) + ); + } + } + + string directory = string.IsNullOrEmpty(Path) + ? SessionState.Path.CurrentFileSystemLocation.Path + : GetUnresolvedProviderPathFromPSPath(Path); + + string targetPath = System.IO.Path.Combine(directory, SettingsFileName); + + // Guard against overwriting an existing settings file unless -Force is specified. + if (File.Exists(targetPath) && !Force) + { + ThrowTerminatingError( + new ErrorRecord( + new IOException( + string.Format( + CultureInfo.CurrentCulture, + Strings.SettingsFileAlreadyExists, + targetPath + ) + ), + "SettingsFileAlreadyExists", + ErrorCategory.ResourceExists, + targetPath + ) + ); + } + + string content; + if (!string.IsNullOrEmpty(BaseOnPreset)) + { + content = GenerateFromPreset(BaseOnPreset); + } + else + { + content = GenerateFromAllRules(); + } + + if (ShouldProcess(targetPath, "Create settings file")) + { + // Ensure the target directory exists. + Directory.CreateDirectory(directory); + File.WriteAllText(targetPath, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + WriteObject(new FileInfo(targetPath)); + } + } + + #endregion Overrides + + #region Settings generation + + /// + /// Generates settings content from a built-in preset. The preset is parsed and + /// the output is normalised to include all top-level fields. + /// + private string GenerateFromPreset(string presetName) + { + string presetPath = Settings.GetSettingPresetFilePath(presetName); + if (presetPath == null || !File.Exists(presetPath)) + { + ThrowTerminatingError( + new ErrorRecord( + new ArgumentException( + string.Format( + CultureInfo.CurrentCulture, + Strings.PresetNotFound, + presetName + ) + ), + "PresetNotFound", + ErrorCategory.ObjectNotFound, + presetName + ) + ); + } + + var parsed = new Settings(presetPath); + var ruleOptionMap = BuildRuleOptionMap(); + + var sb = new StringBuilder(); + WriteHeader(sb, presetName); + sb.AppendLine("@{"); + + sb.AppendLine(" # Rules to run. When populated, only these rules are used."); + sb.AppendLine(" # Leave empty to run all rules."); + WriteStringArray(sb, "IncludeRules", parsed.IncludeRules); + sb.AppendLine(); + + sb.AppendLine(" # Rules to skip. Takes precedence over IncludeRules."); + WriteStringArray(sb, "ExcludeRules", parsed.ExcludeRules); + sb.AppendLine(); + + sb.AppendLine(" # Only report diagnostics at these severity levels."); + sb.AppendLine(" # Leave empty to report all severities."); + WriteSeverityArray(sb, parsed.Severities); + sb.AppendLine(); + + sb.AppendLine(" # Paths to modules or directories containing custom rules."); + sb.AppendLine(" # When specified, these rules are loaded in addition to (or instead"); + sb.AppendLine(" # of) the built-in rules, depending on IncludeDefaultRules."); + sb.AppendLine(" # Note: Relative paths are resolved from the caller's working directory,"); + sb.AppendLine(" # not the location of this settings file."); + WriteStringArray(sb, "CustomRulePath", parsed.CustomRulePath); + sb.AppendLine(); + + sb.AppendLine(" # When set to $true and CustomRulePath is specified, built-in rules"); + sb.AppendLine(" # are loaded alongside custom rules. Has no effect without CustomRulePath."); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, + " IncludeDefaultRules = {0}", parsed.IncludeDefaultRules ? "$true" : "$false")); + sb.AppendLine(); + + sb.AppendLine(" # When set to $true, searches sub-folders under CustomRulePath for"); + sb.AppendLine(" # additional rule modules. Has no effect without CustomRulePath."); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, + " RecurseCustomRulePath = {0}", parsed.RecurseCustomRulePath ? "$true" : "$false")); + sb.AppendLine(); + + sb.AppendLine(" # Per-rule configuration. Only configurable rules appear here."); + sb.AppendLine(" # Values from the preset are shown; other properties use defaults."); + + if (parsed.RuleArguments != null && parsed.RuleArguments.Count > 0) + { + sb.AppendLine(" Rules = @{"); + + bool firstRule = true; + foreach (var ruleEntry in parsed.RuleArguments.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) + { + if (!firstRule) + { + sb.AppendLine(); + } + firstRule = false; + + string ruleName = ruleEntry.Key; + var presetArgs = ruleEntry.Value; + + if (ruleOptionMap.TryGetValue(ruleName, out var optionInfos)) + { + WriteRuleSettings(sb, ruleName, optionInfos, presetArgs); + } + else + { + WriteRuleSettingsRaw(sb, ruleName, presetArgs); + } + } + + sb.AppendLine(" }"); + } + else + { + sb.AppendLine(" Rules = @{}"); + } + + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// + /// Generates settings content that includes every available rule with all + /// configurable properties set to their defaults. + /// + private string GenerateFromAllRules() + { + var ruleNames = new List(); + var ruleOptionMap = BuildRuleOptionMap(ruleNames); + + var sb = new StringBuilder(); + WriteHeader(sb, presetName: null); + sb.AppendLine("@{"); + + sb.AppendLine(" # Rules to run. When populated, only these rules are used."); + sb.AppendLine(" # Leave empty to run all rules."); + WriteStringArray(sb, "IncludeRules", ruleNames); + sb.AppendLine(); + + sb.AppendLine(" # Rules to skip. Takes precedence over IncludeRules."); + WriteStringArray(sb, "ExcludeRules", Enumerable.Empty()); + sb.AppendLine(); + + sb.AppendLine(" # Only report diagnostics at these severity levels."); + sb.AppendLine(" # Leave empty to report all severities."); + WriteSeverityArray(sb, Enumerable.Empty()); + sb.AppendLine(); + + sb.AppendLine(" # Paths to modules or directories containing custom rules."); + sb.AppendLine(" # When specified, these rules are loaded in addition to (or instead"); + sb.AppendLine(" # of) the built-in rules, depending on IncludeDefaultRules."); + sb.AppendLine(" # Note: Relative paths are resolved from the caller's working directory,"); + sb.AppendLine(" # not the location of this settings file."); + WriteStringArray(sb, "CustomRulePath", Enumerable.Empty()); + sb.AppendLine(); + + sb.AppendLine(" # When set to $true and CustomRulePath is specified, built-in rules"); + sb.AppendLine(" # are loaded alongside custom rules. Has no effect without CustomRulePath."); + sb.AppendLine(" IncludeDefaultRules = $false"); + sb.AppendLine(); + + sb.AppendLine(" # When set to $true, searches sub-folders under CustomRulePath for"); + sb.AppendLine(" # additional rule modules. Has no effect without CustomRulePath."); + sb.AppendLine(" RecurseCustomRulePath = $false"); + sb.AppendLine(); + + sb.AppendLine(" # Per-rule configuration. Only configurable rules appear here."); + sb.AppendLine(" Rules = @{"); + + bool firstRule = true; + foreach (var kvp in ruleOptionMap.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)) + { + if (!firstRule) + { + sb.AppendLine(); + } + firstRule = false; + + WriteRuleSettings(sb, kvp.Key, kvp.Value, presetArgs: null); + } + + sb.AppendLine(" }"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// + /// Builds a map of rule name to its configurable property metadata. + /// Optionally populates a list of all rule names encountered. + /// + private Dictionary> BuildRuleOptionMap(List allRuleNames = null) + { + var map = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + string[] modNames = ScriptAnalyzer.Instance.GetValidModulePaths(); + IEnumerable rules = ScriptAnalyzer.Instance.GetRule(modNames, null) + ?? Enumerable.Empty(); + + foreach (IRule rule in rules) + { + string name = rule.GetName(); + allRuleNames?.Add(name); + + if (rule is ConfigurableRule) + { + var options = RuleOptionInfo.GetRuleOptions(rule); + if (options.Count > 0) + { + map[name] = options; + } + } + } + + return map; + } + + #endregion Settings generation + + #region Formatting helpers + + /// + /// Writes a comment header identifying the tool and version that generated + /// the file, along with the preset if one was specified. + /// + private static void WriteHeader(StringBuilder sb, string presetName) + { + Version version = typeof(ScriptAnalyzer).Assembly.GetName().Version; + string versionStr = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", version.Major, version.Minor, version.Build); + + sb.AppendLine("#"); + sb.AppendLine(string.Format( + CultureInfo.InvariantCulture, + "# PSScriptAnalyzer settings file ({0})", + versionStr)); + + if (!string.IsNullOrEmpty(presetName)) + { + sb.AppendLine(string.Format( + CultureInfo.InvariantCulture, + "# Based on the '{0}' preset.", + presetName)); + } + + sb.AppendLine("#"); + sb.AppendLine("# Generated by New-ScriptAnalyzerSettingsFile."); + sb.AppendLine("#"); + sb.AppendLine(); + } + + /// + /// Writes a PowerShell string-array assignment such as IncludeRules = @( ... ). + /// + private static void WriteStringArray(StringBuilder sb, string key, IEnumerable values) + { + var items = values?.ToList() ?? new List(); + + if (items.Count == 0) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0} = @()", key)); + return; + } + + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0} = @(", key)); + foreach (string item in items) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " '{0}'", item)); + } + sb.AppendLine(" )"); + } + + /// + /// Writes the Severity array with an inline comment listing valid values. + /// + private static void WriteSeverityArray(StringBuilder sb, IEnumerable values) + { + string validValues = string.Join(", ", Enum.GetNames(typeof(RuleSeverity))); + var items = values?.ToList() ?? new List(); + + if (items.Count == 0) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " Severity = @() # {0}", validValues)); + return; + } + + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " Severity = @( # {0}", validValues)); + foreach (string item in items) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " '{0}'", item)); + } + sb.AppendLine(" )"); + } + + /// + /// Writes a rule settings block using option metadata, optionally merging + /// with values from a preset. Enable always appears first, followed by + /// the remaining properties sorted alphabetically. + /// + private static void WriteRuleSettings( + StringBuilder sb, + string ruleName, + List optionInfos, + Dictionary presetArgs) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0} = @{{", ruleName)); + + foreach (RuleOptionInfo option in optionInfos) + { + object value = option.DefaultValue; + if (presetArgs != null + && presetArgs.TryGetValue(option.Name, out object presetVal)) + { + value = presetVal; + } + + string formatted = FormatValue(value); + string comment = FormatPossibleValuesComment(option); + + sb.AppendLine(string.Format( + CultureInfo.InvariantCulture, + " {0} = {1}{2}", + option.Name, + formatted, + comment)); + } + + sb.AppendLine(" }"); + } + + /// + /// Writes preset rule arguments verbatim when no option metadata is available. + /// + private static void WriteRuleSettingsRaw( + StringBuilder sb, + string ruleName, + Dictionary args) + { + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, " {0} = @{{", ruleName)); + + foreach (var kvp in args.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)) + { + sb.AppendLine(string.Format( + CultureInfo.InvariantCulture, + " {0} = {1}", + kvp.Key, + FormatValue(kvp.Value))); + } + + sb.AppendLine(" }"); + } + + /// + /// Formats a value as a PowerShell literal suitable for inclusion in a .psd1 file. + /// + private static string FormatValue(object value) + { + if (value is bool boolVal) + { + return boolVal ? "$true" : "$false"; + } + + if (value is int || value is long || value is double || value is float) + { + return Convert.ToString(value, CultureInfo.InvariantCulture); + } + + if (value is string strVal) + { + return string.Format(CultureInfo.InvariantCulture, "'{0}'", strVal); + } + + if (value is Array arr) + { + if (arr.Length == 0) + { + return "@()"; + } + + var elements = new List(); + foreach (object item in arr) + { + elements.Add(FormatValue(item)); + } + return string.Format(CultureInfo.InvariantCulture, "@({0})", string.Join(", ", elements)); + } + + // Fallback - treat as string. + return string.Format(CultureInfo.InvariantCulture, "'{0}'", value); + } + + /// + /// Returns an inline comment listing the valid values, or an empty string + /// when the option is unconstrained. + /// + private static string FormatPossibleValuesComment(RuleOptionInfo option) + { + if (option.PossibleValues == null || option.PossibleValues.Length == 0) + { + return string.Empty; + } + + return " # " + string.Join(", ", option.PossibleValues.Select(v => v.ToString())); + } + + #endregion Formatting helpers + } +} diff --git a/Engine/Commands/TestScriptAnalyzerSettingsFileCommand.cs b/Engine/Commands/TestScriptAnalyzerSettingsFileCommand.cs new file mode 100644 index 000000000..1eb8f8bb3 --- /dev/null +++ b/Engine/Commands/TestScriptAnalyzerSettingsFileCommand.cs @@ -0,0 +1,635 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands +{ + /// + /// Validates a PSScriptAnalyzer settings file as a self-contained unit. + /// Checks that the file is parseable, that referenced rules exist, and that all + /// rule options and their values are valid. + /// + /// Custom rule paths, RecurseCustomRulePath and IncludeDefaultRules are read + /// from the settings file itself so that validation reflects what + /// Invoke-ScriptAnalyzer would see when given the same file. + /// + /// In the default mode each problem is emitted as a DiagnosticRecord with the + /// source extent of the offending text. When -Quiet is specified, returns only + /// $true or $false - indicating whether the settings file is valid. + /// + [Cmdlet(VerbsDiagnostic.Test, "ScriptAnalyzerSettingsFile", + HelpUri = "https://github.com/PowerShell/PSScriptAnalyzer")] + [OutputType(typeof(DiagnosticRecord))] + [OutputType(typeof(bool))] + public class TestScriptAnalyzerSettingsFileCommand : PSCmdlet, IOutputWriter + { + private const string RuleName = "Test-ScriptAnalyzerSettingsFile"; + + #region Parameters + + /// + /// The path to the settings file to validate. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string Path { get; set; } + + /// + /// When specified, returns only $true or $false without emitting + /// diagnostic records. Without this switch the cmdlet writes a + /// DiagnosticRecord for every problem found and produces no output + /// when the file is valid. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Quiet { get; set; } + + #endregion Parameters + + #region Private state + + private string _resolvedPath; + private List _diagnostics; + + #endregion Private state + + #region Overrides + + /// + /// Initialise the helper. Full engine initialisation is + /// deferred to ProcessRecord because we need to read CustomRulePath and + /// IncludeDefaultRules from the settings file first. + /// + protected override void BeginProcessing() + { + Helper.Instance = new Helper(SessionState.InvokeCommand); + Helper.Instance.Initialize(); + } + + /// + /// ProcessRecord: Parse and validate the settings file. + /// + protected override void ProcessRecord() + { + _resolvedPath = GetUnresolvedProviderPathFromPSPath(Path); + _diagnostics = new List(); + + if (!File.Exists(_resolvedPath)) + { + if (Quiet) + { + WriteObject(false); + } + else + { + WriteError(new ErrorRecord( + new FileNotFoundException(string.Format( + CultureInfo.CurrentCulture, + Strings.SettingsFileNotFound, + _resolvedPath)), + "SettingsFileNotFound", + ErrorCategory.ObjectNotFound, + _resolvedPath)); + } + + return; + } + + // Parse with the PowerShell AST to get source extents. + ScriptBlockAst scriptAst = Parser.ParseFile( + _resolvedPath, + out Token[] tokens, + out ParseError[] parseErrors + ); + + if (parseErrors != null && parseErrors.Length > 0) + { + if (Quiet) + { + WriteObject(false); + } + else + { + foreach (ParseError pe in parseErrors) + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileParseError, pe.Message), + pe.Extent, + DiagnosticSeverity.ParseError); + } + + EmitDiagnostics(); + } + + return; + } + + // Locate the root hashtable. + HashtableAst rootHashtable = scriptAst.Find(ast => ast is HashtableAst, searchNestedScriptBlocks: false) as HashtableAst; + if (rootHashtable == null) + { + if (Quiet) + { + WriteObject(false); + } + else + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileParseError, "File does not contain a hashtable."), + scriptAst.Extent, + DiagnosticSeverity.Error); + EmitDiagnostics(); + } + + return; + } + + // Also parse via Settings to get the evaluated data. + Settings parsed; + try + { + parsed = new Settings(_resolvedPath); + } + catch (Exception ex) + { + if (Quiet) + { + WriteObject(false); + } + else + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileParseError, ex.Message), + rootHashtable.Extent, + DiagnosticSeverity.Error); + EmitDiagnostics(); + } + + return; + } + + // Initialise the analyser engine using custom rule paths and + // IncludeDefaultRules from the settings file so that validation + // reflects the same rule set Invoke-ScriptAnalyzer would use (given + // this settings file). + string[] rulePaths = Helper.ProcessCustomRulePaths( + parsed.CustomRulePath?.ToArray(), + SessionState, + parsed.RecurseCustomRulePath); + + // Treat an empty array the same as null — no custom paths were specified. + if (rulePaths != null && rulePaths.Length == 0) + { + rulePaths = null; + } + + bool includeDefaultRules = rulePaths == null || parsed.IncludeDefaultRules; + ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, includeDefaultRules); + + // Build lookup structures. + var topLevelMap = BuildAstKeyMap(rootHashtable); + + string[] modNames = ScriptAnalyzer.Instance.GetValidModulePaths(); + IEnumerable knownRules = ScriptAnalyzer.Instance.GetRule(modNames, null) + ?? Enumerable.Empty(); + + var ruleMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (IRule rule in knownRules) + { + ruleMap[rule.GetName()] = rule; + } + + // Validate IncludeRules. + ValidateRuleNameArray(parsed.IncludeRules, ruleMap, "IncludeRules", topLevelMap); + + // Validate ExcludeRules. + ValidateRuleNameArray(parsed.ExcludeRules, ruleMap, "ExcludeRules", topLevelMap); + + // Validate Severity values. + ValidateSeverityArray(parsed.Severities, topLevelMap); + + // Validate rule arguments. + if (parsed.RuleArguments != null) + { + HashtableAst rulesHashtable = GetNestedHashtable(topLevelMap, "Rules"); + + var rulesAstMap = rulesHashtable != null + ? BuildAstKeyMap(rulesHashtable) + : new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var ruleEntry in parsed.RuleArguments) + { + string ruleName = ruleEntry.Key; + IScriptExtent ruleKeyExtent = GetKeyExtent(rulesAstMap, ruleName) + ?? rulesHashtable?.Extent + ?? rootHashtable.Extent; + + if (!ruleMap.TryGetValue(ruleName, out IRule rule)) + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileRuleArgRuleNotFound, ruleName), + ruleKeyExtent, + DiagnosticSeverity.Error); + continue; + } + + if (!(rule is ConfigurableRule)) + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileRuleNotConfigurable, ruleName), + ruleKeyExtent, + DiagnosticSeverity.Error); + continue; + } + + var optionInfos = RuleOptionInfo.GetRuleOptions(rule); + var optionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var opt in optionInfos) + { + optionMap[opt.Name] = opt; + } + + // Get the AST for this rule's nested hashtable. + HashtableAst ruleHashtable = GetNestedHashtable(rulesAstMap, ruleName); + var ruleArgAstMap = ruleHashtable != null + ? BuildAstKeyMap(ruleHashtable) + : new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var arg in ruleEntry.Value) + { + string argName = arg.Key; + IScriptExtent argKeyExtent = GetKeyExtent(ruleArgAstMap, argName) + ?? ruleKeyExtent; + + if (!optionMap.TryGetValue(argName, out RuleOptionInfo optionInfo)) + { + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileUnrecognisedOption, ruleName, argName), + argKeyExtent, + DiagnosticSeverity.Error); + continue; + } + + // Validate that the value is compatible with the expected type. + if (arg.Value != null && !IsValueCompatible(arg.Value, optionInfo.OptionType)) + { + IScriptExtent valueExtent = GetValueExtent(ruleArgAstMap, argName) + ?? argKeyExtent; + + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileInvalidOptionType, + ruleName, argName, GetFriendlyTypeName(optionInfo.OptionType)), + valueExtent, + DiagnosticSeverity.Error); + } + // Validate constrained string values against the set of possible values. + else if (optionInfo.PossibleValues != null + && optionInfo.PossibleValues.Length > 0 + && arg.Value is string strValue) + { + bool valueValid = optionInfo.PossibleValues.Any(pv => + string.Equals(pv.ToString(), strValue, StringComparison.OrdinalIgnoreCase)); + + if (!valueValid) + { + IScriptExtent valueExtent = GetValueExtent(ruleArgAstMap, argName) + ?? argKeyExtent; + + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileInvalidOptionValue, + ruleName, argName, strValue, + string.Join(", ", optionInfo.PossibleValues.Select(v => v.ToString()))), + valueExtent, + DiagnosticSeverity.Error); + } + } + } + } + } + + if (Quiet) + { + WriteObject(_diagnostics.Count == 0); + } + else + { + EmitDiagnostics(); + } + } + + #endregion Overrides + + #region Diagnostics + + /// + /// Records a DiagnosticRecord for later emission. + /// + private void AddDiagnostic(string message, IScriptExtent extent, DiagnosticSeverity severity) + { + _diagnostics.Add(new DiagnosticRecord( + message, + extent, + RuleName, + severity, + _resolvedPath)); + } + + /// + /// Writes all collected DiagnosticRecord objects to the output pipeline. + /// + private void EmitDiagnostics() + { + foreach (var diag in _diagnostics) + { + WriteObject(diag); + } + } + + #endregion Diagnostics + + #region AST helpers + + /// + /// Builds a case-insensitive dictionary mapping key names to their + /// (key-expression, value-statement) tuples in a HashtableAst. + /// + private static Dictionary> BuildAstKeyMap(HashtableAst hashtableAst) + { + var map = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (hashtableAst?.KeyValuePairs == null) + { + return map; + } + + foreach (var pair in hashtableAst.KeyValuePairs) + { + if (pair.Item1 is StringConstantExpressionAst keyAst) + { + map[keyAst.Value] = pair; + } + } + + return map; + } + + /// + /// Returns the IScriptExtent of a key expression in an AST key map, + /// or null if the key is not found. + /// + private static IScriptExtent GetKeyExtent( + Dictionary> astMap, + string keyName) + { + if (astMap.TryGetValue(keyName, out var pair)) + { + return pair.Item1.Extent; + } + + return null; + } + + /// + /// Returns the IScriptExtent of a value expression in an AST key map, + /// or null if the key is not found. + /// + private static IScriptExtent GetValueExtent( + Dictionary> astMap, + string keyName) + { + if (astMap.TryGetValue(keyName, out var pair)) + { + ExpressionAst valueExpr = (pair.Item2 as PipelineAst)?.GetPureExpression(); + if (valueExpr != null) + { + return valueExpr.Extent; + } + + return pair.Item2.Extent; + } + + return null; + } + + /// + /// Returns the HashtableAst for a nested hashtable value, or null. + /// + private static HashtableAst GetNestedHashtable( + Dictionary> astMap, + string keyName) + { + if (astMap.TryGetValue(keyName, out var pair)) + { + ExpressionAst valueExpr = (pair.Item2 as PipelineAst)?.GetPureExpression(); + return valueExpr as HashtableAst; + } + + return null; + } + + /// + /// Returns the IScriptExtent of a specific string element within an + /// array value in the AST, matching by string value. Falls back to + /// the array extent or key extent if not found. + /// + private static IScriptExtent FindArrayElementExtent( + Dictionary> astMap, + string keyName, + string elementValue) + { + if (!astMap.TryGetValue(keyName, out var pair)) + { + return null; + } + + ExpressionAst valueExpr = (pair.Item2 as PipelineAst)?.GetPureExpression(); + if (valueExpr == null) + { + return pair.Item2.Extent; + } + + // Look for the string element in array expressions. + IEnumerable stringNodes = valueExpr.FindAll( + ast => ast is StringConstantExpressionAst strAst + && string.Equals(strAst.Value, elementValue, StringComparison.OrdinalIgnoreCase), + searchNestedScriptBlocks: false); + + Ast match = stringNodes.FirstOrDefault(); + return match?.Extent ?? valueExpr.Extent; + } + + #endregion AST helpers + + #region Validation helpers + + /// + /// Validates that rule names in an array field exist in the known rule set. + /// Wildcard entries are skipped. + /// + private void ValidateRuleNameArray( + IEnumerable ruleNames, + Dictionary ruleMap, + string fieldName, + Dictionary> topLevelMap) + { + if (ruleNames == null) + { + return; + } + + foreach (string name in ruleNames) + { + if (WildcardPattern.ContainsWildcardCharacters(name)) + { + continue; + } + + if (!ruleMap.ContainsKey(name)) + { + IScriptExtent extent = FindArrayElementExtent(topLevelMap, fieldName, name) + ?? GetKeyExtent(topLevelMap, fieldName); + + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileRuleNotFound, fieldName, name), + extent, + DiagnosticSeverity.Error); + } + } + } + + /// + /// Validates severity values against the RuleSeverity enum. + /// + private void ValidateSeverityArray( + IEnumerable severities, + Dictionary> topLevelMap) + { + if (severities == null) + { + return; + } + + foreach (string sev in severities) + { + if (!Enum.TryParse(sev, ignoreCase: true, out _)) + { + IScriptExtent extent = FindArrayElementExtent(topLevelMap, "Severity", sev) + ?? GetKeyExtent(topLevelMap, "Severity"); + + AddDiagnostic( + string.Format(CultureInfo.CurrentCulture, + Strings.SettingsFileInvalidSeverity, + sev, + string.Join(", ", Enum.GetNames(typeof(RuleSeverity)))), + extent, + DiagnosticSeverity.Error); + } + } + } + + /// + /// Checks whether a value from the settings file is compatible with the + /// target CLR property type. + /// + private static bool IsValueCompatible(object value, Type targetType) + { + if (value == null) + { + return !targetType.IsValueType; + } + + Type valueType = value.GetType(); + + // Direct assignment. + if (targetType.IsAssignableFrom(valueType)) + { + return true; + } + + // Bool property — only accept bool. + if (targetType == typeof(bool)) + { + return value is bool; + } + + // Int property — accept int, long within range, or a string that parses as int. + if (targetType == typeof(int)) + { + if (value is int) + { + return true; + } + + if (value is long l) + { + return l >= int.MinValue && l <= int.MaxValue; + } + + return value is string s && int.TryParse(s, out _); + } + + // String property — only accept actual strings so that non-string + // values for constrained options (with PossibleValues) are caught by + // the type check rather than silently skipping enum validation. + if (targetType == typeof(string)) + { + return value is string; + } + + // Array property — accept arrays or a single element of the right kind. + if (targetType.IsArray) + { + Type elementType = targetType.GetElementType(); + + if (valueType.IsArray) + { + // Check that each element is compatible. + foreach (object item in (Array)value) + { + if (!IsValueCompatible(item, elementType)) + { + return false; + } + } + + return true; + } + + // A single value can be wrapped into a one-element array. + return IsValueCompatible(value, elementType); + } + + return false; + } + + /// + /// Returns a user-friendly name for a CLR type for use in error messages. + /// + private static string GetFriendlyTypeName(Type type) + { + if (type == typeof(bool)) return "bool"; + if (type == typeof(int)) return "int"; + if (type == typeof(string)) return "string"; + if (type == typeof(string[])) return "string[]"; + if (type == typeof(int[])) return "int[]"; + return type.Name; + } + + #endregion Validation helpers + } +} diff --git a/Engine/Generic/RuleInfo.cs b/Engine/Generic/RuleInfo.cs index 755d16d15..8d8977d12 100644 --- a/Engine/Generic/RuleInfo.cs +++ b/Engine/Generic/RuleInfo.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic @@ -18,6 +19,7 @@ public class RuleInfo private string sourceName; private RuleSeverity ruleSeverity; private Type implementingType; + private IReadOnlyList options; /// /// Name: The name of the rule. @@ -90,6 +92,16 @@ public Type ImplementingType private set { implementingType = value; } } + /// + /// Options : The configurable properties for this rule, if any. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IReadOnlyList Options + { + get { return options; } + private set { options = value; } + } + /// /// Constructor for a RuleInfo. /// @@ -128,6 +140,29 @@ public RuleInfo(string name, string commonName, string description, SourceType s ImplementingType = implementingType; } + /// + /// Constructor for a RuleInfo. + /// + /// Name of the rule. + /// Common Name of the rule. + /// Description of the rule. + /// Source type of the rule. + /// Source name of the rule. + /// Severity of the rule. + /// The dotnet type of the rule. + /// The configurable properties for this rule. + public RuleInfo(string name, string commonName, string description, SourceType sourceType, string sourceName, RuleSeverity severity, Type implementingType, IReadOnlyList options) + { + RuleName = name; + CommonName = commonName; + Description = description; + SourceType = sourceType; + SourceName = sourceName; + Severity = severity; + ImplementingType = implementingType; + Options = options; + } + public override string ToString() { return RuleName; diff --git a/Engine/Generic/RuleOptionInfo.cs b/Engine/Generic/RuleOptionInfo.cs new file mode 100644 index 000000000..71e704612 --- /dev/null +++ b/Engine/Generic/RuleOptionInfo.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic +{ + /// + /// Holds metadata for a single configurable rule property. + /// + public class RuleOptionInfo + { + /// + /// The name of the configurable property. + /// + public string Name { get; internal set; } + + /// + /// The CLR type of the property value. + /// + public Type OptionType { get; internal set; } + + /// + /// The default value declared via the ConfigurableRuleProperty attribute. + /// + public object DefaultValue { get; internal set; } + + /// + /// The set of valid values for this property, if constrained. + /// Null when any value of the declared type is acceptable. + /// + public object[] PossibleValues { get; internal set; } + + /// + /// Extracts RuleOptionInfo entries for every ConfigurableRuleProperty on + /// the given rule. For string properties backed by a private enum, the + /// possible values are populated from the enum members. + /// + /// The rule instance to inspect. + /// + /// A list of option metadata, ordered with Enable first then the + /// remainder sorted alphabetically. + /// + public static List GetRuleOptions(IRule rule) + { + var options = new List(); + Type ruleType = rule.GetType(); + + PropertyInfo[] properties = ruleType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + + // Collect all private nested enums declared on the rule type so we + // can match them against string properties whose default value is an + // enum member name. + Type[] nestedEnums = ruleType + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public) + .Where(t => t.IsEnum) + .ToArray(); + + foreach (PropertyInfo prop in properties) + { + var attr = prop.GetCustomAttribute(inherit: true); + if (attr == null) + { + continue; + } + + var info = new RuleOptionInfo + { + Name = prop.Name, + OptionType = prop.PropertyType, + DefaultValue = attr.DefaultValue, + PossibleValues = null + }; + + // For string properties, attempt to find a matching private enum + // whose member names include the default value. This mirrors the + // pattern used by rules such as UseConsistentIndentation and + // ProvideCommentHelp where a string property is parsed into a + // private enum via Enum.TryParse. + // + // When multiple enums contain the default value (e.g. both have + // a "None" member), prefer the enum whose name contains the + // property name or vice-versa (e.g. property "Kind" matches enum + // "IndentationKind"). This helps avoid incorrect matches when a rule + // declares several enums with possible overlapping member names. + if (prop.PropertyType == typeof(string) && attr.DefaultValue is string defaultStr) + { + Type bestMatch = null; + bool bestHasNameRelation = false; + + foreach (Type enumType in nestedEnums) + { + if (!Enum.GetNames(enumType).Any(n => + string.Equals(n, defaultStr, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + bool hasNameRelation = + enumType.Name.IndexOf(prop.Name, StringComparison.OrdinalIgnoreCase) >= 0 || + prop.Name.IndexOf(enumType.Name, StringComparison.OrdinalIgnoreCase) >= 0; + + // Take this enum if we have no match yet, or if it has a + // name-based relationship and the previous match did not. + if (bestMatch == null || (hasNameRelation && !bestHasNameRelation)) + { + bestMatch = enumType; + bestHasNameRelation = hasNameRelation; + } + } + + if (bestMatch != null) + { + info.PossibleValues = Enum.GetNames(bestMatch); + } + } + + options.Add(info); + } + + // Sort with "Enable" first, then alphabetically by name for consistent ordering. + return options + .OrderBy(o => string.Equals(o.Name, "Enable", StringComparison.OrdinalIgnoreCase) ? 0 : 1) + .ThenBy(o => o.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } +} diff --git a/Engine/Helper.cs b/Engine/Helper.cs index a162bfbcf..f36d17433 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -1468,7 +1468,7 @@ public static string[] ProcessCustomRulePaths(string[] rulePaths, SessionState s outPaths.Add(path); } - return outPaths.ToArray(); + return outPaths.Count == 0 ? null : outPaths.ToArray(); } diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 993677254..80a5822da 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -65,7 +65,7 @@ FormatsToProcess = @('ScriptAnalyzer.format.ps1xml') FunctionsToExport = @() # Cmdlets to export from this module -CmdletsToExport = @('Get-ScriptAnalyzerRule', 'Invoke-ScriptAnalyzer', 'Invoke-Formatter') +CmdletsToExport = @('Get-ScriptAnalyzerRule', 'Invoke-ScriptAnalyzer', 'Invoke-Formatter', 'New-ScriptAnalyzerSettingsFile', 'Test-ScriptAnalyzerSettingsFile') # Variables to export from this module VariablesToExport = @() diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index 7e2ca8f31..50348fa75 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -49,6 +49,10 @@ if (Get-Command Register-ArgumentCompleter -ErrorAction Ignore) { } + Register-ArgumentCompleter -CommandName 'New-ScriptAnalyzerSettingsFile' ` + -ParameterName 'BaseOnPreset' ` + -ScriptBlock $settingPresetCompleter + Function RuleNameCompleter { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter) diff --git a/Engine/Strings.resx b/Engine/Strings.resx index 346a25aa6..86ad0cf2c 100644 --- a/Engine/Strings.resx +++ b/Engine/Strings.resx @@ -324,4 +324,40 @@ Ignoring 'TypeNotFound' parse error on type '{0}'. Check if the specified type is correct. This can also be due the type not being known at parse time due to types imported by 'using' statements. + + '{0}' is not a recognised preset. Valid presets are: {1} + + + Could not locate the preset '{0}'. + + + A settings file already exists at '{0}'. Use -Force to overwrite. + + + The settings file '{0}' does not exist. + + + Failed to parse settings file: {0} + + + {0}: rule '{1}' not found. + + + Rules.{0}: rule not found. + + + Rules.{0}: this rule is not configurable. + + + Rules.{0}.{1}: unrecognised option. + + + Rules.{0}.{1}: '{2}' is not a valid value. Expected one of: {3} + + + Severity: '{0}' is not a valid severity. Expected one of: {1} + + + Rules.{0}.{1}: expected a value of type {2}. + \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 422b585bf..d1e2cd98d 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -180,3 +180,33 @@ Describe "TestImplementingType" { $type.BaseType.Name | Should -Be "ConfigurableRule" } } + +Describe "TestOptions" { + BeforeAll { + $configurableRule = Get-ScriptAnalyzerRule PSUseConsistentIndentation + $nonConfigurableRule = Get-ScriptAnalyzerRule PSAvoidUsingInvokeExpression + } + + It "returns Options for a configurable rule" { + $configurableRule.Options | Should -Not -BeNullOrEmpty + } + + It "includes the Enable option" { + $configurableRule.Options.Name | Should -Contain 'Enable' + } + + It "places Enable as the first option" { + $configurableRule.Options[0].Name | Should -Be 'Enable' + } + + It "populates PossibleValues for enum-backed string properties" { + $kindOption = $configurableRule.Options | Where-Object Name -eq 'Kind' + $kindOption.PossibleValues | Should -Not -BeNullOrEmpty + $kindOption.PossibleValues | Should -Contain 'Space' + $kindOption.PossibleValues | Should -Contain 'Tab' + } + + It "returns null Options for a non-configurable rule" { + $nonConfigurableRule.Options | Should -BeNullOrEmpty + } +} diff --git a/Tests/Engine/NewScriptAnalyzerSettingsFile.tests.ps1 b/Tests/Engine/NewScriptAnalyzerSettingsFile.tests.ps1 new file mode 100644 index 000000000..f23cdf5f6 --- /dev/null +++ b/Tests/Engine/NewScriptAnalyzerSettingsFile.tests.ps1 @@ -0,0 +1,283 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $settingsFileName = 'PSScriptAnalyzerSettings.psd1' +} + +Describe "New-ScriptAnalyzerSettingsFile" { + Context "When creating a default settings file (no preset)" { + BeforeAll { + $testDir = Join-Path $TestDrive 'default' + New-Item -ItemType Directory -Path $testDir | Out-Null + $result = New-ScriptAnalyzerSettingsFile -Path $testDir + $settingsPath = Join-Path $testDir $settingsFileName + } + + It "Should return a FileInfo object" { + $result | Should -BeOfType ([System.IO.FileInfo]) + } + + It "Should create the settings file" { + $settingsPath | Should -Exist + } + + It "Should produce a valid PSD1 that can be parsed" { + { Import-PowerShellDataFile -Path $settingsPath } | Should -Not -Throw + } + + It "Should contain the IncludeRules key with at least one rule" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('IncludeRules') | Should -BeTrue + $data['IncludeRules'].Count | Should -BeGreaterThan 0 + } + + It "Should contain the ExcludeRules key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('ExcludeRules') | Should -BeTrue + } + + It "Should contain the Severity key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('Severity') | Should -BeTrue + } + + It "Should contain the CustomRulePath key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('CustomRulePath') | Should -BeTrue + } + + It "Should contain the IncludeDefaultRules key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('IncludeDefaultRules') | Should -BeTrue + } + + It "Should contain the RecurseCustomRulePath key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('RecurseCustomRulePath') | Should -BeTrue + } + + It "Should contain the Rules key" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('Rules') | Should -BeTrue + } + + It "Should include all available rules in IncludeRules" { + $data = Import-PowerShellDataFile -Path $settingsPath + $allRules = Get-ScriptAnalyzerRule | ForEach-Object RuleName + foreach ($rule in $allRules) { + $data['IncludeRules'] | Should -Contain $rule + } + } + + It "Should place Enable first in rule settings" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '(?s)PSUseConsistentIndentation = @\{\s+Enable' + } + + It "Should include inline comments listing valid values for constrained properties" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match "# Space, Tab" + } + + It "Should include a comment with valid severity values" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Information, Warning, Error, ParseError' + } + + It "Should be usable with Invoke-ScriptAnalyzer" { + { Invoke-ScriptAnalyzer -ScriptDefinition '"hello"' -Settings $settingsPath } | Should -Not -Throw + } + + It "Should contain a header with the PSScriptAnalyzer version" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# PSScriptAnalyzer settings file \(\d+\.\d+\.\d+\)' + } + + It "Should contain a header with the generation tool name" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Generated by New-ScriptAnalyzerSettingsFile\.' + } + + It "Should not mention a preset in the header" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Not -Match '# Based on the' + } + + It "Should contain a section comment before IncludeRules" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Rules to run\. When populated, only these rules are used\.' + } + + It "Should contain a section comment before ExcludeRules" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Rules to skip\. Takes precedence over IncludeRules\.' + } + + It "Should contain a section comment before Severity" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Only report diagnostics at these severity levels\.' + } + + It "Should contain a section comment before Rules" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Per-rule configuration\. Only configurable rules appear here\.' + } + } + + Context "When creating a settings file based on a preset" { + BeforeAll { + $testDir = Join-Path $TestDrive 'preset' + New-Item -ItemType Directory -Path $testDir | Out-Null + $result = New-ScriptAnalyzerSettingsFile -Path $testDir -BaseOnPreset CodeFormatting + $settingsPath = Join-Path $testDir $settingsFileName + } + + It "Should create the settings file" { + $settingsPath | Should -Exist + } + + It "Should produce a valid PSD1" { + { Import-PowerShellDataFile -Path $settingsPath } | Should -Not -Throw + } + + It "Should contain all top-level fields" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('IncludeRules') | Should -BeTrue + $data.ContainsKey('ExcludeRules') | Should -BeTrue + $data.ContainsKey('Severity') | Should -BeTrue + $data.ContainsKey('CustomRulePath') | Should -BeTrue + $data.ContainsKey('IncludeDefaultRules') | Should -BeTrue + $data.ContainsKey('RecurseCustomRulePath') | Should -BeTrue + $data.ContainsKey('Rules') | Should -BeTrue + } + + It "Should include the preset rules in IncludeRules" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data['IncludeRules'] | Should -Contain 'PSPlaceOpenBrace' + $data['IncludeRules'] | Should -Contain 'PSUseConsistentIndentation' + } + + It "Should include rule configuration from the preset" { + $data = Import-PowerShellDataFile -Path $settingsPath + $data['Rules'].ContainsKey('PSPlaceOpenBrace') | Should -BeTrue + $data['Rules']['PSPlaceOpenBrace']['Enable'] | Should -BeTrue + } + + It "Should be usable with Invoke-ScriptAnalyzer" { + { Invoke-ScriptAnalyzer -ScriptDefinition '"hello"' -Settings $settingsPath } | Should -Not -Throw + } + + It "Should mention the preset name in the header" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match "# Based on the 'CodeFormatting' preset\." + } + + It "Should contain section comments" { + $content = Get-Content -Path $settingsPath -Raw + $content | Should -Match '# Rules to run' + $content | Should -Match '# Rules to skip' + $content | Should -Match '# Only report diagnostics at these severity levels' + $content | Should -Match '# Per-rule configuration' + } + } + + Context "When a settings file already exists at the target path" { + BeforeAll { + $testDir = Join-Path $TestDrive 'exists' + New-Item -ItemType Directory -Path $testDir | Out-Null + Set-Content -Path (Join-Path $testDir $settingsFileName) -Value '@{}' + } + + It "Should throw a terminating error without -Force" { + { New-ScriptAnalyzerSettingsFile -Path $testDir -ErrorAction Stop } | + Should -Throw -ErrorId 'SettingsFileAlreadyExists*' + } + } + + Context "When using -Force to overwrite an existing file" { + BeforeAll { + $testDir = Join-Path $TestDrive 'force' + New-Item -ItemType Directory -Path $testDir | Out-Null + Set-Content -Path (Join-Path $testDir $settingsFileName) -Value '@{}' + $result = New-ScriptAnalyzerSettingsFile -Path $testDir -Force + $settingsPath = Join-Path $testDir $settingsFileName + } + + It "Should overwrite the existing file" { + $settingsPath | Should -Exist + $data = Import-PowerShellDataFile -Path $settingsPath + $data.ContainsKey('IncludeRules') | Should -BeTrue + } + + It "Should return a FileInfo object" { + $result | Should -BeOfType ([System.IO.FileInfo]) + } + } + + Context "When using -WhatIf" { + It "Should not create the settings file" { + $testDir = Join-Path $TestDrive 'whatif' + New-Item -ItemType Directory -Path $testDir | Out-Null + $settingsPath = Join-Path $testDir $settingsFileName + # WhatIf messages are written directly to the host UI by ShouldProcess, + # bypassing all output streams. Run in a new runspace whose default host + # silently discards host output. + $ps = [powershell]::Create() + try { + $null = $ps.AddCommand('Import-Module').AddParameter('Name', (Get-Module PSScriptAnalyzer).Path).Invoke() + $ps.Commands.Clear() + $null = $ps.AddCommand('New-ScriptAnalyzerSettingsFile').AddParameter('Path', $testDir).AddParameter('WhatIf', $true).Invoke() + } + finally { + $ps.Dispose() + } + $settingsPath | Should -Not -Exist + } + } + + Context "When the -Path parameter points to a non-existent directory" { + BeforeAll { + $nestedDir = Join-Path (Join-Path (Join-Path $TestDrive 'nested') 'sub') 'folder' + $result = New-ScriptAnalyzerSettingsFile -Path $nestedDir + $settingsPath = Join-Path $nestedDir $settingsFileName + } + + It "Should create the directory and the settings file" { + $settingsPath | Should -Exist + } + } + + Context "When using the default path (current directory)" { + BeforeAll { + $testDir = Join-Path $TestDrive 'cwd' + New-Item -ItemType Directory -Path $testDir | Out-Null + Push-Location $testDir + $result = New-ScriptAnalyzerSettingsFile + $settingsPath = Join-Path $testDir $settingsFileName + } + + AfterAll { + Pop-Location + } + + It "Should create the file in the current working directory" { + $settingsPath | Should -Exist + } + } + + Context "Generated settings file for each preset" { + BeforeDiscovery { + $presets = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Settings]::GetSettingPresets() | + ForEach-Object { @{ Preset = $_ } } + } + + It "Should produce a valid PSD1 for the '' preset" -TestCases $presets { + $testDir = Join-Path $TestDrive "preset-$Preset" + New-Item -ItemType Directory -Path $testDir | Out-Null + $settingsPath = Join-Path $testDir $settingsFileName + New-ScriptAnalyzerSettingsFile -Path $testDir -BaseOnPreset $Preset + { Import-PowerShellDataFile -Path $settingsPath } | Should -Not -Throw + } + } +} diff --git a/Tests/Engine/TestScriptAnalyzerSettingsFile.tests.ps1 b/Tests/Engine/TestScriptAnalyzerSettingsFile.tests.ps1 new file mode 100644 index 000000000..01d2664d1 --- /dev/null +++ b/Tests/Engine/TestScriptAnalyzerSettingsFile.tests.ps1 @@ -0,0 +1,424 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Test-ScriptAnalyzerSettingsFile" { + Context "Given a valid generated settings file" { + BeforeAll { + $testDir = Join-Path $TestDrive 'valid' + New-Item -ItemType Directory -Path $testDir | Out-Null + New-ScriptAnalyzerSettingsFile -Path $testDir + $settingsPath = Join-Path $testDir 'PSScriptAnalyzerSettings.psd1' + } + + It "Should produce no output when the file is valid" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + + It "Should return true with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeTrue + } + } + + Context "Given a valid preset-based settings file" { + BeforeAll { + $testDir = Join-Path $TestDrive 'preset' + New-Item -ItemType Directory -Path $testDir | Out-Null + New-ScriptAnalyzerSettingsFile -Path $testDir -BaseOnPreset CodeFormatting + $settingsPath = Join-Path $testDir 'PSScriptAnalyzerSettings.psd1' + } + + It "Should produce no output when the file is valid" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + + It "Should return true with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeTrue + } + } + + Context "Given a file that does not exist" { + It "Should write a non-terminating error and produce no output" { + $bogusPath = Join-Path $TestDrive 'nonexistent.psd1' + $result = Test-ScriptAnalyzerSettingsFile -Path $bogusPath -ErrorVariable errs -ErrorAction SilentlyContinue + $result | Should -BeNullOrEmpty + $errs | Should -Not -BeNullOrEmpty + $errs[0].FullyQualifiedErrorId | Should -BeLike 'SettingsFileNotFound*' + } + + It "Should return false with -Quiet" { + $bogusPath = Join-Path $TestDrive 'nonexistent.psd1' + Test-ScriptAnalyzerSettingsFile -Path $bogusPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with an unknown rule name" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'unknown-rule.psd1' + $content = " + @{ + IncludeRules = @( + 'PSBogusRuleThatDoesNotExist' + ) + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0] | Should -BeOfType ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]) + } + + It "Should report the unknown rule name in the message" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Message | Should -BeLike "*PSBogusRuleThatDoesNotExist*" + } + + It "Should include an extent pointing to the offending text" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Extent | Should -Not -BeNullOrEmpty + $result[0].Extent.Text | Should -Be "'PSBogusRuleThatDoesNotExist'" + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with an invalid rule option name" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-option.psd1' + $content = " + @{ + IncludeRules = @('PSUseConsistentIndentation') + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = `$true + CompletelyBogusOption = 42 + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0] | Should -BeOfType ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]) + } + + It "Should report the unrecognised option in the message" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Message | Should -BeLike "*CompletelyBogusOption*unrecognised option*" + } + + It "Should include an extent pointing to the option name" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Extent.Text | Should -Be 'CompletelyBogusOption' + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with an invalid rule option value" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-value.psd1' + $content = " + @{ + IncludeRules = @('PSUseConsistentIndentation') + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = `$true + Kind = 'banana' + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0] | Should -BeOfType ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]) + } + + It "Should report the invalid value in the message" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Message | Should -BeLike "*banana*not a valid value*" + } + + It "Should include an extent pointing to the bad value" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Extent.Text | Should -Be "'banana'" + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with an invalid severity value" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-severity.psd1' + $content = " + @{ + Severity = @('Critical') + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0] | Should -BeOfType ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]) + } + + It "Should report the invalid severity in the message" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Message | Should -BeLike "*Critical*not a valid severity*" + } + + It "Should include an extent pointing to the bad value" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result[0].Extent.Text | Should -Be "'Critical'" + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with wildcard rule names in IncludeRules" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'wildcard.psd1' + $content = " + @{ + IncludeRules = @('PSDSC*') + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should produce no output - wildcards are valid" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + } + + Context "Given an unparseable file" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'broken.psd1' + Set-Content -Path $settingsPath -Value 'this is not valid psd1 content {{{' + } + + It "Should output DiagnosticRecord objects with parse errors" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0] | Should -BeOfType ([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]) + $result[0].Severity | Should -Be 'ParseError' + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "DiagnosticRecord properties" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'diag-props.psd1' + $content = " + @{ + Severity = @('Critical') + } + " + Set-Content -Path $settingsPath -Value $content + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + } + + It "Should set RuleName to Test-ScriptAnalyzerSettingsFile" { + $result[0].RuleName | Should -Be 'Test-ScriptAnalyzerSettingsFile' + } + + It "Should set ScriptPath to the settings file path" { + $result[0].ScriptPath | Should -Be $settingsPath + } + + It "Should set Severity to Error for validation problems" { + $result[0].Severity | Should -Be 'Error' + } + + It "Should include line number information in the extent" { + $result[0].Extent.StartLineNumber | Should -BeGreaterThan 0 + } + } + + Context "Given a file with a wrong type for a bool option" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-bool.psd1' + $content = " + @{ + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = 123 + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord for the type mismatch" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0].Message | Should -BeLike "*Enable*expected a value of type bool*" + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with a string where an int is expected" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-int.psd1' + $content = " + @{ + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = `$true + IndentationSize = 'abc' + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should output a DiagnosticRecord for the type mismatch" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0].Message | Should -BeLike "*IndentationSize*expected a value of type int*" + } + + It "Should return false with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeFalse + } + } + + Context "Given a file with a string where a string array is expected" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'bad-array.psd1' + $content = " + @{ + Rules = @{ + PSUseSingularNouns = @{ + NounAllowList = 'Data' + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should accept a single string for a string array property" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + } + + Context "Given a file with valid types for all options" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'valid-types.psd1' + $content = " + @{ + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = `$true + IndentationSize = 4 + } + } + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should produce no output" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + } + + Context "Given a file with IncludeDefaultRules" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'include-defaults.psd1' + $content = " + @{ + IncludeDefaultRules = `$true + IncludeRules = @('PSUseConsistentIndentation') + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should validate built-in rules when IncludeDefaultRules is true" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + + It "Should return true with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeTrue + } + } + + Context "Given a file with CustomRulePath pointing to community rules" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'custom-rules.psd1' + $communityRulesPath = Join-Path $PSScriptRoot 'CommunityAnalyzerRules' + $content = " + @{ + CustomRulePath = @('$communityRulesPath') + IncludeDefaultRules = `$true + IncludeRules = @('PSUseConsistentIndentation', 'Measure-RequiresModules') + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should validate both built-in and custom rule names" { + $result = Test-ScriptAnalyzerSettingsFile -Path $settingsPath + $result | Should -BeNullOrEmpty + } + + It "Should return true with -Quiet" { + Test-ScriptAnalyzerSettingsFile -Path $settingsPath -Quiet | Should -BeTrue + } + } + + Context "Given a file with CustomRulePath but without IncludeDefaultRules" { + BeforeAll { + $settingsPath = Join-Path $TestDrive 'custom-no-defaults.psd1' + $communityRulesPath = Join-Path $PSScriptRoot 'CommunityAnalyzerRules' + $content = " + @{ + CustomRulePath = @('$communityRulesPath') + IncludeRules = @('PSUseConsistentIndentation') + } + " + Set-Content -Path $settingsPath -Value $content + } + + It "Should report built-in rules as unknown when IncludeDefaultRules is not set" { + $result = @(Test-ScriptAnalyzerSettingsFile -Path $settingsPath) + $result.Count | Should -BeGreaterThan 0 + $result[0].Message | Should -BeLike "*PSUseConsistentIndentation*" + } + } +} diff --git a/docs/Cmdlets/New-ScriptAnalyzerSettingsFile.md b/docs/Cmdlets/New-ScriptAnalyzerSettingsFile.md new file mode 100644 index 000000000..30b27b978 --- /dev/null +++ b/docs/Cmdlets/New-ScriptAnalyzerSettingsFile.md @@ -0,0 +1,187 @@ +--- +external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml +Module Name: PSScriptAnalyzer +ms.date: 04/17/2026 +schema: 2.0.0 +--- + +# New-ScriptAnalyzerSettingsFile + +## SYNOPSIS +Creates a new PSScriptAnalyzer settings file. + +## SYNTAX + +``` +New-ScriptAnalyzerSettingsFile [[-Path] ] [-BaseOnPreset ] [-Force] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION + +The `New-ScriptAnalyzerSettingsFile` cmdlet creates a `PSScriptAnalyzerSettings.psd1` file in the +specified directory. + +When the **BaseOnPreset** parameter is provided, the generated file contains the rules and +configuration defined by the given preset. + +When **BaseOnPreset** is not provided, the generated file includes all current rules in the +`IncludeRules` list and populates the `Rules` section with all configurable properties, set to their +default values. + +If a settings file already exists at the target path, the cmdlet emits a terminating error unless +the **Force** parameter is specified - in which case it's overwritten. + +## EXAMPLES + +### EXAMPLE 1 - Create a default settings file in the current directory + +```powershell +New-ScriptAnalyzerSettingsFile +``` + +Creates `PSScriptAnalyzerSettings.psd1` in the current working directory including all rules and +all configurable options set to their defaults. + +### EXAMPLE 2 - Create a settings file based on a preset + +```powershell +New-ScriptAnalyzerSettingsFile -BaseOnPreset CodeFormatting +``` + +Creates a settings file pre-populated with the rules and configuration from the `CodeFormatting` +preset. + +### EXAMPLE 3 - Create a settings file in a specific directory + +```powershell +New-ScriptAnalyzerSettingsFile -Path ./src/MyModule +``` + +Creates the settings file in the `./src/MyModule` directory. + +### EXAMPLE 4 - Preview the operation without creating the file + +```powershell +New-ScriptAnalyzerSettingsFile -WhatIf +``` + +Shows what the cmdlet would do without actually writing the file. + +## PARAMETERS + +### -Path + +The directory where the settings file will be created. Defaults to the current working directory when not specified. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: Current directory +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -BaseOnPreset + +The name of a built-in preset to use as the basis for the generated settings file. Valid values are +discovered at runtime from the shipped preset files and can be tab-completed in the shell. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would happen if the cmdlet runs. The cmdlet is not run. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrite an existing settings file at the target path. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm + +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +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). + +## INPUTS + +### None + +## OUTPUTS + +### System.IO.FileInfo + +The cmdlet returns a **FileInfo** object representing the created settings file. + +## NOTES + +The output file is always named `PSScriptAnalyzerSettings.psd1` so that the automatic settings +discovery in `Invoke-ScriptAnalyzer` picks it up when analysing scripts in the same directory. + +Note: Relative paths in `CustomRulePath` are resolved from the caller's current working directory, +Relative paths in `CustomRulePath` are resolved from the caller's current working directory, +not from the location of the settings file. This matches `Invoke-ScriptAnalyzer` behavior. + +## RELATED LINKS + +[Invoke-ScriptAnalyzer](Invoke-ScriptAnalyzer.md) + +[Get-ScriptAnalyzerRule](Get-ScriptAnalyzerRule.md) + +[Invoke-Formatter](Invoke-Formatter.md) + +[Test-ScriptAnalyzerSettingsFile](Test-ScriptAnalyzerSettingsFile.md) diff --git a/docs/Cmdlets/Test-ScriptAnalyzerSettingsFile.md b/docs/Cmdlets/Test-ScriptAnalyzerSettingsFile.md new file mode 100644 index 000000000..b313f93dd --- /dev/null +++ b/docs/Cmdlets/Test-ScriptAnalyzerSettingsFile.md @@ -0,0 +1,151 @@ +--- +external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml +Module Name: PSScriptAnalyzer +ms.date: 04/17/2026 +schema: 2.0.0 +--- + +# Test-ScriptAnalyzerSettingsFile + +## SYNOPSIS +Validates a PSScriptAnalyzer settings file as a self-contained unit. + +## SYNTAX + +``` +Test-ScriptAnalyzerSettingsFile [-Path] [-Quiet] [] +``` + +## DESCRIPTION + +The `Test-ScriptAnalyzerSettingsFile` cmdlet validates a PSScriptAnalyzer settings file as a +self-contained unit. It reads `CustomRulePath`, `RecurseCustomRulePath`, and `IncludeDefaultRules` +directly from the file so that validation reflects the same rule set `Invoke-ScriptAnalyzer` would +see when given the same file. + +The cmdlet verifies that: + +- The file can be parsed as a PowerShell data file. +- All rule names referenced in `IncludeRules`, `ExcludeRules`, and `Rules` correspond to known + rules (wildcard patterns are skipped). +- All `Severity` values are valid. +- Rule option names in the `Rules` section correspond to actual configurable properties. +- Rule option values that are constrained to a set of choices contain a valid value. + +By default, when problems are found the cmdlet outputs a `DiagnosticRecord` for each one, with the +source extent pointing to the offending text in the file. This is the same object type returned by +`Invoke-ScriptAnalyzer`, so existing formatting and tooling works out of the box. When the file is +valid, no output is produced. + +When `-Quiet` is specified the cmdlet returns only `$true` or `$false` and suppresses all +diagnostic output. + +## EXAMPLES + +### EXAMPLE 1 - Validate a settings file + +```powershell +Test-ScriptAnalyzerSettingsFile -Path ./PSScriptAnalyzerSettings.psd1 +``` + +Outputs a `DiagnosticRecord` for each problem found, with line and column information. Produces no +output when the file is valid. + +### EXAMPLE 2 - Validate quietly in a conditional + +```powershell +if (Test-ScriptAnalyzerSettingsFile -Path ./PSScriptAnalyzerSettings.psd1 -Quiet) { + Invoke-ScriptAnalyzer -Path ./src -Settings ./PSScriptAnalyzerSettings.psd1 +} +``` + +Returns `$true` or `$false` without producing diagnostic output. + +### EXAMPLE 3 - Validate a file that uses custom rules + +```powershell +# Settings.psd1 contains CustomRulePath and IncludeDefaultRules keys. +# The cmdlet reads those from the file directly — no extra parameters needed. +Test-ScriptAnalyzerSettingsFile -Path ./Settings.psd1 +``` + +Validates rule names against both built-in and custom rules as specified in the settings file. + +## PARAMETERS + +### -Path + +The path to the settings file to validate. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Quiet + +Suppresses diagnostic output and returns only `$true` or `$false`. Without this switch the cmdlet +outputs a `DiagnosticRecord` for each problem found and produces no output when the file is valid. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +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). + +## INPUTS + +### None + +## OUTPUTS + +### Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord + +Without `-Quiet`, a `DiagnosticRecord` is output for each problem found. Each record includes the +error message, the source extent (file, line and column), a severity, and the rule name +`Test-ScriptAnalyzerSettingsFile`. No output is produced when the file is valid. + +### System.Boolean + +With `-Quiet`, returns `$true` when the file is valid and `$false` otherwise. + +## NOTES + +The cmdlet reads `CustomRulePath`, `RecurseCustomRulePath`, and `IncludeDefaultRules` from the +settings file so it validates rule names against the same set of rules that `Invoke-ScriptAnalyzer` +would load. This means the settings file is validated as a self-contained unit without requiring +extra command-line parameters. + +Note: Relative paths in `CustomRulePath` are resolved from the caller's current working directory, +not from the location of the settings file. This matches `Invoke-ScriptAnalyzer` behaviour. + +The `DiagnosticRecord` objects use the same type as `Invoke-ScriptAnalyzer`, so they benefit from +the same default formatting and can be piped to the same downstream tooling. + +## RELATED LINKS + +[New-ScriptAnalyzerSettingsFile](New-ScriptAnalyzerSettingsFile.md) + +[Invoke-ScriptAnalyzer](Invoke-ScriptAnalyzer.md) + +[Get-ScriptAnalyzerRule](Get-ScriptAnalyzerRule.md) From 97f30d63d8ec2ff4cec492bcb52dae895389c4e1 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Fri, 15 May 2026 20:05:30 +0200 Subject: [PATCH 127/130] Add new MissingTryBlock rule (#2179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #2098 Add new MissingTryBlock rule * Changed severity level of MissingTryBlock rule from Error to Warning in the rule definition and resolved copy-pasta in the rule description. Co-authored-by: Copilot * Updated tests for MissingTryBlock rule to reflect severity change from Error to Warning. * Update Rules/MissingTryBlock.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Grammar: “which is likely a mistake and result in … error” is ungrammatical. * Update Rules/Strings.resx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Rules/MissingTryBlock.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * minor but worthwhile * Update docs/Rules/MissingTryBlock.md Co-authored-by: Sean Wheeler * Applied feedback from @andrewconnell to add a note about the rule not being enabled by default, and to add a note about potential false positives with functions named "catch" or "finally". Also added a test context for when the rule is disabled. Updated the rule implementation to inherit from ConfigurableRule and set Enable to false in the constructor. Updated the AnalyzeScript method to be an override, and added overrides for GetCommonName, GetDescription, GetName, GetSeverity, and GetSourceName. * Update docs/Rules/MissingTryBlock.md Co-authored-by: Liam Peters * Update Tests/Rules/MissingTryBlock.tests.ps1 Co-authored-by: Liam Peters * Update Rules/MissingTryBlock.cs Co-authored-by: Liam Peters * Update Rules/MissingTryBlock.cs Co-authored-by: Liam Peters --------- Co-authored-by: Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sean Wheeler Co-authored-by: Liam Peters --- Rules/MissingTryBlock.cs | 128 +++++++++++++++++++++ Rules/Strings.resx | 12 ++ Tests/Rules/MissingTryBlock.tests.ps1 | 158 ++++++++++++++++++++++++++ docs/Rules/MissingTryBlock.md | 65 +++++++++++ docs/Rules/README.md | 1 + 5 files changed, 364 insertions(+) create mode 100644 Rules/MissingTryBlock.cs create mode 100644 Tests/Rules/MissingTryBlock.tests.ps1 create mode 100644 docs/Rules/MissingTryBlock.md diff --git a/Rules/MissingTryBlock.cs b/Rules/MissingTryBlock.cs new file mode 100644 index 000000000..3a415f647 --- /dev/null +++ b/Rules/MissingTryBlock.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that warns when catch or finally blocks are used without a corresponding try block + /// + + public class MissingTryBlock : ConfigurableRule + { + + /// + /// Construct an object of MissingTryBlock type. + /// + public MissingTryBlock() { + Enable = false; + } + + /// + /// Find bare word "catch" or "finally" tokens that are not part of a TryStatementAst + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + // Find the bare word 'catch' or 'finally' StringConstantExpressionAst nodes used as commands + var missingTryAsts = ast.FindAll(testAst => + // Normally should be part of a TryStatementAst + testAst is StringConstantExpressionAst stringAst && + // Check whether "catch" or "finally" are bare words + stringAst.StringConstantType == StringConstantType.BareWord && + ( + String.Equals(stringAst.Value, "catch", StringComparison.OrdinalIgnoreCase) || + String.Equals(stringAst.Value, "finally", StringComparison.OrdinalIgnoreCase) + ) && + stringAst.Parent is CommandAst commandAst && + // Only violate if the catch or finally is the first command element + commandAst.CommandElements[0] == stringAst, + true + ); + + foreach (StringConstantExpressionAst missingTryAst in missingTryAsts) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.MissingTryBlockError, + CultureInfo.CurrentCulture.TextInfo.ToTitleCase(missingTryAst.Value)), + missingTryAst.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + missingTryAst.Value + ); + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.MissingTryBlockCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.MissingTryBlockDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.MissingTryBlockName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} + diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 2a04fd759..a296b04f3 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -276,6 +276,18 @@ Module Manifest Fields + + MissingTryBlock + + + Missing Try Block + + + The catch and finally blocks should be preceded by a try block. + + + {0} is missing a try block + If a script file is in a PowerShell module folder, then that folder must be loadable. diff --git a/Tests/Rules/MissingTryBlock.tests.ps1 b/Tests/Rules/MissingTryBlock.tests.ps1 new file mode 100644 index 000000000..20b25afb8 --- /dev/null +++ b/Tests/Rules/MissingTryBlock.tests.ps1 @@ -0,0 +1,158 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + +BeforeAll { + $ruleName = "PSMissingTryBlock" +} + +Describe "MissingTryBlock" { + + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $true } } + } + } + + Context "Violates" { + It "Catch is missing a try block" { + $scriptDefinition = { catch { "An error occurred." } }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be catch + $violations.Message | Should -Be 'Catch is missing a try block' + $violations.RuleSuppressionID | Should -Be catch + } + + It "Finally is missing a try block" { + $scriptDefinition = { finally { "Finalizing..." } }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be finally + $violations.Message | Should -Be 'Finally is missing a try block' + $violations.RuleSuppressionID | Should -Be finally + } + + It "Single line catch and finally is missing a try block" { + $scriptDefinition = { + catch { "An error occurred." } finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be catch + $violations.Message | Should -Be 'Catch is missing a try block' + $violations.RuleSuppressionID | Should -Be catch + } + + It "Multi line catch and finally is missing a try block" { + $scriptDefinition = { + catch { "An error occurred." } + finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 2 + $violations[0].Severity | Should -Be Warning + $violations[0].Extent.Text | Should -Be catch + $violations[0].Message | Should -Be 'Catch is missing a try block' + $violations[0].RuleSuppressionID | Should -Be catch + $violations[1].Severity | Should -Be Warning + $violations[1].Extent.Text | Should -Be finally + $violations[1].Message | Should -Be 'Finally is missing a try block' + $violations[1].RuleSuppressionID | Should -Be finally + } + } + + Context "Compliant" { + It "try-catch block" { + $scriptDefinition = { + try { NonsenseString } + catch { "An error occurred." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "try-catch-final statement" { + $scriptDefinition = { + try { NonsenseString } + catch { "An error occurred." } + finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Single line try statement" { + $scriptDefinition = { + try { NonsenseString } catch { "An error occurred." } finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Catch as parameter" { + $scriptDefinition = { Write-Host Catch }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Catch as double quoted string" { + $scriptDefinition = { "Catch" }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Catch as single quoted string" { + $scriptDefinition = { 'Catch' }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Suppressed" { + It "Multi line catch and finally is missing a try block" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSMissingTryBlock', '', Justification = 'Test')] + param() + catch { "An error occurred." } + finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Multi line catch and finally is missing a try block for catch only" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSMissingTryBlock', 'finally', Justification = 'Test')] + param() + catch { "An error occurred." } + finally { "Finalizing..." } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + } + } + + Context "Disabled" { + + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $false } } + } + } + + It "Doesn't emit a violation" { + $scriptDefinition = { catch { "An error occurred." } }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + +} \ No newline at end of file diff --git a/docs/Rules/MissingTryBlock.md b/docs/Rules/MissingTryBlock.md new file mode 100644 index 000000000..5265259d1 --- /dev/null +++ b/docs/Rules/MissingTryBlock.md @@ -0,0 +1,65 @@ +--- +description: Missing Try Block +ms.date: 04/22/2026 +ms.topic: reference +title: MissingTryBlock +--- +# MissingTryBlock + +**Severity Level: Warning** + +## Description + +The `catch` and `finally` blocks must be preceded by a `try` block. Without a `try` block, the +`catch` and `finally` are interpreted as commands and result in a runtime error, such as: + +> "The term 'catch' is not recognized as a name of a cmdlet" + +This rule identifies instances where `catch` or `finally` blocks are present with out an associated +`try` block. + +> [!NOTE] +> This rule is not enabled by default. The user needs to enable it through settings. + +## How + +Add a `try` block before the `catch` and `finally` blocks. + +> [!NOTE] +> This rule could result in a false positive as it will fire on user code that violates the rule +> [AvoidReservedWordsAsFunctionNames][1] for functions named `catch` or `finally`: +> If you have functions named `catch` or `finally`, you can either rename the function or disable +> this rule. + +## Example + +### Wrong + +```powershell +catch { "An error occurred." } +``` + +### Correct + +```powershell +try { $a = 1 / $b } +catch { "Attempted to divide by zero." } +``` + +## Configuration + +```powershell +Rules = @{ + PSMissingTryBlock = @{ + Enable = $true + } +} +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. + +[1]: AvoidReservedWordsAsFunctionNames.md "Avoid using reserved words as function names." \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index fca031e33..73e09a4da 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -50,6 +50,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [DSCUseVerboseMessageInDSCResource](./DSCUseVerboseMessageInDSCResource.md) | Error | Yes | | | [MisleadingBacktick](./MisleadingBacktick.md) | Warning | Yes | | | [MissingModuleManifestField](./MissingModuleManifestField.md) | Warning | Yes | | +| [MissingTryBlock](./MissingTryBlock.md) | Warning | No | Yes | | [PlaceCloseBrace](./PlaceCloseBrace.md) | Warning | No | Yes | | [PlaceOpenBrace](./PlaceOpenBrace.md) | Warning | No | Yes | | [PossibleIncorrectComparisonWithNull](./PossibleIncorrectComparisonWithNull.md) | Warning | Yes | | From 6dadae3b7cf6a5913d8d4470012f2e4e10832b01 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Mon, 18 May 2026 19:19:03 +0200 Subject: [PATCH 128/130] Add new AvoidUsingArrayList rule (#2174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented the AvoidUsingArrayList rule to warn when the ArrayList class is used in PowerShell scripts. Added tests for both violations and non-violations of this rule. Updated documentation to include the new rule and its guidelines. * Testing-Commit-CSpell-issue * Apply suggestion from @liamjpeters I clearly used several other rules as a kind of template...🤪 Co-authored-by: Liam Peters * Update docs/Rules/AvoidUsingArrayList.md Co-authored-by: Liam Peters * Updated rule help * Changed "unintentionally" * Update Rules/AvoidUsingArrayList.cs Co-authored-by: Liam Peters * Update Tests/Rules/AvoidUsingArrayList.tests.ps1 Co-authored-by: Liam Peters * ArrayListName could be null * Resolved camelCase * Remove ComponentModel namespace * Fixed tests * Updated Tests * fixed and tested empty (dynamic) BoundParameter * Robuster Pester tests * Configurable (enable by default) * Fixed ConstantValue null check test and rule * Better UsingStatements handling and disable AvoidUsingArrayList by default * `[ArrayList]::new()` without a `using namespace System.Collections` * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Resolve Copilot suggestions and resolved `Method not found: ScriptBlockAst.get_UsingStatements()` error --------- Co-authored-by: Liam Peters Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Rules/AvoidUsingArrayList.cs | 195 +++++++++++++++ Rules/Strings.resx | 12 + Tests/Rules/AvoidUsingArrayList.tests.ps1 | 281 ++++++++++++++++++++++ docs/Rules/AvoidUsingArrayList.md | 61 +++++ docs/Rules/README.md | 1 + 5 files changed, 550 insertions(+) create mode 100644 Rules/AvoidUsingArrayList.cs create mode 100644 Tests/Rules/AvoidUsingArrayList.tests.ps1 create mode 100644 docs/Rules/AvoidUsingArrayList.md diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs new file mode 100644 index 000000000..14808d2b3 --- /dev/null +++ b/Rules/AvoidUsingArrayList.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System.Text.RegularExpressions; +using System.Linq; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// AvoidUsingArrayList: Checks for use of the ArrayList class + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class AvoidUsingArrayList : ConfigurableRule + { + + /// + /// Construct an object of AvoidUsingArrayList type. + /// + public AvoidUsingArrayList() { + Enable = false; + } + + /// + /// Analyzes the given ast to find the [violation] + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) { throw new ArgumentNullException(nameof(ast), Strings.NullAstErrorMessage); } + + // If there is an using statement for the Collections namespace, check for the full typename. + // Otherwise also check for the bare ArrayList name. + Regex arrayListName = null; + if (ast is ScriptBlockAst sbAst) { + // sbAst.UsingStatements causes an error: Method not found: ScriptBlockAst.get_UsingStatements() + IEnumerable usingStatements = usingStatements = sbAst.FindAll(testAst => testAst is UsingStatementAst, false); + foreach (UsingStatementAst usingAst in usingStatements.Cast()) + { + if ( + usingAst.UsingStatementKind == UsingStatementKind.Namespace && + ( + usingAst.Name.Value.Equals("Collections", StringComparison.OrdinalIgnoreCase) || + usingAst.Name.Value.Equals("System.Collections", StringComparison.OrdinalIgnoreCase) + ) + ) + { + arrayListName = new Regex(@"^((System\.)?Collections\.)?ArrayList$", RegexOptions.IgnoreCase); + break; + } + } + } + if (arrayListName == null) { arrayListName = new Regex(@"^(System\.)?Collections\.ArrayList$", RegexOptions.IgnoreCase); } + + // Find all type initializers that create a new instance of the ArrayList class. + IEnumerable typeAsts = ast.FindAll(testAst => + ( + testAst is ConvertExpressionAst convertAst && + convertAst.StaticType != null && + convertAst.StaticType.FullName == "System.Collections.ArrayList" + ) || + ( + testAst is TypeExpressionAst typeAst && + typeAst.TypeName != null && + arrayListName.IsMatch(typeAst.TypeName.Name) && + typeAst.Parent is InvokeMemberExpressionAst parentAst && + parentAst.Member != null && + parentAst.Member is StringConstantExpressionAst memberAst && + memberAst.Value.Equals("new", StringComparison.OrdinalIgnoreCase) + ), + true + ); + + foreach (Ast typeAst in typeAsts) + { + IScriptExtent Extent = typeAst is ConvertExpressionAst? typeAst.Extent : typeAst.Parent.Extent; + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidUsingArrayListError, + Extent.Text), + Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + + // Find all New-Object cmdlets that create a new instance of the ArrayList class. + var newObjectCommands = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdAst.GetCommandName() != null && + cmdAst.GetCommandName().Equals("New-Object", StringComparison.OrdinalIgnoreCase), + true); + + foreach (CommandAst cmd in newObjectCommands) + { + // Use StaticParameterBinder to reliably get parameter values + var bindingResult = StaticParameterBinder.BindCommand(cmd, true); + + // Check for -TypeName parameter + if ( + bindingResult.BoundParameters.TryGetValue("TypeName", out ParameterBindingResult typeNameBinding) && + typeNameBinding.ConstantValue is string typeName && + arrayListName.IsMatch(typeName) + ) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidUsingArrayListError, + cmd.Extent.Text), + cmd.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingArrayListCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingArrayListDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidUsingArrayListName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} + diff --git a/Rules/Strings.resx b/Rules/Strings.resx index a296b04f3..4e79088d3 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -945,6 +945,18 @@ Line ends with a semicolon + + Avoid using the ArrayList class + + + Avoid using the ArrayList class in PowerShell scripts. Consider using generic collections or fixed arrays instead. + + + AvoidUsingArrayList + + + The ArrayList class is used in '{0}'. Consider using a generic collection or a fixed array instead. + PlaceOpenBrace diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 new file mode 100644 index 000000000..dc32a7a8b --- /dev/null +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -0,0 +1,281 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +using namespace System.Management.Automation.Language + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + +BeforeAll { + $ruleName = "PSAvoidUsingArrayList" + $ruleMessage = "The ArrayList class is used in '{0}'. Consider using a generic collection or a fixed array instead." + $usingCollections = 'using namespace system.collections' + [Environment]::NewLine + $usingGeneric = 'using namespace System.Collections.Generic' + [Environment]::NewLine +} + +Describe "AvoidArrayList" { + + BeforeAll { + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $true } } + } + } + + Context "When there are violations" { + + It "Unquoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object ArrayList}) + } + + It "Single quoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object 'ArrayList' + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object 'ArrayList'}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object 'ArrayList'}) + } + + It "Double quoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object "ArrayList" + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object "ArrayList"}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object "ArrayList"}) + } + + It "New-Object with full parameter name" { + $scriptDefinition = $usingCollections + { + $List = New-Object -TypeName ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -TypeName ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -TypeName ArrayList}) + } + + It "New-Object with abbreviated parameter name and odd casing" { + $scriptDefinition = $usingCollections + { + $List = New-Object -Type ArrayLIST + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -Type ArrayLIST}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -Type ArrayLIST}) + } + + It "New-Object with full type name" { + $scriptDefinition = $usingCollections + { + $List = New-Object -TypeName System.Collections.ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -TypeName System.Collections.ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -TypeName System.Collections.ArrayList}) + } + + It "New-Object with semi full type name and odd casing" { + $scriptDefinition = $usingCollections + { + $List = New-Object COLLECTIONS.ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object COLLECTIONS.ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object COLLECTIONS.ArrayList}) + } + + It "Type initializer with 3 parameters" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList](1,2,3) + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList](1,2,3)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList](1,2,3)}) + } + + It "Type initializer with array parameters" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList]@(1,2,3) + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList]@(1,2,3)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList]@(1,2,3)}) + } + + It "Type initializer with new constructor" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList]::new()}) + } + + It "Full type name initializer with new constructor" { + $scriptDefinition = $usingCollections + { + $List = [System.Collections.ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[System.Collections.ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[System.Collections.ArrayList]::new()}) + } + + It "Semi full type name initializer with new constructor and odd casing" { + $scriptDefinition = $usingCollections + { + $List = [COLLECTIONS.ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[COLLECTIONS.ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[COLLECTIONS.ArrayList]::new()}) + } + } + + Context "When there are no violations" { + + It "New-Object List[Object]" { + $scriptDefinition = { + $List = New-Object List[Object] + 1..3 | ForEach-Object { $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "[List[Object]]::new()" { + $scriptDefinition = { + $List = [List[Object]]::new() + 1..3 | ForEach-Object { $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Using the pipeline" { + $scriptDefinition = { + $List = 1..3 | ForEach-Object { $_ } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "new()" { + $scriptDefinition = { + $List = [ArrayList]::new() + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Out of the namespace scope" { + $scriptDefinition = $usingGeneric + { + $List = New-Object ArrayList + $List = [ArrayList](1,2,3) + $List = [ArrayList]@(1,2,3) + $List = [ArrayList]::new() + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Disabled" { + + It "New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + + It "Type initializer" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList](1,2,3) + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + + It "New constructor" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + } + + Context "Explicitly disabled" { + + BeforeAll { + $settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $false } } + } + } + + It "New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Test for potential errors" { + + It "Dynamic types shouldn't error" { + $scriptDefinition = { + $type = "System.Collections.ArrayList" + New-Object -TypeName $type + }.ToString() + + $analyzer = { Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings } + $analyzer | Should -Not -Throw # but won't violate either (too complex to cover) + } + } +} diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md new file mode 100644 index 000000000..35c776f10 --- /dev/null +++ b/docs/Rules/AvoidUsingArrayList.md @@ -0,0 +1,61 @@ +--- +description: Avoid using ArrayList +ms.date: 04/16/2025 +ms.topic: reference +title: AvoidUsingArrayList +--- +# AvoidUsingArrayList + +**Severity Level: Warning** + +## Description + +Per .NET best practices, the [`ArrayList` class][1] is not recommended for new development, +the same recommendation applies to PowerShell: + +Avoid the ArrayList class for new development. +The `ArrayList` class is a non-generic collection that can hold objects of any type. +This is in line with the fact that PowerShell is a weakly typed language. However, the +`ArrayList` class does not provide any explicit type safety and performance benefits +of generic collections. Instead of using an `ArrayList`, consider using either a +[`System.Collections.Generic.List[Object]`][2] class or a fixed PowerShell array. +Besides, the `ArrayList.Add` method returns the index of the added element which often +unintentionally pollutes the PowerShell pipeline and therefore might cause unexpected issues. + +## How to Fix + +In cases where only the `Add` method is used, you might just replace the `ArrayList` class +with a generic `List[Object]` class but you could also consider using the idiomatic PowerShell +pipeline syntax instead. + +## Example + +### Wrong + +```powershell +# Using an ArrayList +$List = [System.Collections.ArrayList]::new() +1..3 | ForEach-Object { $List.Add($_) } # Note that this will return the index of the added element +``` + +### Correct + +```powershell +# Using a generic List +$List = [System.Collections.Generic.List[Object]]::new() +1..3 | ForEach-Object { $List.Add($_) } # This will not return anything +``` + +```powershell +# Creating a fixed array by using the PowerShell pipeline +$List = 1..3 | ForEach-Object { $_ } +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. + +[1]: https://learn.microsoft.com/dotnet/api/system.collections.arraylist "ArrayList Class" +[2]: https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1 "List Class" \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 73e09a4da..19bd798ce 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -28,6 +28,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | | | [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | | | [AvoidUsingAllowUnencryptedAuthentication](./AvoidUsingAllowUnencryptedAuthentication.md) | Warning | Yes | | +| [AvoidUsingArrayList](./AvoidUsingArrayList.md) | Warning | No | Yes | | [AvoidUsingBrokenHashAlgorithms](./AvoidUsingBrokenHashAlgorithms.md) | Warning | Yes | | | [AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | Yes2 | | [AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | Yes | | From eb6cebea76c8238a486ff3527d9b30c8ed704967 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Mon, 18 May 2026 19:20:05 +0200 Subject: [PATCH 129/130] Add new InvalidMultiDotValue rule (#2180) * Add new InvalidMultiDotValue rule * Fixed several issues as commented by Liam. Co-authored-by: Copilot * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Improved wording based on Copilot suggestions. * Update docs/Rules/InvalidMultiDotValue.md Co-authored-by: Sean Wheeler * Update docs/Rules/InvalidMultiDotValue.md Co-authored-by: Sean Wheeler * **opt-in by default.** * Remove dead code and change `PSAvoidExclaimOperator` from documentation --------- Co-authored-by: Copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sean Wheeler --- Rules/InvalidMultiDotValue.cs | 155 +++++++++++++++++ Rules/Strings.resx | 15 ++ Tests/Rules/InvalidMultiDotValue.tests.ps1 | 191 +++++++++++++++++++++ docs/Rules/InvalidMultiDotValue.md | 62 +++++++ docs/Rules/README.md | 1 + 5 files changed, 424 insertions(+) create mode 100644 Rules/InvalidMultiDotValue.cs create mode 100644 Tests/Rules/InvalidMultiDotValue.tests.ps1 create mode 100644 docs/Rules/InvalidMultiDotValue.md diff --git a/Rules/InvalidMultiDotValue.cs b/Rules/InvalidMultiDotValue.cs new file mode 100644 index 000000000..008d2cc79 --- /dev/null +++ b/Rules/InvalidMultiDotValue.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation.Language; +using System.Linq; + + + +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that reports an error when an unquoted value contains multiple dots, + /// which is likely an attempt to construct a version number (e.g., 1.2.3) + /// that is not properly quoted and thus misinterpreted as a double with member access. + /// + public class InvalidMultiDotValue : ConfigurableRule + { + + /// + /// Construct an object of InvalidMultiDotValue type. + /// + public InvalidMultiDotValue() { + Enable = false; + } + + /// + /// Analyzes the given ast to find the [violation] + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + // Find all MemberExpressionAst nodes representing invalid unquoted multi-dot values + IEnumerable invalidAsts = ast.FindAll(testAst => + // An expression with 3 or more dots is seen as a double with an additional property + testAst is MemberExpressionAst memberAst && + // The first two values are seen as a double + memberAst.Expression.StaticType == typeof(double) && + // the rest is seen as a member of type int or double + memberAst.Member is ConstantExpressionAst constantAst && + ( + constantAst.StaticType == typeof(int) || // e.g.: [Version]1.2.3 + constantAst.StaticType == typeof(double) // e.g.: [Version]1.2.3.4 + ), + true + ).Cast(); + + var correctionDescription = Strings.InvalidMultiDotValueCorrectionDescription; + foreach (MemberExpressionAst invalidAst in invalidAsts) + { + var corrections = new List { + new CorrectionExtent( + invalidAst.Extent.StartLineNumber, + invalidAst.Extent.EndLineNumber, + invalidAst.Extent.StartColumnNumber, + invalidAst.Extent.EndColumnNumber, + "'" + invalidAst.Extent.Text + "'", + fileName, + correctionDescription + ) + }; + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.InvalidMultiDotValueError, + invalidAst.Extent.Text + ), + invalidAst.Extent, + GetName(), + DiagnosticSeverity.Error, + fileName, + invalidAst.Extent.Text, + suggestedCorrections: corrections + ); + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.InvalidMultiDotValueCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.InvalidMultiDotValueDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.InvalidMultiDotValueName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} + diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 4e79088d3..cf399d783 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1245,6 +1245,21 @@ The insecure AllowUnencryptedAuthentication switch was used. This should be avoided except for compatibility with legacy systems. + + Invalid Multi-Dot Value + + + PowerShell does not support an implicit value with multiple dots. Any unquoted value with 2 or more dots will result in `$null`. + + + InvalidMultiDotValue + + + The unquoted '{0}' expression is not a valid syntax. Types with multiple dots need to be constructed from either a quoted string or individual components. + + + Quote the value that contains multiple dots + AvoidUsingAllowUnencryptedAuthentication diff --git a/Tests/Rules/InvalidMultiDotValue.tests.ps1 b/Tests/Rules/InvalidMultiDotValue.tests.ps1 new file mode 100644 index 000000000..7d4bc9e68 --- /dev/null +++ b/Tests/Rules/InvalidMultiDotValue.tests.ps1 @@ -0,0 +1,191 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + +BeforeAll { + $ruleName = "PSInvalidMultiDotValue" + $ruleMessage = "The unquoted '{0}' expression is not a valid syntax. Types with multiple dots need to be constructed from either a quoted string or individual components." + $correctionDescription = 'Quote the value that contains multiple dots' +} + +Describe "InvalidMultiDotValue" { + + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $true } } + } + } + + Context "Violates" { + It "3 version components" { + $scriptDefinition = { $version = 1.2.3 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Error + $violations.Extent.Text | Should -Be '1.2.3' + $violations.Message | Should -Be ($ruleMessage -f '1.2.3') + $violations.RuleSuppressionID | Should -Be '1.2.3' + $violations.SuggestedCorrections.Text | Should -Be "'1.2.3'" + $violations.SuggestedCorrections.Description | Should -Be $correctionDescription + } + + It "4 version components" { + $scriptDefinition = { $version = 1.2.3.4 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Error + $violations.Extent.Text | Should -Be '1.2.3.4' + $violations.Message | Should -Be ($ruleMessage -f '1.2.3.4') + $violations.RuleSuppressionID | Should -Be '1.2.3.4' + $violations.SuggestedCorrections.Text | Should -Be "'1.2.3.4'" + $violations.SuggestedCorrections.Description | Should -Be $correctionDescription + } + + + It "With class initializer" { + $scriptDefinition = { $version = [Version]1.2.3 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Error + $violations.Extent.Text | Should -Be '1.2.3' + $violations.Message | Should -Be ($ruleMessage -f '1.2.3') + $violations.RuleSuppressionID | Should -Be '1.2.3' + $violations.SuggestedCorrections.Text | Should -Be "'1.2.3'" + $violations.SuggestedCorrections.Description | Should -Be $correctionDescription + } + + It "As parameter" { + $scriptDefinition = { + param( + [Version]$version = 1.2.3 + ) + Write-Verbose $version + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Error + $violations.Extent.Text | Should -Be '1.2.3' + $violations.Message | Should -Be ($ruleMessage -f '1.2.3') + $violations.RuleSuppressionID | Should -Be '1.2.3' + $violations.SuggestedCorrections.Text | Should -Be "'1.2.3'" + $violations.SuggestedCorrections.Description | Should -Be $correctionDescription + } + + # Even an IP address is apparently expect below. + # The violation message and description presume a version + # is expected because this is the more commonly used type. + It "IP Address" { + $scriptDefinition = { $IP = [System.Net.IPAddress]127.0.0.1 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Error + $violations.Extent.Text | Should -Be '127.0.0.1' + $violations.Message | Should -Be ($ruleMessage -f '127.0.0.1') + $violations.RuleSuppressionID | Should -Be '127.0.0.1' + $violations.SuggestedCorrections.Text | Should -Be "'127.0.0.1'" + $violations.SuggestedCorrections.Description | Should -Be $correctionDescription + } + } + + Context "Compliant" { + It "From string" { + $scriptDefinition = { $Version = [Version]'1.2.3' }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "From version components" { + $scriptDefinition = { $Version = [Version]::new(1, 2, 3, 4) }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "From (bare) double" { + $scriptDefinition = { $Version = [Version]1.2 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + + It "Dot notation" { #PowerShell:27356 + $scriptDefinition = { + $1.2.3.4 + $intKeys = @{ 1 = @{ 2 = @{ 3 = @{ 4 = 'test' } } } } + $intKeys.1.2.3.4 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Disabled" { + + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $false } } + } + } + + It "ConvertFrom-SecureString -AsPlainText" { + $scriptDefinition = { $version = 1.2.3 }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Suppressed" { + It "All" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSInvalidMultiDotValue', '', Justification = 'Test')] + param() + $version = 1.2.3 + $IP = [System.Net.IPAddress]127.0.0.1 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "1.2.3" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSInvalidMultiDotValue', '1.2.3', Justification = 'Test')] + param() + $version = 1.2.3 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "127.0.0.1" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSInvalidMultiDotValue', '127.0.0.1', Justification = 'Test')] + param() + $IP = [System.Net.IPAddress]127.0.0.1 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Fixing" { + + BeforeAll { # See request: #1938 + $tempFile = Join-Path $TestDrive 'TestScript.ps1' + } + + It "Version" { + Set-Content -LiteralPath $tempFile -Value {$version = 1.2.3}.ToString() -NoNewLine + $violations = Invoke-ScriptAnalyzer -Path $tempFile -Settings $Settings -fix + Get-Content -LiteralPath $tempFile -Raw | Should -Be {$version = '1.2.3'}.ToString() + } + + It "IP Address" { + Set-Content -LiteralPath $tempFile -Value {$IP = [System.Net.IPAddress]127.0.0.1}.ToString() -NoNewLine + $violations = Invoke-ScriptAnalyzer -Path $tempFile -Settings $Settings -fix + Get-Content -LiteralPath $tempFile -Raw | Should -Be {$IP = [System.Net.IPAddress]'127.0.0.1'}.ToString() + } + } +} \ No newline at end of file diff --git a/docs/Rules/InvalidMultiDotValue.md b/docs/Rules/InvalidMultiDotValue.md new file mode 100644 index 000000000..90634693e --- /dev/null +++ b/docs/Rules/InvalidMultiDotValue.md @@ -0,0 +1,62 @@ +--- +description: Invalid unquoted multi-dot value construction +ms.date: 04/24/2024 +ms.topic: reference +title: InvalidMultiDotValue +--- +# InvalidMultiDotValue + +**Severity Level: Error** + +## Description + +PowerShell doesn't support unquoted literal values with multiple dots (`.`). Any value with two or +more dots results in `$null`. This rule identifies instances where such values are used, which can +lead to unexpected behavior or errors in the code. + +To create values of the intended type, enclose the value in quotes and use type-casting or use type +constructor methods to create the appropriate object. + + +## Example + +### Wrong + +```powershell +$version = 1.2.3 +``` + +or even: + +```powershell +$IP = [System.Net.IPAddress]127.0.0.1 +``` + +Where both examples will result in `$null` instead of any specific object. + +### Correct + +```powershell +# Use type-casting with quoted value +$IP = [System.Net.IPAddress]'127.0.0.1' +$version = [Version]'1.2.3' + +# Use type constructor method +$version = [Version]::new(1, 2, 3) +``` + +## Configuration + +```powershell +Rules = @{ + PSInvalidMultiDotValue = @{ + Enable = $true + } +} +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 19bd798ce..06d9204b0 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -49,6 +49,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [DSCUseIdenticalMandatoryParametersForDSC](./DSCUseIdenticalMandatoryParametersForDSC.md) | Error | Yes | | | [DSCUseIdenticalParametersForDSC](./DSCUseIdenticalParametersForDSC.md) | Error | Yes | | | [DSCUseVerboseMessageInDSCResource](./DSCUseVerboseMessageInDSCResource.md) | Error | Yes | | +| [InvalidMultiDotValue](./InvalidMultiDotValue.md) | Error | No | Yes | | [MisleadingBacktick](./MisleadingBacktick.md) | Warning | Yes | | | [MissingModuleManifestField](./MissingModuleManifestField.md) | Warning | Yes | | | [MissingTryBlock](./MissingTryBlock.md) | Warning | No | Yes | From 4b0117ca7d2887711c9699f467ba7171f8859156 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Mon, 18 May 2026 20:34:47 +0200 Subject: [PATCH 130/130] Add new AvoidDynamicallyCreatingVariableNames rule (#2178) * 1st commit * Avoid dynamic variable names rule implementation and tests * Removed `using System.Linq;` and added some tests * Covering Liam's feedback Co-authored-by: Copilot * Update docs/Rules/AvoidDynamicallyCreatingVariableNames.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/Rules/AvoidDynamicallyCreatingVariableNames.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Rules/Strings.resx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Corrected alphabetical order of rules in README.md * Corrected $ruleMessage in Test Co-authored-by: Copilot * Changed newVariableAst.Parent.Extent to newVariableAst.Extent * Made rule configurable (disabled) and updated documentation and tests accordingly. Increased number of information tests to 20 in Get-ScriptAnalyzerRule tests. * Change rule name from PSAvoidExclaimOperator to PSAvoidDynamicallyCreatingVariableNames in documentation. --------- Co-authored-by: Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AvoidDynamicallyCreatingVariableNames.cs | 141 +++++++++++++++ Rules/Strings.resx | 12 ++ Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 2 +- ...DynamicallyCreatingVariableNames.tests.ps1 | 164 ++++++++++++++++++ .../AvoidDynamicallyCreatingVariableNames.md | 77 ++++++++ docs/Rules/README.md | 1 + 6 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 Rules/AvoidDynamicallyCreatingVariableNames.cs create mode 100644 Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 create mode 100644 docs/Rules/AvoidDynamicallyCreatingVariableNames.md diff --git a/Rules/AvoidDynamicallyCreatingVariableNames.cs b/Rules/AvoidDynamicallyCreatingVariableNames.cs new file mode 100644 index 000000000..5e5df7566 --- /dev/null +++ b/Rules/AvoidDynamicallyCreatingVariableNames.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management.Automation.Language; + +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// Rule that informs the user when they create variables with dynamic names in the general variable scope. + /// This might lead to conflicts with other variables. + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class AvoidDynamicallyCreatingVariableNames : ConfigurableRule + { + + /// + /// Construct an object of AvoidDynamicallyCreatingVariableNames type. + /// + public AvoidDynamicallyCreatingVariableNames() { + Enable = false; + } + + readonly HashSet cmdList = new HashSet(Helper.Instance.CmdletNameAndAliases("New-Variable"), StringComparer.OrdinalIgnoreCase); + + /// + /// Analyzes the given ast to find the [violation] + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + // Find all "New-Variable" commands in the Ast + IEnumerable newVariableAsts = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdList.Contains(cmdAst.GetCommandName()), + true + ).Cast(); + + foreach (CommandAst newVariableAst in newVariableAsts) + { + // Use StaticParameterBinder to reliably get parameter values + var bindingResult = StaticParameterBinder.BindCommand(newVariableAst, true); + if (!bindingResult.BoundParameters.ContainsKey("Name")) { continue; } + var nameBindingResult = bindingResult.BoundParameters["Name"]; + // Dynamic parameters return null for the ConstantValue property + if (nameBindingResult.ConstantValue != null) { continue; } + string variableName = nameBindingResult.Value.ToString(); + if (variableName.StartsWith("\"") && variableName.EndsWith("\"")) + { + variableName = variableName.Substring(1, variableName.Length - 2); + } + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidDynamicallyCreatingVariableNamesError, + variableName), + newVariableAst.Extent, + GetName(), + DiagnosticSeverity.Information, + fileName, + variableName + ); + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidDynamicallyCreatingVariableNamesCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidDynamicallyCreatingVariableNamesDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidDynamicallyCreatingVariableNamesName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Information; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} diff --git a/Rules/Strings.resx b/Rules/Strings.resx index cf399d783..2f2b1e1a9 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -885,6 +885,18 @@ The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + AvoidDynamicallyCreatingVariableNames + + + Avoid dynamically creating variable names + + + Do not create variables with a dynamic name, as this might introduce conflicts with other variables and is difficult to maintain. + + + '{0}' is a dynamic variable name. Please avoid creating variables with a dynamic name + Avoid global functions and aliases diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index d1e2cd98d..2886a0c88 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -152,7 +152,7 @@ Describe "TestSeverity" { It "filters rules based on multiple severity inputs"{ $rules = Get-ScriptAnalyzerRule -Severity Error,Information - $rules.Count | Should -Be 19 + $rules.Count | Should -Be 20 } It "takes lower case inputs" { diff --git a/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 new file mode 100644 index 000000000..1a47abbac --- /dev/null +++ b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +[Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingCmdletAliases', 'nv', Justification = 'For test purposes')] +param() + +BeforeAll { + $ruleName = "PSAvoidDynamicallyCreatingVariableNames" + $ruleMessage = "'{0}' is a dynamic variable name. Please avoid creating variables with a dynamic name" +} + +Describe "AvoidDynamicallyCreatingVariableNames" { + + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $true } } + } + } + + Context "Violates" { + It "Basic dynamic variable name" { + $scriptDefinition = { New-Variable -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {New-Variable -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + + It "Using alias" { + $scriptDefinition = { nv -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {nv -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + + It "Using uppercase" { + $scriptDefinition = { NEW-VARIABLE -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {NEW-VARIABLE -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + + It "Common dynamic variable iteration" { + $scriptDefinition = { + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {New-Variable -Name "My$_" -Value ($i++)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f 'My$_') + } + + It "Unquoted positional binding" { + $scriptDefinition = { + $myVarName = 'foo' + New-Variable $myVarName + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {New-Variable $myVarName}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$myVarName') + } + + It "Quoted positional binding" { + $scriptDefinition = { + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable "My$_" ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {New-Variable "My$_" ($i++)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f 'My$_') + } + } + + Context "Compliant" { + It "Common hash table population" { + $scriptDefinition = { + $My = @{} + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $My[$_] = $i++ + } + $My.Two # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Scoped hash table population" { + $scriptDefinition = { + New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $Script:My[$_] = $i++ + } + $Script:My.Two # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + + It "Verbatim (single quoted) name with dollar sign" { + $scriptDefinition = { + New-Variable -Name '$Sign1' + New-Variable -Name '$Sign2' -Value 'Dollar' + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Disabled" { + BeforeAll { + $Settings = @{ + IncludeRules = @($ruleName) + Rules = @{ $ruleName = @{ Enable = $false } } + } + } + + It "ConvertFrom-SecureString -AsPlainText" { + $scriptDefinition = { New-Variable -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } + + Context "Suppressed" { + It "Basic dynamic variable name" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicallyCreatingVariableNames', '$Test', Justification = 'Test')] + Param() + New-Variable -Name $Test + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + It "Common dynamic variable iteration" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicallyCreatingVariableNames', 'My$_', Justification = 'Test')] + Param() + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $Settings + $violations | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/docs/Rules/AvoidDynamicallyCreatingVariableNames.md b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md new file mode 100644 index 000000000..4d5035f3e --- /dev/null +++ b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md @@ -0,0 +1,77 @@ +--- +description: Avoid dynamic variable names, instead use a hash table or similar dictionary type. +ms.date: 04/21/2026 +ms.topic: reference +title: AvoidDynamicallyCreatingVariableNames +--- +# AvoidDynamicallyCreatingVariableNames + +**Severity Level: Information** + +## Description + +Don't create variables with dynamic names. It also makes the code difficult to understand and can +lead to unexpected behavior if the variable names are not unique or if they collide with existing +variables. A dynamic name is a name constructed using string concatenation or interpolation. +This rule checks for the use of `New-Variable` with a dynamic name. + +> [!NOTE] +> This rule is not enabled by default. The user needs to enable it through settings. + +## How to Fix + +Use a hash table or similar dictionary type to store values with dynamic keys. When you require a +specific scope, option, or visibility, put the dictionary (hashtable) in that scope and apply the +appropriate option or visibility. + +## Example + +### Wrong + +```powershell +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) +} +$MyTwo # returns 2 +``` + +### Correct + +```powershell +$My = @{} +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $My[$_] = $i++ +} +$My.Two # returns 2 +``` + +In this example, you want the values to be read-only and available in the script scope. +Put the hashtable in the script scope and make it read-only. + +```powershell +New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $Script:My[$_] = $i++ +} +$Script:My.Two # returns 2 +``` + +## Configuration + +```powershell +Rules = @{ + PSAvoidDynamicallyCreatingVariableNames = @{ + Enable = $true + } +} +``` + +### Parameters + +- `Enable`: **bool** (Default value is `$false`) + + Enable or disable the rule during ScriptAnalyzer invocation. + +## References +- [New-Variable](xref:Microsoft.PowerShell.Utility.New-Variable) + diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 06d9204b0..fac3c7d40 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -14,6 +14,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidAssignmentToAutomaticVariable](./AvoidAssignmentToAutomaticVariable.md) | Warning | Yes | | | [AvoidDefaultValueForMandatoryParameter](./AvoidDefaultValueForMandatoryParameter.md) | Warning | Yes | | | [AvoidDefaultValueSwitchParameter](./AvoidDefaultValueSwitchParameter.md) | Warning | Yes | | +| [AvoidDynamicallyCreatingVariableNames](./AvoidDynamicallyCreatingVariableNames.md) | Information | No | Yes | | [AvoidExclaimOperator](./AvoidExclaimOperator.md) | Warning | No | | | [AvoidGlobalAliases1](./AvoidGlobalAliases.md) | Warning | Yes | | | [AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | Yes | |