From 0e873c28fe829b3ea4375a1cda17078ec3db0d02 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Tue, 21 Apr 2026 17:21:35 +0200 Subject: [PATCH 01/10] 1st commit --- Rules/AvoidDynamicVariableNames.cs | 87 +++++++++++++++++++++++++ Rules/Strings.resx | 12 ++++ docs/Rules/AvoidDynamicVariableNames.md | 51 +++++++++++++++ docs/Rules/README.md | 1 + 4 files changed, 151 insertions(+) create mode 100644 Rules/AvoidDynamicVariableNames.cs create mode 100644 docs/Rules/AvoidDynamicVariableNames.md diff --git a/Rules/AvoidDynamicVariableNames.cs b/Rules/AvoidDynamicVariableNames.cs new file mode 100644 index 000000000..a5a661a07 --- /dev/null +++ b/Rules/AvoidDynamicVariableNames.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management.Automation.Language; +using System.Linq; + +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that warns when reserved words are used as function names + /// + public class AvoidDynamicVariableNames : IScriptRule + { + /// + /// Analyzes the PowerShell AST for uses of reserved words as function names. + /// + /// The PowerShell Abstract Syntax Tree to analyze. + /// The name of the file being analyzed (for diagnostic reporting). + /// A collection of diagnostic records for each violation. + public IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); + + // Find all FunctionDefinitionAst in the Ast + var newVariableAsts = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + ( + String.Equals(cmdAst.GetCommandName(), "New-Variable", StringComparison.OrdinalIgnoreCase) || + String.Equals(cmdAst.GetCommandName(), "Set-Variable", StringComparison.OrdinalIgnoreCase) + ), + true + ); + + foreach (CommandAst newVariableAst in newVariableAsts) + { + // Use StaticParameterBinder to reliably get parameter values + var bindingResult = StaticParameterBinder.BindCommand(newVariableAst, true); + + // Dynamic parameters return null for the ConstantValue property + if ( + bindingResult.BoundParameters.ContainsKey("Name") && + bindingResult.BoundParameters["Name"] == null + ) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidDynamicVariableNamesError, + newVariableAst.Parent.Extent.Text), + newVariableAst.Parent.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + } + } + + public string GetCommonName() => Strings.AvoidDynamicVariableNamesCommonName; + + public string GetDescription() => Strings.AvoidDynamicVariableNamesDescription; + + public string GetName() => string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidDynamicVariableNamesName); + + public RuleSeverity GetSeverity() => RuleSeverity.Warning; + + public string GetSourceName() => Strings.SourceName; + + public SourceType GetSourceType() => SourceType.Builtin; + } +} \ No newline at end of file diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 2a04fd759..429fb7c63 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -873,6 +873,18 @@ The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + AvoidDynamicVariableNames + + + Avoid dynamic variable names + + + Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other variables and is difficult to maintain. + + + Avoid dynamically creating variable names + Avoid global functions and aliases diff --git a/docs/Rules/AvoidDynamicVariableNames.md b/docs/Rules/AvoidDynamicVariableNames.md new file mode 100644 index 000000000..6dce6f83d --- /dev/null +++ b/docs/Rules/AvoidDynamicVariableNames.md @@ -0,0 +1,51 @@ +--- +description: Avoid dynamic variable names, instead use a hash table or similar dictionary type. +ms.date: 04/21/2026 +ms.topic: reference +title: AvoidDynamicVariableNames +--- +# AvoidDynamicVariableNames + +**Severity Level: Warning** + +## Description + +Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other +variables and is difficult to maintain. + +## How + +Use a hash table or similar dictionary type to store values with dynamic keys. + +## Example + +### Wrong + +```powershell +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) +} +$MyTwo # returns 2 +``` + +### Correct + +```powershell +$My = @{} +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $My[$_] = $i++ +} +$My.Two # returns 2 +``` + +When it concerns a specific scope, option or visibility, put the concerned dictionary (hash table) in that +scope, option or visibility. In example, if the values should be read only and available in the script scope, +put the _hash table_ in the script scope and make it read only.: + +```powershell +New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script +'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $Script:My[$_] = $i++ +} +$Script:My.Two # returns 2 +``` \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index fca031e33..ee82f511f 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -34,6 +34,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | Yes | | | [AvoidUsingDeprecatedManifestFields](./AvoidUsingDeprecatedManifestFields.md) | Warning | Yes | | | [AvoidUsingDoubleQuotesForConstantString](./AvoidUsingDoubleQuotesForConstantString.md) | Information | No | | +| [AvoidUsingDynamicVariableNames](./AvoidUsingDynamicVariableNames.md) | Warning | Yes | | | [AvoidUsingEmptyCatchBlock](./AvoidUsingEmptyCatchBlock.md) | Warning | Yes | | | [AvoidUsingInvokeExpression](./AvoidUsingInvokeExpression.md) | Warning | Yes | | | [AvoidUsingPlainTextForPassword](./AvoidUsingPlainTextForPassword.md) | Warning | Yes | | From b4aa1a7d2ccc6f06aedb4e059b5538120d5167dd Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 22 Apr 2026 12:21:36 +0200 Subject: [PATCH 02/10] Avoid dynamic variable names rule implementation and tests --- Rules/AvoidDynamicVariableNames.cs | 32 +++---- Rules/Strings.resx | 4 +- .../Rules/AvoidDynamicVariableNames.tests.ps1 | 86 +++++++++++++++++++ docs/Rules/AvoidDynamicVariableNames.md | 4 +- 4 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 Tests/Rules/AvoidDynamicVariableNames.tests.ps1 diff --git a/Rules/AvoidDynamicVariableNames.cs b/Rules/AvoidDynamicVariableNames.cs index a5a661a07..716069eb8 100644 --- a/Rules/AvoidDynamicVariableNames.cs +++ b/Rules/AvoidDynamicVariableNames.cs @@ -47,24 +47,26 @@ testAst is CommandAst cmdAst && { // Use StaticParameterBinder to reliably get parameter values var bindingResult = StaticParameterBinder.BindCommand(newVariableAst, true); - + if (!bindingResult.BoundParameters.ContainsKey("Name")) { continue; } + var nameBindingResult = bindingResult.BoundParameters["Name"]; // Dynamic parameters return null for the ConstantValue property - if ( - bindingResult.BoundParameters.ContainsKey("Name") && - bindingResult.BoundParameters["Name"] == null - ) + if (nameBindingResult.ConstantValue != null) { continue; } + string variableName = nameBindingResult.Value.ToString(); + if (variableName.StartsWith("\"") && variableName.EndsWith("\"")) { - yield return new DiagnosticRecord( - string.Format( - CultureInfo.CurrentCulture, - Strings.AvoidDynamicVariableNamesError, - newVariableAst.Parent.Extent.Text), - newVariableAst.Parent.Extent, - GetName(), - DiagnosticSeverity.Warning, - fileName - ); + variableName = variableName.Substring(1, variableName.Length - 2); } + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidDynamicVariableNamesError, + variableName), + newVariableAst.Parent.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName, + variableName + ); } } diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 429fb7c63..e493cfd91 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -880,10 +880,10 @@ Avoid dynamic variable names - Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other variables and is difficult to maintain. + Do not create variables with a dynamic name, this might introduce conflicts with other variables and is difficult to maintain. - Avoid dynamically creating variable names + '{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name Avoid global functions and aliases diff --git a/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 b/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 new file mode 100644 index 000000000..19e0f33c6 --- /dev/null +++ b/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + +BeforeAll { + $ruleName = "PSAvoidDynamicVariableNames" + $ruleMessage = "'{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name" +} + +Describe "AvoidDynamicVariableNames" { + Context "Violates" { + It "Basic dynamic variable name" { + $scriptDefinition = { New-Variable -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Variable -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + It "Common dynamic variable iteration" { + $scriptDefinition = { + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Variable -Name "My$_" -Value ($i++)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f 'My$_') + } + } + + Context "Compliant" { + It "Common hash table population" { + $scriptDefinition = { + $My = @{} + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $My[$_] = $i++ + } + $My.Two # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + + It "Scoped hash table population" { + $scriptDefinition = { + New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + $Script:My[$_] = $i++ + } + $Script:My.Two # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + } + + Context "Suppressed" { + It "Basic dynamic variable name" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicVariableNames', '$Test', Justification = 'Test')] + Param() + New-Variable -Name $Test + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + It "Common dynamic variable iteration" { + $scriptDefinition = { + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicVariableNames', 'My$_', Justification = 'Test')] + Param() + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable -Name "My$_" -Value ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/docs/Rules/AvoidDynamicVariableNames.md b/docs/Rules/AvoidDynamicVariableNames.md index 6dce6f83d..05086fd6a 100644 --- a/docs/Rules/AvoidDynamicVariableNames.md +++ b/docs/Rules/AvoidDynamicVariableNames.md @@ -10,8 +10,8 @@ title: AvoidDynamicVariableNames ## Description -Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other -variables and is difficult to maintain. +Do not create variables with a dynamic name, this might introduce conflicts with +other variables and is difficult to maintain. ## How From 065c07e4aa9fd821dba65bba301cb68d939b6f43 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 22 Apr 2026 19:07:44 +0200 Subject: [PATCH 03/10] Removed `using System.Linq;` and added some tests --- Rules/AvoidDynamicVariableNames.cs | 1 - .../Rules/AvoidDynamicVariableNames.tests.ps1 | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidDynamicVariableNames.cs b/Rules/AvoidDynamicVariableNames.cs index 716069eb8..44e9fcc3e 100644 --- a/Rules/AvoidDynamicVariableNames.cs +++ b/Rules/AvoidDynamicVariableNames.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Globalization; using System.Management.Automation.Language; -using System.Linq; #if !CORECLR using System.ComponentModel.Composition; diff --git a/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 b/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 index 19e0f33c6..9babbb90e 100644 --- a/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 +++ b/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 @@ -19,6 +19,7 @@ Describe "AvoidDynamicVariableNames" { $violations.Extent.Text | Should -Be {New-Variable -Name $Test}.ToString() $violations.Message | Should -Be ($ruleMessage -f '$Test') } + It "Common dynamic variable iteration" { $scriptDefinition = { 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { @@ -32,6 +33,20 @@ Describe "AvoidDynamicVariableNames" { $violations.Extent.Text | Should -Be {New-Variable -Name "My$_" -Value ($i++)}.ToString() $violations.Message | Should -Be ($ruleMessage -f 'My$_') } + + It "Set-Variable by positional parameter" { + $scriptDefinition = { + 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { + New-Variable "My$_" ($i++) + } + $MyTwo # returns 2 + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Variable "My$_" ($i++)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f 'My$_') + } } Context "Compliant" { @@ -58,6 +73,15 @@ Describe "AvoidDynamicVariableNames" { $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations | Should -BeNullOrEmpty } + + It "Verbatim (single quoted) name with dollar sign" { + $scriptDefinition = { + New-Variable -Name '$Sign' + Set-Variable -Name '$Sign' -Value 'Dollar' + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } } Context "Suppressed" { From ce37bb8b27109b13451d5fa21f7eccd3ebf574c7 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 11:04:55 +0200 Subject: [PATCH 04/10] Covering Liam's feedback Co-authored-by: Copilot --- ... AvoidDynamicallyCreatingVariableNames.cs} | 33 ++++++------ Rules/Strings.resx | 12 ++--- ...ynamicallyCreatingVariableNames.tests.ps1} | 51 +++++++++++++++---- ... AvoidDynamicallyCreatingVariableNames.md} | 2 +- docs/Rules/README.md | 2 +- 5 files changed, 66 insertions(+), 34 deletions(-) rename Rules/{AvoidDynamicVariableNames.cs => AvoidDynamicallyCreatingVariableNames.cs} (68%) rename Tests/Rules/{AvoidDynamicVariableNames.tests.ps1 => AvoidDynamicallyCreatingVariableNames.tests.ps1} (66%) rename docs/Rules/{AvoidDynamicVariableNames.md => AvoidDynamicallyCreatingVariableNames.md} (97%) diff --git a/Rules/AvoidDynamicVariableNames.cs b/Rules/AvoidDynamicallyCreatingVariableNames.cs similarity index 68% rename from Rules/AvoidDynamicVariableNames.cs rename to Rules/AvoidDynamicallyCreatingVariableNames.cs index 44e9fcc3e..713fa8fc5 100644 --- a/Rules/AvoidDynamicVariableNames.cs +++ b/Rules/AvoidDynamicallyCreatingVariableNames.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Management.Automation.Language; #if !CORECLR @@ -18,29 +19,29 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif /// - /// Rule that warns when reserved words are used as function names + /// Rule that informs the user when they create variables with dynamic names in the general variable scope. + /// This might lead to conflicts with other variables. /// - public class AvoidDynamicVariableNames : IScriptRule + public class AvoidDynamicallyCreatingVariableNames : IScriptRule { /// - /// Analyzes the PowerShell AST for uses of reserved words as function names. + /// Analyzes the PowerShell AST for uses of "New-Variable" command with a dynamic name argument. /// /// The PowerShell Abstract Syntax Tree to analyze. /// The name of the file being analyzed (for diagnostic reporting). /// A collection of diagnostic records for each violation. + + readonly HashSet cmdList = Helper.Instance.CmdletNameAndAliases("New-Variable").ToHashSet(StringComparer.OrdinalIgnoreCase); public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - // Find all FunctionDefinitionAst in the Ast - var newVariableAsts = ast.FindAll(testAst => + // Find all "New-Variable" commands in the Ast + IEnumerable newVariableAsts = ast.FindAll(testAst => testAst is CommandAst cmdAst && - ( - String.Equals(cmdAst.GetCommandName(), "New-Variable", StringComparison.OrdinalIgnoreCase) || - String.Equals(cmdAst.GetCommandName(), "Set-Variable", StringComparison.OrdinalIgnoreCase) - ), + cmdList.Contains(cmdAst.GetCommandName()), true - ); + ).Cast(); foreach (CommandAst newVariableAst in newVariableAsts) { @@ -58,28 +59,28 @@ testAst is CommandAst cmdAst && yield return new DiagnosticRecord( string.Format( CultureInfo.CurrentCulture, - Strings.AvoidDynamicVariableNamesError, + Strings.AvoidDynamicallyCreatingVariableNamesError, variableName), newVariableAst.Parent.Extent, GetName(), - DiagnosticSeverity.Warning, + DiagnosticSeverity.Information, fileName, variableName ); } } - public string GetCommonName() => Strings.AvoidDynamicVariableNamesCommonName; + public string GetCommonName() => Strings.AvoidDynamicallyCreatingVariableNamesCommonName; - public string GetDescription() => Strings.AvoidDynamicVariableNamesDescription; + public string GetDescription() => Strings.AvoidDynamicallyCreatingVariableNamesDescription; public string GetName() => string.Format( CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), - Strings.AvoidDynamicVariableNamesName); + Strings.AvoidDynamicallyCreatingVariableNamesName); - public RuleSeverity GetSeverity() => RuleSeverity.Warning; + public RuleSeverity GetSeverity() => RuleSeverity.Information; public string GetSourceName() => Strings.SourceName; diff --git a/Rules/Strings.resx b/Rules/Strings.resx index e493cfd91..1a414db00 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -873,16 +873,16 @@ The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - AvoidDynamicVariableNames + + AvoidDynamicallyCreatingVariableNames - - Avoid dynamic variable names + + Avoid dynamically creating variable names - + Do not create variables with a dynamic name, this might introduce conflicts with other variables and is difficult to maintain. - + '{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name diff --git a/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 similarity index 66% rename from Tests/Rules/AvoidDynamicVariableNames.tests.ps1 rename to Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 index 9babbb90e..7dc174b4f 100644 --- a/Tests/Rules/AvoidDynamicVariableNames.tests.ps1 +++ b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 @@ -2,24 +2,43 @@ # Licensed under the MIT License. [Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +[Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingCmdletAliases', 'nv', Justification = 'For test purposes')] param() BeforeAll { - $ruleName = "PSAvoidDynamicVariableNames" + $ruleName = "PSAvoidDynamicallyCreatingVariableNames" $ruleMessage = "'{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name" } -Describe "AvoidDynamicVariableNames" { +Describe "AvoidDynamicallyCreatingVariableNames" { Context "Violates" { It "Basic dynamic variable name" { $scriptDefinition = { New-Variable -Name $Test }.ToString() $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations.Count | Should -Be 1 - $violations.Severity | Should -Be Warning + $violations.Severity | Should -Be Information $violations.Extent.Text | Should -Be {New-Variable -Name $Test}.ToString() $violations.Message | Should -Be ($ruleMessage -f '$Test') } + It "Using alias" { + $scriptDefinition = { nv -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {nv -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + + It "Using uppercase" { + $scriptDefinition = { NEW-VARIABLE -Name $Test }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {NEW-VARIABLE -Name $Test}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$Test') + } + It "Common dynamic variable iteration" { $scriptDefinition = { 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { @@ -29,12 +48,24 @@ Describe "AvoidDynamicVariableNames" { }.ToString() $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations.Count | Should -Be 1 - $violations.Severity | Should -Be Warning + $violations.Severity | Should -Be Information $violations.Extent.Text | Should -Be {New-Variable -Name "My$_" -Value ($i++)}.ToString() $violations.Message | Should -Be ($ruleMessage -f 'My$_') } - It "Set-Variable by positional parameter" { + It "Unquoted positional binding" { + $scriptDefinition = { + $myVarName = 'foo' + New-Variable $myVarName + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Information + $violations.Extent.Text | Should -Be {New-Variable $myVarName}.ToString() + $violations.Message | Should -Be ($ruleMessage -f '$myVarName') + } + + It "Quoted positional binding" { $scriptDefinition = { 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { New-Variable "My$_" ($i++) @@ -43,7 +74,7 @@ Describe "AvoidDynamicVariableNames" { }.ToString() $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations.Count | Should -Be 1 - $violations.Severity | Should -Be Warning + $violations.Severity | Should -Be Information $violations.Extent.Text | Should -Be {New-Variable "My$_" ($i++)}.ToString() $violations.Message | Should -Be ($ruleMessage -f 'My$_') } @@ -76,8 +107,8 @@ Describe "AvoidDynamicVariableNames" { It "Verbatim (single quoted) name with dollar sign" { $scriptDefinition = { - New-Variable -Name '$Sign' - Set-Variable -Name '$Sign' -Value 'Dollar' + New-Variable -Name '$Sign1' + New-Variable -Name '$Sign2' -Value 'Dollar' }.ToString() $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations | Should -BeNullOrEmpty @@ -87,7 +118,7 @@ Describe "AvoidDynamicVariableNames" { Context "Suppressed" { It "Basic dynamic variable name" { $scriptDefinition = { - [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicVariableNames', '$Test', Justification = 'Test')] + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicallyCreatingVariableNames', '$Test', Justification = 'Test')] Param() New-Variable -Name $Test }.ToString() @@ -96,7 +127,7 @@ Describe "AvoidDynamicVariableNames" { } It "Common dynamic variable iteration" { $scriptDefinition = { - [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicVariableNames', 'My$_', Justification = 'Test')] + [Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDynamicallyCreatingVariableNames', 'My$_', Justification = 'Test')] Param() 'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process { New-Variable -Name "My$_" -Value ($i++) diff --git a/docs/Rules/AvoidDynamicVariableNames.md b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md similarity index 97% rename from docs/Rules/AvoidDynamicVariableNames.md rename to docs/Rules/AvoidDynamicallyCreatingVariableNames.md index 05086fd6a..cb909bbe5 100644 --- a/docs/Rules/AvoidDynamicVariableNames.md +++ b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md @@ -6,7 +6,7 @@ title: AvoidDynamicVariableNames --- # AvoidDynamicVariableNames -**Severity Level: Warning** +**Severity Level: Information** ## Description diff --git a/docs/Rules/README.md b/docs/Rules/README.md index ee82f511f..80ce7af2e 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -34,7 +34,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | Yes | | | [AvoidUsingDeprecatedManifestFields](./AvoidUsingDeprecatedManifestFields.md) | Warning | Yes | | | [AvoidUsingDoubleQuotesForConstantString](./AvoidUsingDoubleQuotesForConstantString.md) | Information | No | | -| [AvoidUsingDynamicVariableNames](./AvoidUsingDynamicVariableNames.md) | Warning | Yes | | +| [AvoidDynamicallyCreatingVariableNames](./AvoidDynamicallyCreatingVariableNames.md) | Information | Yes | | | [AvoidUsingEmptyCatchBlock](./AvoidUsingEmptyCatchBlock.md) | Warning | Yes | | | [AvoidUsingInvokeExpression](./AvoidUsingInvokeExpression.md) | Warning | Yes | | | [AvoidUsingPlainTextForPassword](./AvoidUsingPlainTextForPassword.md) | Warning | Yes | | From 8a7bbb0fc5e09c8380d2d9c49603e5bcc514a082 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 12:05:32 +0200 Subject: [PATCH 05/10] Update docs/Rules/AvoidDynamicallyCreatingVariableNames.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/Rules/AvoidDynamicallyCreatingVariableNames.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Rules/AvoidDynamicallyCreatingVariableNames.md b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md index cb909bbe5..041d0b3a1 100644 --- a/docs/Rules/AvoidDynamicallyCreatingVariableNames.md +++ b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md @@ -2,9 +2,9 @@ description: Avoid dynamic variable names, instead use a hash table or similar dictionary type. ms.date: 04/21/2026 ms.topic: reference -title: AvoidDynamicVariableNames +title: AvoidDynamicallyCreatingVariableNames --- -# AvoidDynamicVariableNames +# AvoidDynamicallyCreatingVariableNames **Severity Level: Information** From ba0f9a103999fa1f3c3d6fcd3e5dec2c98cffd26 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 12:06:25 +0200 Subject: [PATCH 06/10] Update docs/Rules/AvoidDynamicallyCreatingVariableNames.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/Rules/AvoidDynamicallyCreatingVariableNames.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Rules/AvoidDynamicallyCreatingVariableNames.md b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md index 041d0b3a1..c8f7860ec 100644 --- a/docs/Rules/AvoidDynamicallyCreatingVariableNames.md +++ b/docs/Rules/AvoidDynamicallyCreatingVariableNames.md @@ -38,9 +38,9 @@ $My = @{} $My.Two # returns 2 ``` -When it concerns a specific scope, option or visibility, put the concerned dictionary (hash table) in that -scope, option or visibility. In example, if the values should be read only and available in the script scope, -put the _hash table_ in the script scope and make it read only.: +When a specific scope, option, or visibility is required, put the dictionary (hash table) in that +scope and apply the appropriate option or visibility. For example, if the values should be read-only and +available in the script scope, put the _hash table_ in the script scope and make it read-only. ```powershell New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script From 680a3cc8c712c3da083fda7e4236a5c1e5c3df97 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 12:06:56 +0200 Subject: [PATCH 07/10] Update Rules/Strings.resx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Rules/Strings.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 1a414db00..4b0ab5aa5 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -880,10 +880,10 @@ Avoid dynamically creating variable names - Do not create variables with a dynamic name, this might introduce conflicts with other variables and is difficult to maintain. + Do not create variables with a dynamic name, as this might introduce conflicts with other variables and is difficult to maintain. - '{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name + '{0}' is a dynamic variable name. Please avoid creating variables with a dynamic name Avoid global functions and aliases From b7b7f1984d2dd6514e8a54b61e2d10e9f0d9af22 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 16:21:55 +0200 Subject: [PATCH 08/10] Corrected alphabetical order of rules in README.md --- docs/Rules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/README.md b/docs/Rules/README.md index 80ce7af2e..f214cbbb5 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -14,6 +14,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidAssignmentToAutomaticVariable](./AvoidAssignmentToAutomaticVariable.md) | Warning | Yes | | | [AvoidDefaultValueForMandatoryParameter](./AvoidDefaultValueForMandatoryParameter.md) | Warning | Yes | | | [AvoidDefaultValueSwitchParameter](./AvoidDefaultValueSwitchParameter.md) | Warning | Yes | | +| [AvoidDynamicallyCreatingVariableNames](./AvoidDynamicallyCreatingVariableNames.md) | Information | Yes | | | [AvoidExclaimOperator](./AvoidExclaimOperator.md) | Warning | No | | | [AvoidGlobalAliases1](./AvoidGlobalAliases.md) | Warning | Yes | | | [AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | Yes | | @@ -34,7 +35,6 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | Yes | | | [AvoidUsingDeprecatedManifestFields](./AvoidUsingDeprecatedManifestFields.md) | Warning | Yes | | | [AvoidUsingDoubleQuotesForConstantString](./AvoidUsingDoubleQuotesForConstantString.md) | Information | No | | -| [AvoidDynamicallyCreatingVariableNames](./AvoidDynamicallyCreatingVariableNames.md) | Information | Yes | | | [AvoidUsingEmptyCatchBlock](./AvoidUsingEmptyCatchBlock.md) | Warning | Yes | | | [AvoidUsingInvokeExpression](./AvoidUsingInvokeExpression.md) | Warning | Yes | | | [AvoidUsingPlainTextForPassword](./AvoidUsingPlainTextForPassword.md) | Warning | Yes | | From ae989fde74e8bbe29c969d35f204e02337eef7e2 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 16:29:13 +0200 Subject: [PATCH 09/10] Corrected $ruleMessage in Test Co-authored-by: Copilot --- Rules/AvoidDynamicallyCreatingVariableNames.cs | 2 +- Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rules/AvoidDynamicallyCreatingVariableNames.cs b/Rules/AvoidDynamicallyCreatingVariableNames.cs index 713fa8fc5..a186412e3 100644 --- a/Rules/AvoidDynamicallyCreatingVariableNames.cs +++ b/Rules/AvoidDynamicallyCreatingVariableNames.cs @@ -31,7 +31,7 @@ public class AvoidDynamicallyCreatingVariableNames : IScriptRule /// The name of the file being analyzed (for diagnostic reporting). /// A collection of diagnostic records for each violation. - readonly HashSet cmdList = Helper.Instance.CmdletNameAndAliases("New-Variable").ToHashSet(StringComparer.OrdinalIgnoreCase); + readonly HashSet cmdList = new HashSet(Helper.Instance.CmdletNameAndAliases("New-Variable"), StringComparer.OrdinalIgnoreCase); public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); diff --git a/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 index 7dc174b4f..62c49cf29 100644 --- a/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 +++ b/Tests/Rules/AvoidDynamicallyCreatingVariableNames.tests.ps1 @@ -7,7 +7,7 @@ param() BeforeAll { $ruleName = "PSAvoidDynamicallyCreatingVariableNames" - $ruleMessage = "'{0}' is a dynamic variable name. Please, avoid creating variables with a dynamic name" + $ruleMessage = "'{0}' is a dynamic variable name. Please avoid creating variables with a dynamic name" } Describe "AvoidDynamicallyCreatingVariableNames" { From 6fe86a94db66ffc25f7f441b397f904339600e7e Mon Sep 17 00:00:00 2001 From: iRon7 Date: Wed, 29 Apr 2026 16:34:47 +0200 Subject: [PATCH 10/10] Changed newVariableAst.Parent.Extent to newVariableAst.Extent --- Rules/AvoidDynamicallyCreatingVariableNames.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules/AvoidDynamicallyCreatingVariableNames.cs b/Rules/AvoidDynamicallyCreatingVariableNames.cs index a186412e3..f6ffb237f 100644 --- a/Rules/AvoidDynamicallyCreatingVariableNames.cs +++ b/Rules/AvoidDynamicallyCreatingVariableNames.cs @@ -61,7 +61,7 @@ testAst is CommandAst cmdAst && CultureInfo.CurrentCulture, Strings.AvoidDynamicallyCreatingVariableNamesError, variableName), - newVariableAst.Parent.Extent, + newVariableAst.Extent, GetName(), DiagnosticSeverity.Information, fileName,