Skip to content

Commit 382278a

Browse files
committed
Add AvoidReservedWordsAsFunctionNames rule
1 parent 6fb3193 commit 382278a

File tree

6 files changed

+255
-1
lines changed

6 files changed

+255
-1
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 AvoidReservedWordsAsFunctionNames : IScriptRule
25+
{
26+
27+
// The list of PowerShell reserved words.
28+
// https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words
29+
static readonly HashSet<string> reservedWords = new HashSet<string>(
30+
new[] {
31+
"assembly", "base", "begin", "break",
32+
"catch", "class", "command", "configuration",
33+
"continue", "data", "define", "do",
34+
"dynamicparam", "else", "elseif", "end",
35+
"enum", "exit", "filter", "finally",
36+
"for", "foreach", "from", "function",
37+
"hidden", "if", "in", "inlinescript",
38+
"interface", "module", "namespace", "parallel",
39+
"param", "private", "process", "public",
40+
"return", "sequence", "static", "switch",
41+
"throw", "trap", "try", "type",
42+
"until", "using","var", "while", "workflow"
43+
},
44+
StringComparer.OrdinalIgnoreCase
45+
);
46+
47+
/// <summary>
48+
/// Analyzes the PowerShell AST for uses of reserved words as function names.
49+
/// </summary>
50+
/// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param>
51+
/// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param>
52+
/// <returns>A collection of diagnostic records for each violation.</returns>
53+
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
54+
{
55+
if (ast == null)
56+
{
57+
throw new ArgumentNullException(Strings.NullAstErrorMessage);
58+
}
59+
60+
// Find all FunctionDefinitionAst in the Ast
61+
var functionDefinitions = ast.FindAll(
62+
astNode => astNode is FunctionDefinitionAst,
63+
true
64+
).Cast<FunctionDefinitionAst>();
65+
66+
foreach (var function in functionDefinitions)
67+
{
68+
if (reservedWords.Contains(function.Name))
69+
{
70+
yield return new DiagnosticRecord(
71+
string.Format(
72+
CultureInfo.CurrentCulture,
73+
Strings.AvoidReservedWordsAsFunctionNamesError,
74+
function.Name),
75+
Helper.Instance.GetScriptExtentForFunctionName(function) ?? function.Extent,
76+
GetName(),
77+
DiagnosticSeverity.Warning,
78+
fileName
79+
);
80+
}
81+
}
82+
}
83+
84+
public string GetCommonName() => Strings.AvoidReservedWordsAsFunctionNamesCommonName;
85+
86+
public string GetDescription() => Strings.AvoidReservedWordsAsFunctionNamesDescription;
87+
88+
public string GetName() => string.Format(
89+
CultureInfo.CurrentCulture,
90+
Strings.NameSpaceFormat,
91+
GetSourceName(),
92+
Strings.AvoidReservedWordsAsFunctionNamesName);
93+
94+
public RuleSeverity GetSeverity() => RuleSeverity.Warning;
95+
96+
public string GetSourceName() => Strings.SourceName;
97+
98+
public SourceType GetSourceType() => SourceType.Builtin;
99+
}
100+
}

