Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Engine/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,15 @@ public IScriptExtent GetScriptExtentForFunctionName(FunctionDefinitionAst functi
token =>
ContainsExtent(functionDefinitionAst.Extent, token.Extent)
&& token.Text.Equals(functionDefinitionAst.Name));
var funcNameToken = funcNameTokens.FirstOrDefault();
return funcNameToken == null ? null : funcNameToken.Extent;

// If the functions name is 'function' then the first token in the
// list is the function keyword itself, so we need to skip it
if (functionDefinitionAst.Name.Equals("function"))
{
var funcNameToken = funcNameTokens.Skip(1).FirstOrDefault() ?? funcNameTokens.FirstOrDefault();
return funcNameToken?.Extent;
}
return funcNameTokens.FirstOrDefault()?.Extent;
}

/// <summary>
Expand Down
102 changes: 102 additions & 0 deletions Rules/AvoidReservedWordsAsFunctionNames.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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
static readonly HashSet<string> reservedWords = new HashSet<string>(
new[] {
"assembly", "base", "begin", "break",
"catch", "class", "command", "configuration",
"continue", "data", "define", "do",
"dynamicparam", "else", "elseif", "end",
"enum", "exit", "filter", "finally",
"for", "foreach", "from", "function",
"hidden", "if", "in", "inlinescript",
"interface", "module", "namespace", "parallel",
"param", "private", "process", "public",
"return", "sequence", "static", "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)
{
if (reservedWords.Contains(
Helper.Instance.FunctionNameWithoutScope(function.Name)
))
{
yield return new DiagnosticRecord(
string.Format(
CultureInfo.CurrentCulture,
Strings.AvoidReservedWordsAsFunctionNamesError,
Helper.Instance.FunctionNameWithoutScope(function.Name)),
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;
}
}
12 changes: 12 additions & 0 deletions Rules/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1224,4 +1224,16 @@
<data name="AvoidUsingAllowUnencryptedAuthenticationName" xml:space="preserve">
<value>AvoidUsingAllowUnencryptedAuthentication</value>
</data>
<data name="AvoidReservedWordsAsFunctionNamesCommonName" xml:space="preserve">
<value>Avoid Reserved Words as function names</value>
</data>
<data name="AvoidReservedWordsAsFunctionNamesDescription" xml:space="preserve">
<value>Avoid using reserved words as function names. Using reserved words as function names can cause errors or unexpected behavior in scripts.</value>
</data>
<data name="AvoidReservedWordsAsFunctionNamesName" xml:space="preserve">
<value>AvoidReservedWordsAsFunctionNames</value>
</data>
<data name="AvoidReservedWordsAsFunctionNamesError" xml:space="preserve">
<value>The reserved word '{0}' was used as a function name. This should be avoided.</value>
</data>
</root>
2 changes: 1 addition & 1 deletion Tests/Engine/GetScriptAnalyzerRule.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Describe "Test Name parameters" {

It "get Rules with no parameters supplied" {
$defaultRules = Get-ScriptAnalyzerRule
$expectedNumRules = 70
$expectedNumRules = 71
if ($PSVersionTable.PSVersion.Major -le 4)
{
# for PSv3 PSAvoidGlobalAliases is not shipped because
Expand Down
132 changes: 132 additions & 0 deletions Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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 = @(
'assembly','base','begin','break',
'catch','class','command','configuration',
'continue','data','define','do',
'dynamicparam','else','elseif','end',
'enum','exit','filter','finally',
'for','foreach','from','function',
'hidden','if','in','inlinescript',
'interface','module','namespace','parallel',
'param','private','process','public',
'return','sequence','static','switch',
'throw','trap','try','type',
'until','using','var','while','workflow'
)

$randomCasedReservedWords = @(
'aSSeMbLy','bASe','bEgIN','bReAk',
'cAtCh','CLasS','cOMmAnD','cONfiGuRaTioN',
'cONtiNuE','dAtA','dEFInE','Do',
'DyNaMiCpArAm','eLsE','eLsEiF','EnD',
'EnUm','eXiT','fIlTeR','fINaLLy',
'FoR','fOrEaCh','fROm','fUnCtIoN',
'hIdDeN','iF','IN','iNlInEsCrIpT',
'InTeRfAcE','mOdUlE','nAmEsPaCe','pArAlLeL',
'PaRaM','pRiVaTe','pRoCeSs','pUbLiC',
'ReTuRn','sEqUeNcE','StAtIc','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
}
}
}

$substringReservedWords = $reservedWords | ForEach-Object {
"$($_)Func"
}

$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 $_
}

# 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 {
$scriptDefinition = "function $_ { }"
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 0
}
}
}
42 changes: 42 additions & 0 deletions docs/Rules/AvoidReservedWordsAsFunctionNames.md
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!"
}
```
1 change: 1 addition & 0 deletions docs/Rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The PSScriptAnalyzer contains the following rule definitions.
| [AvoidMultipleTypeAttributes<sup>1</sup>](./AvoidMultipleTypeAttributes.md) | Warning | Yes | |
| [AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | Yes | |
| [AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | Yes | Yes |
| [AvoidReservedWordsAsFunctionNames](./AvoidReservedWordsAsFunctionNames.md) | Warning | Yes | |
| [AvoidSemicolonsAsLineTerminators](./AvoidSemicolonsAsLineTerminators.md) | Warning | No | |
| [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | |
| [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | |
Expand Down