-
Notifications
You must be signed in to change notification settings - Fork 401
Add AvoidReservedWordsAsFunctionNames Rule #2128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
andyleejordan
merged 9 commits into
PowerShell:main
from
liamjpeters:#2099PSAvoidReservedWordsAsFunctionNames
Oct 22, 2025
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
fbe1489
fix: Update helper GetScriptExtentForFunctionName to return correct e…
liamjpeters b4f67c2
Add AvoidReservedWordsAsFunctionNames rule
liamjpeters 1819eb3
fix: Handle functions with scopes
liamjpeters 1e5c1b2
Copyright header
liamjpeters 3b56a76
Extend Substring tests to starts with reserved word, ends with reserv…
liamjpeters 4510ff4
Check if the function name is 'Function' case insensitively. Add a te…
liamjpeters a9e6182
Fix casing of common name
liamjpeters 80172fe
AvoidReservedWordsAsFunctionNames: factor our function name for reada…
liamjpeters 8f89a1b
Remove reserved words which do not pose an issue:
liamjpeters File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // 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 | ||
|
|
||
| /// <summary> | ||
| /// Rule that warns when reserved words are used as function names | ||
| /// </summary> | ||
| public class AvoidReservedWordsAsFunctionNames : IScriptRule | ||
| { | ||
|
|
||
| // The list of PowerShell reserved words. | ||
| // https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words | ||
| // | ||
| // The Below are omitted as they don't pose an issue being a function | ||
| // name: | ||
| // assembly, base, command, hidden, in, inlinescript, interface, module, | ||
| // namespace, private, public, static | ||
| static readonly HashSet<string> reservedWords = new HashSet<string>( | ||
| new[] { | ||
| "begin", "break", "catch", "class", "configuration", | ||
| "continue", "data", "define", "do", | ||
| "dynamicparam", "else", "elseif", "end", | ||
| "enum", "exit", "filter", "finally", | ||
| "for", "foreach", "from", "function", | ||
| "if", "parallel", "param", "process", | ||
| "return", "sequence", "switch", | ||
| "throw", "trap", "try", "type", | ||
| "until", "using","var", "while", "workflow" | ||
| }, | ||
| StringComparer.OrdinalIgnoreCase | ||
| ); | ||
|
|
||
| /// <summary> | ||
| /// Analyzes the PowerShell AST for uses of reserved words as function names. | ||
| /// </summary> | ||
| /// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param> | ||
| /// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param> | ||
| /// <returns>A collection of diagnostic records for each violation.</returns> | ||
| public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName) | ||
| { | ||
| if (ast == null) | ||
| { | ||
| throw new ArgumentNullException(Strings.NullAstErrorMessage); | ||
| } | ||
|
|
||
| // Find all FunctionDefinitionAst in the Ast | ||
| var functionDefinitions = ast.FindAll( | ||
| astNode => astNode is FunctionDefinitionAst, | ||
| true | ||
| ).Cast<FunctionDefinitionAst>(); | ||
|
|
||
| foreach (var function in functionDefinitions) | ||
| { | ||
| string functionName = Helper.Instance.FunctionNameWithoutScope(function.Name); | ||
| if (reservedWords.Contains(functionName)) | ||
| { | ||
| yield return new DiagnosticRecord( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.AvoidReservedWordsAsFunctionNamesError, | ||
| functionName), | ||
| Helper.Instance.GetScriptExtentForFunctionName(function) ?? function.Extent, | ||
| GetName(), | ||
| DiagnosticSeverity.Warning, | ||
| fileName | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public string GetCommonName() => Strings.AvoidReservedWordsAsFunctionNamesCommonName; | ||
|
|
||
| public string GetDescription() => Strings.AvoidReservedWordsAsFunctionNamesDescription; | ||
|
|
||
| public string GetName() => string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.NameSpaceFormat, | ||
| GetSourceName(), | ||
| Strings.AvoidReservedWordsAsFunctionNamesName); | ||
|
|
||
| public RuleSeverity GetSeverity() => RuleSeverity.Warning; | ||
|
|
||
| public string GetSourceName() => Strings.SourceName; | ||
|
|
||
| public SourceType GetSourceType() => SourceType.Builtin; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| # Keep in sync with the rule's reserved words list in | ||
| # Rules/AvoidReservedWordsAsFunctionNames.cs | ||
| $reservedWords = @( | ||
| 'begin','break','catch','class','configuration', | ||
| 'continue','data','define','do', | ||
| 'dynamicparam','else','elseif','end', | ||
| 'enum','exit','filter','finally', | ||
| 'for','foreach','from','function', | ||
| 'if','parallel','param','process', | ||
| 'return','sequence','switch', | ||
| 'throw','trap','try','type', | ||
| 'until','using','var','while','workflow' | ||
| ) | ||
|
|
||
| $randomCasedReservedWords = @( | ||
| 'bEgIN','bReAk','cAtCh','CLasS','cONfiGuRaTioN', | ||
| 'cONtiNuE','dAtA','dEFInE','Do', | ||
| 'DyNaMiCpArAm','eLsE','eLsEiF','EnD', | ||
| 'EnUm','eXiT','fIlTeR','fINaLLy', | ||
| 'FoR','fOrEaCh','fROm','fUnCtIoN', | ||
| 'iF','pArAlLeL','PaRaM','pRoCeSs', | ||
| 'ReTuRn','sEqUeNcE','SwItCh', | ||
| 'tHrOw','TrAp','tRy','TyPe', | ||
| 'uNtIl','UsInG','VaR','wHiLe','wOrKfLoW' | ||
| ) | ||
|
|
||
| $functionScopes = @( | ||
| "global", "local", "script", "private" | ||
| ) | ||
|
|
||
| # Generate all combinations of reserved words and function scopes | ||
| $scopedReservedWordCases = foreach ($scope in $functionScopes) { | ||
| foreach ($word in $reservedWords) { | ||
| @{ | ||
| Scope = $scope | ||
| Name = $word | ||
| } | ||
| } | ||
| } | ||
|
|
||
| # Build variants of reserved words where the reserverd word: | ||
| # appearing at the start and end of a function | ||
| # name. | ||
| $substringReservedWords = $reservedWords | ForEach-Object { | ||
| "$($_)A", "A$($_)", $_.Substring(0, $_.Length - 1) | ||
| } | ||
|
|
||
| $safeFunctionNames = @( | ||
| 'Get-Something','Do-Work','Classify-Data','Begin-Process' | ||
| ) | ||
|
|
||
| BeforeAll { | ||
| $ruleName = 'PSAvoidReservedWordsAsFunctionNames' | ||
| } | ||
|
|
||
| Describe 'AvoidReservedWordsAsFunctionNames' { | ||
| Context 'When function names are reserved words' { | ||
| It 'flags reserved word "<_>" as a violation' -TestCases $reservedWords { | ||
|
|
||
| $scriptDefinition = "function $_ { 'test' }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
|
|
||
| $violations.Count | Should -Be 1 | ||
| $violations[0].Severity | Should -Be 'Warning' | ||
| $violations[0].RuleName | Should -Be $ruleName | ||
| # Message text should include the function name as used | ||
| $violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided." | ||
| # Extent should ideally capture only the function name | ||
| $violations[0].Extent.Text | Should -Be $_ | ||
| } | ||
|
|
||
| It 'flags the correct extent for a function named Function' { | ||
|
|
||
| $scriptDefinition = "Function Function { 'test' }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
|
|
||
| $violations.Count | Should -Be 1 | ||
| $violations[0].Severity | Should -Be 'Warning' | ||
| $violations[0].RuleName | Should -Be $ruleName | ||
| # Message text should include the function name as used | ||
| $violations[0].Message | Should -Be "The reserved word 'Function' was used as a function name. This should be avoided." | ||
| # Extent should ideally capture only the function name | ||
| $violations[0].Extent.Text | Should -Be 'Function' | ||
|
|
||
| # Make sure the extent is the correct `Function` (not the one at the | ||
| # very start) | ||
| $violations[0].Extent.StartOffset | Should -not -Be 0 | ||
| } | ||
|
|
||
| # Functions can have scopes. So function global:function {} should still | ||
| # alert. | ||
| It 'flags reserved word "<Name>" with scope "<Scope>" as a violation' -TestCases $scopedReservedWordCases { | ||
| param($Scope, $Name) | ||
|
|
||
| $scriptDefinition = "function $($Scope):$($Name) { 'test' }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
|
|
||
| $violations.Count | Should -Be 1 | ||
| $violations[0].Severity | Should -Be 'Warning' | ||
| $violations[0].RuleName | Should -Be $ruleName | ||
| $violations[0].Message | Should -Be "The reserved word '$Name' was used as a function name. This should be avoided." | ||
| $violations[0].Extent.Text | Should -Be "$($Scope):$($Name)" | ||
| } | ||
|
|
||
|
|
||
| It 'detects case-insensitively for "<_>"' -TestCases $randomCasedReservedWords { | ||
| $scriptDefinition = "function $_ { }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
| $violations.Count | Should -Be 1 | ||
| $violations[0].Message | Should -Be "The reserved word '$_' was used as a function name. This should be avoided." | ||
| } | ||
|
|
||
| It 'reports one finding per offending function' { | ||
| $scriptDefinition = 'function class { };function For { };function Safe-Name { };function TRy { }' | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
|
|
||
| $violations.Count | Should -Be 3 | ||
| $violations | ForEach-Object { $_.Severity | Should -Be 'Warning' } | ||
| ($violations | Select-Object -ExpandProperty Extent | Select-Object -ExpandProperty Text) | | ||
| Sort-Object | | ||
| Should -Be @('class','For','TRy') | ||
| } | ||
| } | ||
|
|
||
| Context 'When there are no violations' { | ||
| It 'does not flag safe function name "<_>"' -TestCases $safeFunctionNames { | ||
| $scriptDefinition = "function $_ { }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
| $violations.Count | Should -Be 0 | ||
| } | ||
|
|
||
| It 'does not flag when script has no functions' { | ||
| $scriptDefinition = '"hello";$x = 1..3 | ForEach-Object { $_ }' | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
| $violations.Count | Should -Be 0 | ||
| } | ||
|
|
||
| It 'does not flag substring-like name "<_>"' -TestCases $substringReservedWords { | ||
liamjpeters marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $scriptDefinition = "function $_ { }" | ||
| $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) | ||
| $violations.Count | Should -Be 0 | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| description: Avoid reserved words as function names | ||
| ms.date: 08/31/2025 | ||
| ms.topic: reference | ||
| title: AvoidReservedWordsAsFunctionNames | ||
| --- | ||
| # AvoidReservedWordsAsFunctionNames | ||
|
|
||
| **Severity Level: Warning** | ||
|
|
||
| ## Description | ||
|
|
||
| Avoid using reserved words as function names. Using reserved words as function | ||
| names can cause errors or unexpected behavior in scripts. | ||
|
|
||
| ## How to Fix | ||
|
|
||
| Avoid using any of the reserved words as function names. Instead, choose a | ||
| different name that is not reserved. | ||
|
|
||
| See [`about_Reserved_Words`](https://learn.microsoft.com/en-gb/powershell/module/microsoft.powershell.core/about/about_reserved_words) for a list of reserved | ||
| words in PowerShell. | ||
|
|
||
| ## Example | ||
|
|
||
| ### Wrong | ||
|
|
||
| ```powershell | ||
| # Function is a reserved word | ||
| function function { | ||
| Write-Host "Hello, World!" | ||
| } | ||
| ``` | ||
|
|
||
| ### Correct | ||
|
|
||
| ```powershell | ||
| # myFunction is not a reserved word | ||
| function myFunction { | ||
| Write-Host "Hello, World!" | ||
| } | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.