From 58af78698737f5d54c621a8007906eb2a64ba2f2 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 10 Oct 2024 07:40:48 +0100 Subject: [PATCH 01/36] 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 02/36] 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 03/36] 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 04/36] 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 05/36] 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 06/36] 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 07/36] 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 08/36] 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 09/36] 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 10/36] 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 11/36] 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 12/36] 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 13/36] 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 14/36] 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 15/36] 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 16/36] 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 17/36] 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 18/36] 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 19/36] 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 20/36] 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 21/36] 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 22/36] 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 23/36] 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 24/36] 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 25/36] 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 26/36] 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 27/36] 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 28/36] 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 29/36] 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 30/36] 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 31/36] 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 32/36] 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 33/36] 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 34/36] 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 35/36] 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 36/36] 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