Rules/Strings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,4 +1224,16 @@
12241224
<data name="AvoidUsingAllowUnencryptedAuthenticationName" xml:space="preserve">
12251225
<value>AvoidUsingAllowUnencryptedAuthentication</value>
12261226
</data>
1227+
<data name="AvoidReservedWordsAsFunctionNamesCommonName" xml:space="preserve">
1228+
<value>Avoid Reserved Words as function names</value>
1229+
</data>
1230+
<data name="AvoidReservedWordsAsFunctionNamesDescription" xml:space="preserve">
1231+
<value>Avoid using reserved words as function names. Using reserved words as function names can cause errors or unexpected behavior in scripts.</value>
1232+
</data>
1233+
<data name="AvoidReservedWordsAsFunctionNamesName" xml:space="preserve">
1234+
<value>AvoidReservedWordsAsFunctionNames</value>
1235+
</data>
1236+
<data name="AvoidReservedWordsAsFunctionNamesError" xml:space="preserve">
1237+
<value>The reserved word '{0}' was used as a function name. This should be avoided.</value>
1238+
</data>
12271239
</root>

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Describe "Test Name parameters" {
6363

6464
It "get Rules with no parameters supplied" {
6565
$defaultRules = Get-ScriptAnalyzerRule
66-
$expectedNumRules = 70
66+
$expectedNumRules = 71
6767
if ($PSVersionTable.PSVersion.Major -le 4)
6868
{
6969
# for PSv3 PSAvoidGlobalAliases is not shipped because
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Keep in sync with the rule's reserved words list in
2+
# Rules/AvoidReservedWordsAsFunctionNames.cs
3+
$reservedWords = @(
4+
'assembly','base','begin','break',
5+
'catch','class','command','configuration',
6+
'continue','data','define','do',
7+
'dynamicparam','else','elseif','end',
8+
'enum','exit','filter','finally',
9+
'for','foreach','from','function',
10+
'hidden','if','in','inlinescript',
11+
'interface','module','namespace','parallel',
12+
'param','private','process','public',
13+
'return','sequence','static','switch',
14+
'throw','trap','try','type',
15+
'until','using','var','while','workflow'
16+
)
17+
18+
$randomCasedReservedWords = @(
19+
'aSSeMbLy','bASe','bEgIN','bReAk',
20+
'cAtCh','CLasS','cOMmAnD','cONfiGuRaTioN',
21+
'cONtiNuE','dAtA','dEFInE','Do',
22+
'DyNaMiCpArAm','eLsE','eLsEiF','EnD',
23+
'EnUm','eXiT','fIlTeR','fINaLLy',
24+
'FoR','fOrEaCh','fROm','fUnCtIoN',
25+
'hIdDeN','iF','IN','iNlInEsCrIpT',
26+
'InTeRfAcE','mOdUlE','nAmEsPaCe','pArAlLeL',
27+
'PaRaM','pRiVaTe','pRoCeSs','pUbLiC',
28+
'ReTuRn','sEqUeNcE','StAtIc','SwItCh',
29+
'tHrOw','TrAp','tRy','TyPe',
30+
'uNtIl','UsInG','VaR','wHiLe','wOrKfLoW'
31+
)
32+
33+
$substringReservedWords = $reservedWords | ForEach-Object {
34+
"$($_)Func"
35+
}
36+
37+
$safeFunctionNames = @(
38+
'Get-Something','Do-Work','Classify-Data','Begin-Process'
39+
)
40+
41+
BeforeAll {
42+
$ruleName = 'PSAvoidReservedWordsAsFunctionNames'
43+
}
44+
45+
Describe 'AvoidReservedWordsAsFunctionNames' {
46+
Context 'When function names are reserved words' {
47+
It 'flags reserved word "<_>" as a violation' -TestCases $reservedWords {
48+
49+
$scriptDefinition = "function $_ { 'test' }"
50+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
51+
52+
$violations.Count | Should -Be 1
53+
$violations[0].Severity | Should -Be 'Warning'
54+
$violations[0].RuleName | Should -Be $ruleName
55+
# Message text should include the function name as used
56+
$violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided."
57+
# Extent should ideally capture only the function name
58+
$violations[0].Extent.Text | Should -Be $_
59+
}
60+
61+
It 'detects case-insensitively for "<_>"' -TestCases $randomCasedReservedWords {
62+
$scriptDefinition = "function $_ { }"
63+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
64+
$violations.Count | Should -Be 1
65+
$violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided."
66+
}
67+
68+
It 'reports one finding per offending function' {
69+
$scriptDefinition = 'function class { };function For { };function Safe-Name { };function TRy { }'
70+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
71+
72+
$violations.Count | Should -Be 3
73+
$violations | ForEach-Object { $_.Severity | Should -Be 'Warning' }
74+
($violations | Select-Object -ExpandProperty Extent | Select-Object -ExpandProperty Text) |
75+
Sort-Object |
76+
Should -Be @('class','For','TRy')
77+
}
78+
}
79+
80+
Context 'When there are no violations' {
81+
It 'does not flag safe function name "<_>"' -TestCases $safeFunctionNames {
82+
$scriptDefinition = "function $_ { }"
83+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
84+
$violations.Count | Should -Be 0
85+
}
86+
87+
It 'does not flag when script has no functions' {
88+
$scriptDefinition = '"hello";$x = 1..3 | ForEach-Object { $_ }'
89+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
90+
$violations.Count | Should -Be 0
91+
}
92+
93+
It 'does not flag substring-like name "<_>"' -TestCases $substringReservedWords {
94+
$scriptDefinition = "function $_ { }"
95+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
96+
$violations.Count | Should -Be 0
97+
}
98+
}
99+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
description: Avoid reserved words as function names
3+
ms.date: 08/31/2025
4+
ms.topic: reference
5+
title: AvoidReservedWordsAsFunctionNames
6+
---
7+
# AvoidReservedWordsAsFunctionNames
8+
9+
**Severity Level: Warning**
10+
11+
## Description
12+
13+
Avoid using reserved words as function names. Using reserved words as function
14+
names can cause errors or unexpected behavior in scripts.
15+
16+
## How to Fix
17+
18+
Avoid using any of the reserved words as function names. Instead, choose a
19+
different name that is not reserved.
20+
21+
See [`about_Reserved_Words`](https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words) for a list of reserved
22+
words in PowerShell.
23+
24+
## Example
25+
26+
### Wrong
27+
28+
```powershell
29+
# Function is a reserved word
30+
function function {
31+
Write-Host "Hello, World!"
32+
}
33+
```
34+
35+
### Correct
36+
37+
```powershell
38+
# myFunction is not a reserved word
39+
function myFunction {
40+
Write-Host "Hello, World!"
41+
}
42+
```

docs/Rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The PSScriptAnalyzer contains the following rule definitions.
2323
| [AvoidMultipleTypeAttributes<sup>1</sup>](./AvoidMultipleTypeAttributes.md) | Warning | Yes | |
2424
| [AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | Yes | |
2525
| [AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | Yes | Yes |
26+
| [AvoidReservedWordsAsFunctionNames](./AvoidReservedWordsAsFunctionNames.md) | Warning | Yes | |
2627
| [AvoidSemicolonsAsLineTerminators](./AvoidSemicolonsAsLineTerminators.md) | Warning | No | |
2728
| [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | |
2829
| [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | |

0 commit comments

Comments
 (0)