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
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/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml
index 971cdc351..abea9ab3c 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
@@ -141,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 6afc5be8e..76352e7c7 100644
--- a/CHANGELOG.MD
+++ b/CHANGELOG.MD
@@ -1,16 +1,105 @@
# 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
+* 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
+
+**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)
@@ -24,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)
@@ -67,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/Directory.Packages.props b/Directory.Packages.props
index e43bd7dfd..50150e6ac 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,15 +3,13 @@
-
-
-
-
+
+
diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
index 3be9cd7fc..a444327e0 100644
--- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs
+++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
@@ -34,6 +34,9 @@ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
#region Private variables
List processedPaths;
+ // 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
@@ -412,6 +415,37 @@ protected override void EndProcessing()
{
ScriptAnalyzer.Instance.CleanUp();
base.EndProcessing();
+
+ var diagnosticCount = diagnosticCounts.Values.Sum();
+
+ if (ReportSummary.IsPresent)
+ {
+ if (diagnosticCount == 0)
+ {
+ Host.UI.WriteLine("0 rule violations found.");
+ }
+ else
+ {
+ 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(diagnosticCount);
+ }
}
protected override void StopProcessing()
@@ -426,92 +460,50 @@ 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()
{
if (!IsFileParameterSet())
{
- return ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _);
- }
-
- var diagnostics = new List();
- foreach (string path in this.processedPaths)
- {
- if (fix)
- {
- ShouldProcess(path, $"Analyzing and fixing path with Recurse={this.recurse}");
- diagnostics.AddRange(ScriptAnalyzer.Instance.AnalyzeAndFixPath(path, this.ShouldProcess, this.recurse));
- }
- else
+ foreach (var record in ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _))
{
- ShouldProcess(path, $"Analyzing path with Recurse={this.recurse}");
- diagnostics.AddRange(ScriptAnalyzer.Instance.AnalyzePath(path, this.ShouldProcess, this.recurse));
+ yield return record;
}
+ yield break;
}
- return diagnostics;
- }
-
- private void WriteToOutput(IEnumerable diagnosticRecords)
- {
- foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers)
+ foreach (var path in this.processedPaths)
{
- var errorCount = 0;
- var warningCount = 0;
- var infoCount = 0;
- var parseErrorCount = 0;
-
- foreach (DiagnosticRecord diagnostic in diagnosticRecords)
+ if (!ShouldProcess(path, $"Analyzing path with Fix={this.fix} and Recurse={this.recurse}"))
{
- 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");
- }
+ continue;
}
- if (ReportSummary.IsPresent)
+ if (fix)
{
- var numberOfRuleViolations = infoCount + warningCount + errorCount;
- if (numberOfRuleViolations == 0)
+ foreach (var record in ScriptAnalyzer.Instance.AnalyzeAndFixPath(path, this.ShouldProcess, this.recurse))
{
- Host.UI.WriteLine("0 rule violations found.");
+ yield return record;
}
- else
+ }
+ else
+ {
+ foreach (var record in ScriptAnalyzer.Instance.AnalyzePath(path, this.ShouldProcess, this.recurse))
{
- 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);
- }
+ yield return record;
}
}
}
-
- if (EnableExit.IsPresent)
- {
- this.Host.SetShouldExit(diagnosticRecords.Count());
- }
}
private void ProcessPath()
@@ -535,4 +527,4 @@ private bool OverrideSwitchParam(bool paramValue, string paramName)
#endregion // Private Methods
}
-}
+}
\ No newline at end of file
diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj
index 3025c9a08..63b9a1b9c 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,39 +69,15 @@
-
+
-
+
$(DefineConstants);PSV7;CORECLR
-
-
-
-
-
-
-
-
-
+
-
-
- $(DefineConstants);PSV3
-
-
-
- $(DefineConstants);PSV3;PSV4
-
-
-
- $(DefineConstants);PSV3
-
-
-
- $(DefineConstants);PSV3;PSV4
-
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/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/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/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1
index 5e933ca4a..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 = ''
@@ -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 = ''
}
}
diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs
index 1a885eabe..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));
}
@@ -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/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/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/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 716224c7c..d038ec756 100644
--- a/README.md
+++ b/README.md
@@ -72,12 +72,12 @@ To install **PSScriptAnalyzer** from source code:
### Requirements
-- [Latest .NET 6.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
-* If building for Windows PowerShell versions, then the .NET Framework 4.6.2 [targeting pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net462) (also referred to as developer/targeting pack) need to be installed. This is only possible on Windows.
-* Optionally but recommended for development: [Visual Studio 2017/2019](https://www.visualstudio.com/downloads)
+- [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.
+- 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/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/Rules/AvoidAssignmentToAutomaticVariable.cs b/Rules/AvoidAssignmentToAutomaticVariable.cs
index c188da341..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)
{
@@ -89,7 +114,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/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/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/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/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/Rules/Strings.resx b/Rules/Strings.resx
index ff75828cf..260214967 100644
--- a/Rules/Strings.resx
+++ b/Rules/Strings.resx
@@ -1,17 +1,17 @@
-
@@ -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
@@ -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.
@@ -1129,7 +1138,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
@@ -1188,9 +1197,6 @@
AvoidUsingBrokenHashAlgorithms
-
- Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'.
-
AvoidExclaimOperator
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/Rules/UseConsistentWhitespace.cs b/Rules/UseConsistentWhitespace.cs
index a062e5d3f..e6d4cff99 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,
@@ -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/Rules/UseCorrectCasing.cs b/Rules/UseCorrectCasing.cs
index 9d3abd098..5d1393d45 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.Information,
+ fileName,
+ correction,
+ suggestedCorrections: GetCorrectionExtent(ast, extent, correction));
+ }
+
+ private DiagnosticRecord GetDiagnosticRecord(CommandParameterAst ast, string fileName, string correction, string message)
+ {
+ var extent = ast.Extent;
+ return new DiagnosticRecord(
+ string.Format(CultureInfo.CurrentCulture, message, extent.Text, correction),
+ extent,
+ GetName(),
+ DiagnosticSeverity.Information,
+ fileName,
+ correction,
+ suggestedCorrections: GetCorrectionExtent(ast, extent, correction));
}
///
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/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
}
}
diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
index 06b94cb78..b930c9980 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
@@ -561,69 +561,89 @@ 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
}
- Describe "-ReportSummary switch" {
- BeforeAll {
- $pssaPath = (Get-Module PSScriptAnalyzer).Path
+ It "Returns exit code equivalent to number of warnings for multiple piped files" {
+ if ($IsCoreCLR)
+ {
+ $pwshExe = (Get-Process -Id $PID).Path
+ }
+ else
+ {
+ $pwshExe = 'powershell'
+ }
- if ($IsCoreCLR)
- {
- $pwshExe = (Get-Process -Id $PID).Path
- }
- else
- {
- $pwshExe = 'powershell'
- }
+ $pssaPath = (Get-Module PSScriptAnalyzer).Path
- $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*'
- }
+ & $pwshExe -NoProfile {
+ Import-Module $Args[0]
+ Get-ChildItem $Args[1] | Invoke-ScriptAnalyzer -EnableExit
+ } -Args $pssaPath, "$PSScriptRoot\RecursionDirectoryTest"
- It "prints the correct report summary using the -NoReportSummary switch" {
- $result = & $pwshExe -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary"
+ $LASTEXITCODE | Should -Be 2
+ }
+}
- "$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"
+Describe "-ReportSummary switch" {
+ BeforeAll {
+ $pssaPath = (Get-Module PSScriptAnalyzer).Path
- "$result" | Should -Not -BeLike $reportSummaryFor1Warning
+ if ($IsCoreCLR)
+ {
+ $pwshExe = (Get-Process -Id $PID).Path
+ }
+ else
+ {
+ $pwshExe = 'powershell'
}
+
+ $reportSummaryFor1Warning = '*1 rule violation found. Severity distribution: Error = 0, Warning = 1, Information = 0*'
}
- # 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 "prints the correct report summary using the -NoReportSummary switch" {
+ $result = & $pwshExe -NoProfile -Command "Import-Module '$pssaPath'; Invoke-ScriptAnalyzer -ScriptDefinition gci -ReportSummary"
+
+ "$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"
+
+ "$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
}
}
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"
+ }
+ }
}
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" {
diff --git a/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1 b/Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1
index 8130990c2..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){}"
@@ -94,6 +103,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
}
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
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
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" {
diff --git a/Tests/Rules/UseConsistentWhitespace.tests.ps1 b/Tests/Rules/UseConsistentWhitespace.tests.ps1
index 30d8cce57..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 {
@@ -535,7 +577,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 +627,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'
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/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 | `
diff --git a/build.ps1 b/build.ps1
index bbc6c505a..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")]
@@ -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 604c5d17e..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
}
@@ -144,13 +144,7 @@ function Start-ScriptAnalyzerBuild
$framework = 'net462'
if ($PSVersion -eq 7) {
- $framework = 'net6'
- }
-
- # 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)
+ $framework = 'net8'
}
Push-Location -Path $projectRoot
@@ -165,6 +159,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"
@@ -172,14 +170,6 @@ function Start-ScriptAnalyzerBuild
switch ($PSVersion)
{
- 3
- {
- $destinationDirBinaries = "$script:destinationDir\PSv3"
- }
- 4
- {
- $destinationDirBinaries = "$script:destinationDir\PSv4"
- }
5
{
$destinationDirBinaries = "$script:destinationDir"
@@ -195,7 +185,7 @@ function Start-ScriptAnalyzerBuild
}
$buildConfiguration = $Configuration
- if ((3, 4, 7) -contains $PSVersion) {
+ if ($PSVersion -eq 7) {
$buildConfiguration = "PSV${PSVersion}${Configuration}"
}
@@ -304,7 +294,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
@@ -343,11 +336,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} -Command $scriptBlock
+ $pwshVersion = & $powershell --version
+ Write-Verbose "Testing with $pwshVersion"
+ & $powershell -NoProfile -Command $scriptBlock
}
}
finally {
@@ -551,6 +552,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"
@@ -559,6 +567,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"
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..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
@@ -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 f64a7888d..6afcb3a4b 100644
--- a/docs/Rules/UseCorrectCasing.md
+++ b/docs/Rules/UseCorrectCasing.md
@@ -10,11 +10,40 @@ title: UseCorrectCasing
## Description
-This is a style/formatting rule. PowerShell is case insensitive where applicable. The casing of
+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.
+## 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.
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)
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"
}
}
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
+ }
+}