Skip to content
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", StringComparison.OrdinalIgnoreCase))
{
var funcNameToken = funcNameTokens.Skip(1).FirstOrDefault() ?? funcNameTokens.FirstOrDefault();
return funcNameToken?.Extent;
}
return funcNameTokens.FirstOrDefault()?.Extent;
}

/// <summary>
Expand Down
103 changes: 103 additions & 0 deletions Rules/AvoidReservedWordsAsFunctionNames.cs
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;
}
}
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
147 changes: 147 additions & 0 deletions Tests/Rules/AvoidReservedWordsAsFunctionNames.tests.ps1
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 {
$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