Skip to content

Commit 0e873c2

Browse files
committed
1st commit
1 parent a143b9f commit 0e873c2

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

Rules/AvoidDynamicVariableNames.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Globalization;
8+
using System.Management.Automation.Language;
9+
using System.Linq;
10+
11+
#if !CORECLR
12+
using System.ComponentModel.Composition;
13+
#endif
14+
15+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
16+
{
17+
#if !CORECLR
18+
[Export(typeof(IScriptRule))]
19+
#endif
20+
21+
/// <summary>
22+
/// Rule that warns when reserved words are used as function names
23+
/// </summary>
24+
public class AvoidDynamicVariableNames : IScriptRule
25+
{
26+
/// <summary>
27+
/// Analyzes the PowerShell AST for uses of reserved words as function names.
28+
/// </summary>
29+
/// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param>
30+
/// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param>
31+
/// <returns>A collection of diagnostic records for each violation.</returns>
32+
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
33+
{
34+
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
35+
36+
// Find all FunctionDefinitionAst in the Ast
37+
var newVariableAsts = ast.FindAll(testAst =>
38+
testAst is CommandAst cmdAst &&
39+
(
40+
String.Equals(cmdAst.GetCommandName(), "New-Variable", StringComparison.OrdinalIgnoreCase) ||
41+
String.Equals(cmdAst.GetCommandName(), "Set-Variable", StringComparison.OrdinalIgnoreCase)
42+
),
43+
true
44+
);
45+
46+
foreach (CommandAst newVariableAst in newVariableAsts)
47+
{
48+
// Use StaticParameterBinder to reliably get parameter values
49+
var bindingResult = StaticParameterBinder.BindCommand(newVariableAst, true);
50+
51+
// Dynamic parameters return null for the ConstantValue property
52+
if (
53+
bindingResult.BoundParameters.ContainsKey("Name") &&
54+
bindingResult.BoundParameters["Name"] == null
55+
)
56+
{
57+
yield return new DiagnosticRecord(
58+
string.Format(
59+
CultureInfo.CurrentCulture,
60+
Strings.AvoidDynamicVariableNamesError,
61+
newVariableAst.Parent.Extent.Text),
62+
newVariableAst.Parent.Extent,
63+
GetName(),
64+
DiagnosticSeverity.Warning,
65+
fileName
66+
);
67+
}
68+
}
69+
}
70+
71+
public string GetCommonName() => Strings.AvoidDynamicVariableNamesCommonName;
72+
73+
public string GetDescription() => Strings.AvoidDynamicVariableNamesDescription;
74+
75+
public string GetName() => string.Format(
76+
CultureInfo.CurrentCulture,
77+
Strings.NameSpaceFormat,
78+
GetSourceName(),
79+
Strings.AvoidDynamicVariableNamesName);
80+
81+
public RuleSeverity GetSeverity() => RuleSeverity.Warning;
82+
83+
public string GetSourceName() => Strings.SourceName;
84+
85+
public SourceType GetSourceType() => SourceType.Builtin;
86+
}
87+
}

Rules/Strings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,18 @@
873873
<data name="UseCompatibleTypesTypeAcceleratorError" xml:space="preserve">
874874
<value>The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}'</value>
875875
</data>
876+
<data name="AvoidDynamicVariableNamesName" xml:space="preserve">
877+
<value>AvoidDynamicVariableNames</value>
878+
</data>
879+
<data name="AvoidDynamicVariableNamesCommonName" xml:space="preserve">
880+
<value>Avoid dynamic variable names</value>
881+
</data>
882+
<data name="AvoidDynamicVariableNamesDescription" xml:space="preserve">
883+
<value>Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other variables and is difficult to maintain.</value>
884+
</data>
885+
<data name="AvoidDynamicVariableNamesError" xml:space="preserve">
886+
<value>Avoid dynamically creating variable names</value>
887+
</data>
876888
<data name="AvoidGlobalFunctionsCommonName" xml:space="preserve">
877889
<value>Avoid global functions and aliases</value>
878890
</data>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
description: Avoid dynamic variable names, instead use a hash table or similar dictionary type.
3+
ms.date: 04/21/2026
4+
ms.topic: reference
5+
title: AvoidDynamicVariableNames
6+
---
7+
# AvoidDynamicVariableNames
8+
9+
**Severity Level: Warning**
10+
11+
## Description
12+
13+
Do not dynamically create variable names in the general variable pool, this might introduce conflicts with other
14+
variables and is difficult to maintain.
15+
16+
## How
17+
18+
Use a hash table or similar dictionary type to store values with dynamic keys.
19+
20+
## Example
21+
22+
### Wrong
23+
24+
```powershell
25+
'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process {
26+
New-Variable -Name "My$_" -Value ($i++)
27+
}
28+
$MyTwo # returns 2
29+
```
30+
31+
### Correct
32+
33+
```powershell
34+
$My = @{}
35+
'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process {
36+
$My[$_] = $i++
37+
}
38+
$My.Two # returns 2
39+
```
40+
41+
When it concerns a specific scope, option or visibility, put the concerned dictionary (hash table) in that
42+
scope, option or visibility. In example, if the values should be read only and available in the script scope,
43+
put the _hash table_ in the script scope and make it read only.:
44+
45+
```powershell
46+
New-Variable -Name My -Value @{} -Option ReadOnly -Scope Script
47+
'One', 'Two', 'Three' | ForEach-Object -Begin { $i = 1 } -Process {
48+
$Script:My[$_] = $i++
49+
}
50+
$Script:My.Two # returns 2
51+
```

docs/Rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The PSScriptAnalyzer contains the following rule definitions.
3434
| [AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | Yes | |
3535
| [AvoidUsingDeprecatedManifestFields](./AvoidUsingDeprecatedManifestFields.md) | Warning | Yes | |
3636
| [AvoidUsingDoubleQuotesForConstantString](./AvoidUsingDoubleQuotesForConstantString.md) | Information | No | |
37+
| [AvoidUsingDynamicVariableNames](./AvoidUsingDynamicVariableNames.md) | Warning | Yes | |
3738
| [AvoidUsingEmptyCatchBlock](./AvoidUsingEmptyCatchBlock.md) | Warning | Yes | |
3839
| [AvoidUsingInvokeExpression](./AvoidUsingInvokeExpression.md) | Warning | Yes | |
3940
| [AvoidUsingPlainTextForPassword](./AvoidUsingPlainTextForPassword.md) | Warning | Yes | |

0 commit comments

Comments
 (0)