From c34eb236b4571eff1d820f0506646fa13e890445 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Tue, 14 Apr 2026 18:58:53 +0200 Subject: [PATCH 01/15] Implemented the AvoidUsingArrayList rule to warn when the ArrayList class is used in PowerShell scripts. Added tests for both violations and non-violations of this rule. Updated documentation to include the new rule and its guidelines. --- .vscode/settings.json | 5 +- Rules/AvoidUsingArrayList.cs | 144 ++++++++++++++++++ Rules/Strings.resx | 12 ++ Tests/Rules/AvoidUsingArrayList.ps1 | 18 +++ Tests/Rules/AvoidUsingArrayList.tests.ps1 | 26 ++++ .../Rules/AvoidUsingArrayListNoViolations.ps1 | 19 +++ docs/Rules/AvoidUsingArrayList.md | 47 ++++++ docs/Rules/README.md | 1 + 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 Rules/AvoidUsingArrayList.cs create mode 100644 Tests/Rules/AvoidUsingArrayList.ps1 create mode 100644 Tests/Rules/AvoidUsingArrayList.tests.ps1 create mode 100644 Tests/Rules/AvoidUsingArrayListNoViolations.ps1 create mode 100644 docs/Rules/AvoidUsingArrayList.md diff --git a/.vscode/settings.json b/.vscode/settings.json index d55f534ff..eb5ff8a7e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ "**/bower_components": true, "/PSCompatibilityCollector/profiles": true, "/PSCompatibilityCollector/optional_profiles": true - } + }, + "cSpell.words": [ + "CORECLR" + ] } \ No newline at end of file diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs new file mode 100644 index 000000000..ca3ed2f98 --- /dev/null +++ b/Rules/AvoidUsingArrayList.cs @@ -0,0 +1,144 @@ +// 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.Text.RegularExpressions; +using System.ComponentModel; + + +#if !CORECLR +using System.ComponentModel.Composition; +#endif + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + + /// + /// Rule that warns when the ArrayList class is used in a PowerShell script. + /// + public class AvoidUsingArrayListAsFunctionNames : IScriptRule + { + + /// + /// Analyzes the PowerShell AST for uses of the ArrayList class. + /// + /// The PowerShell Abstract Syntax Tree to analyze. + /// The name of the file being analyzed (for diagnostic reporting). + /// A collection of diagnostic records for each violation. + + public IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) { throw new ArgumentNullException(Strings.NullAstErrorMessage); } + + // If there is an using statement for the Collections namespace, check for the full typename. + // Otherwise also check for the bare ArrayList name. + Regex ArrayListName = null; + var sbAst = ast as ScriptBlockAst; + foreach (UsingStatementAst usingAst in sbAst.UsingStatements) + { + if ( + usingAst.UsingStatementKind == UsingStatementKind.Namespace && + ( + usingAst.Name.Value.Equals("Collections", StringComparison.OrdinalIgnoreCase) || + usingAst.Name.Value.Equals("System.Collections", StringComparison.OrdinalIgnoreCase) + ) + ) + { + ArrayListName = new Regex(@"^((System\.)?Collections\.)?ArrayList$", RegexOptions.IgnoreCase); + break; + } + } + if (ArrayListName == null) { ArrayListName = new Regex(@"^(System\.)?Collections\.ArrayList", RegexOptions.IgnoreCase); } + + // Find all type initializers that create a new instance of the ArrayList class. + IEnumerable typeAsts = ast.FindAll(testAst => + ( + testAst is ConvertExpressionAst convertAst && + convertAst.StaticType != null && + convertAst.StaticType.FullName == "System.Collections.ArrayList" + ) || + ( + testAst is TypeExpressionAst typeAst && + typeAst.TypeName != null && + ArrayListName.IsMatch(typeAst.TypeName.Name) && + typeAst.Parent is InvokeMemberExpressionAst parentAst && + parentAst.Member != null && + parentAst.Member is StringConstantExpressionAst memberAst && + memberAst.Value.Equals("new", StringComparison.OrdinalIgnoreCase) + ), + true + ); + + foreach (Ast typeAst in typeAsts) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidUsingArrayListError, + typeAst.Parent.Extent.Text), + typeAst.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + + // Find all New-Object cmdlets that create a new instance of the ArrayList class. + var newObjectCommands = ast.FindAll(testAst => + testAst is CommandAst cmdAst && + cmdAst.GetCommandName() != null && + cmdAst.GetCommandName().Equals("New-Object", StringComparison.OrdinalIgnoreCase), + true); + + foreach (CommandAst cmd in newObjectCommands) + { + // Use StaticParameterBinder to reliably get parameter values + var bindingResult = StaticParameterBinder.BindCommand(cmd, true); + + // Check for -TypeName parameter + if ( + bindingResult.BoundParameters.ContainsKey("TypeName") && + ArrayListName.IsMatch(bindingResult.BoundParameters["TypeName"].ConstantValue as string) + ) + { + yield return new DiagnosticRecord( + string.Format( + CultureInfo.CurrentCulture, + Strings.AvoidUsingArrayListError, + cmd.Extent.Text), + bindingResult.BoundParameters["TypeName"].Value.Extent, + GetName(), + DiagnosticSeverity.Warning, + fileName + ); + } + + } + + + } + + public string GetCommonName() => Strings.AvoidUsingArrayListCommonName; + + public string GetDescription() => Strings.AvoidUsingArrayListDescription; + + public string GetName() => string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidUsingArrayListName); + + public RuleSeverity GetSeverity() => RuleSeverity.Warning; + + public string GetSourceName() => Strings.SourceName; + + public SourceType GetSourceType() => SourceType.Builtin; + } +} \ No newline at end of file diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 2a04fd759..f73b68d1e 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -932,6 +932,18 @@ Line ends with a semicolon + + + Avoid using the ArrayList class + + + Avoid using the ArrayList class in PowerShell scripts. Consider using generic collections or fixed arrays instead. + + + AvoidUsingArrayList + + + The ArrayList class is used in '{0}'. Consider using a generic collection or a fixed array instead. PlaceOpenBrace diff --git a/Tests/Rules/AvoidUsingArrayList.ps1 b/Tests/Rules/AvoidUsingArrayList.ps1 new file mode 100644 index 000000000..0644220e6 --- /dev/null +++ b/Tests/Rules/AvoidUsingArrayList.ps1 @@ -0,0 +1,18 @@ +using namespace system.collections + +# Using New-Object +$List = New-Object ArrayList +$List = New-Object 'ArrayList' +$List = New-Object "ArrayList" +$List = New-Object -Type ArrayList +$List = New-Object -TypeName ArrayLIST +$List = New-Object Collections.ArrayList +$List = New-Object System.Collections.ArrayList + +# Using type initializer +$List = [ArrayList](1,2,3) +$List = [ArrayLIST]@(1,2,3) +$List = [ArrayList]::new() +$List = [Collections.ArrayList]::New() +$List = [System.Collections.ArrayList]::new() +1..3 | ForEach-Object { $null = $List.Add($_) } diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 new file mode 100644 index 000000000..b9c51cf36 --- /dev/null +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeAll { + $ruleName = "PSAvoidArrayList" + $ruleMessage = "The ArrayList class is used in '*'. Consider using a generic collection or a fixed array instead." +} + +Describe "AvoidUsingWriteHost" { + Context "When there are violations" { + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + It "has ArrayList violations" { + $violations.Count | Should -Be 12 + } + + It "has the correct description message" { + $violations[0].Message | Should -Like $ruleMessage + } + } + + Context "When there are no violations" { + It "returns no violations" { + $noViolations.Count | Should -Be 0 + } + } +} diff --git a/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 b/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 new file mode 100644 index 000000000..469d8214a --- /dev/null +++ b/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 @@ -0,0 +1,19 @@ +using namespace System.Collections.Generic + +# Using a generic List +$List = New-Object List[Object] +1..3 | ForEach-Object { $List.Add($_) } # This will not return anything + +$List = [List[Object]]::new() +1..3 | ForEach-Object { $List.Add($_) } # This will not return anything + +# Creating a fixed array by using the PowerShell pipeline +$List = 1..3 | ForEach-Object { $_ } + +# This should not violate because there isn't a +# `using namespace System.Collections` directive +# and ArrayList could belong to another namespace +$List = New-Object ArrayList +$List = [ArrayList](1,2,3) +$List = [ArrayList]@(1,2,3) +$List = [ArrayList]::new() \ No newline at end of file diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md new file mode 100644 index 000000000..d117f57c9 --- /dev/null +++ b/docs/Rules/AvoidUsingArrayList.md @@ -0,0 +1,47 @@ +--- +description: Avoid reserved words as function names +ms.date: 08/31/2025 +ms.topic: reference +title: AvoidUsingArrayList +--- +# AvoidUsingArrayList + +**Severity Level: Warning** + +## Description + + Important + +Avoid the ArrayList class for new development. +The `ArrayList` class is a non-generic collection that can hold objects of any type. This is inline with the fact +that PowerShell is a weakly typed language. However, the `ArrayList` class does not provide any explicit type +safety and performance benefits of generic collections. Instead of using an `ArrayList`, consider using either a +[`System.Collections.Generic.List[Object]`](https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1) +class or a fixed PowerShell array. Besides, the `ArrayList.Add` method returns the index of the added element which +often unintendedly pollutes the PowerShell pipeline and therefore might cause unexpected issues. + + +## How to Fix + +## Example + +### Wrong + +```powershell +# Using an ArrayList +$List = [System.Collections.ArrayList]::new() +1..3 | ForEach-Object { $List.Add($_) } # Note that this will return the index of the added element +``` + +### Correct + +```powershell +# Using a generic List +$List = [System.Collections.Generic.List[Object]]::new() +1..3 | ForEach-Object { $List.Add($_) } # This will not return anything +``` + +```PowerShell +# Creating a fixed array by using the PowerShell pipeline +$List = 1..3 | ForEach-Object { $_ } +``` \ No newline at end of file diff --git a/docs/Rules/README.md b/docs/Rules/README.md index fca031e33..40ef2958d 100644 --- a/docs/Rules/README.md +++ b/docs/Rules/README.md @@ -28,6 +28,7 @@ The PSScriptAnalyzer contains the following rule definitions. | [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | | | [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | | | [AvoidUsingAllowUnencryptedAuthentication](./AvoidUsingAllowUnencryptedAuthentication.md) | Warning | Yes | | +| [AvoidUsingArrayList](./AvoidUsingArrayList.md) | Warning | Yes | | | [AvoidUsingBrokenHashAlgorithms](./AvoidUsingBrokenHashAlgorithms.md) | Warning | Yes | | | [AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | Yes2 | | [AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | Yes | | From ba1167d577989d302f71c2a2d6ea09d25de20101 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 10:16:44 +0200 Subject: [PATCH 02/15] Testing-Commit-CSpell-issue --- .vscode/settings.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index eb5ff8a7e..d55f534ff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,8 +10,5 @@ "**/bower_components": true, "/PSCompatibilityCollector/profiles": true, "/PSCompatibilityCollector/optional_profiles": true - }, - "cSpell.words": [ - "CORECLR" - ] + } } \ No newline at end of file From 77384e11bb0b43c2dfd06e0007cd6fd0cb5ac168 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 10:48:13 +0200 Subject: [PATCH 03/15] Apply suggestion from @liamjpeters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I clearly used several other rules as a kind of template...🤪 Co-authored-by: Liam Peters --- docs/Rules/AvoidUsingArrayList.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md index d117f57c9..b0b8b68e9 100644 --- a/docs/Rules/AvoidUsingArrayList.md +++ b/docs/Rules/AvoidUsingArrayList.md @@ -1,5 +1,5 @@ --- -description: Avoid reserved words as function names +description: Avoid using ArrayList ms.date: 08/31/2025 ms.topic: reference title: AvoidUsingArrayList From 77d1e6dac8b90ccf54324b431ebf2c5fb5be69a9 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 10:55:19 +0200 Subject: [PATCH 04/15] Update docs/Rules/AvoidUsingArrayList.md Co-authored-by: Liam Peters --- docs/Rules/AvoidUsingArrayList.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md index b0b8b68e9..4b478ee06 100644 --- a/docs/Rules/AvoidUsingArrayList.md +++ b/docs/Rules/AvoidUsingArrayList.md @@ -1,6 +1,6 @@ --- description: Avoid using ArrayList -ms.date: 08/31/2025 +ms.date: 04/15/2026 ms.topic: reference title: AvoidUsingArrayList --- From d9b88a8b23b29315660a76f85e3c863128fe5dd7 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 11:58:04 +0200 Subject: [PATCH 05/15] Updated rule help --- docs/Rules/AvoidUsingArrayList.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md index b0b8b68e9..79add59c6 100644 --- a/docs/Rules/AvoidUsingArrayList.md +++ b/docs/Rules/AvoidUsingArrayList.md @@ -1,6 +1,6 @@ --- description: Avoid using ArrayList -ms.date: 08/31/2025 +ms.date: 04/16/2025 ms.topic: reference title: AvoidUsingArrayList --- @@ -10,19 +10,24 @@ title: AvoidUsingArrayList ## Description - Important +Per dotnet best practices, the +[`ArrayList` class](https://learn.microsoft.com/dotnet/api/system.collections.arraylist) +is not recommended for new development, the same recommendation applies to PowerShell: Avoid the ArrayList class for new development. The `ArrayList` class is a non-generic collection that can hold objects of any type. This is inline with the fact that PowerShell is a weakly typed language. However, the `ArrayList` class does not provide any explicit type safety and performance benefits of generic collections. Instead of using an `ArrayList`, consider using either a [`System.Collections.Generic.List[Object]`](https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1) -class or a fixed PowerShell array. Besides, the `ArrayList.Add` method returns the index of the added element which -often unintendedly pollutes the PowerShell pipeline and therefore might cause unexpected issues. - +class or a fixed PowerShell array. +Besides, the `ArrayList.Add` method returns the index of the added element which often unintendedly pollutes the +PowerShell pipeline and therefore might cause unexpected issues. ## How to Fix +In cases where only the `Add` method is used, you might just replace the `ArrayList` class with a generic +`List[Object]` class but you could also consider using the idiomatic PowerShell pipeline syntax instead. + ## Example ### Wrong From c0aa82b322e60abd05b085338193258af2fdc0e6 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 13:32:59 +0200 Subject: [PATCH 06/15] Changed "unintentionally" --- docs/Rules/AvoidUsingArrayList.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Rules/AvoidUsingArrayList.md b/docs/Rules/AvoidUsingArrayList.md index 79add59c6..b56d47424 100644 --- a/docs/Rules/AvoidUsingArrayList.md +++ b/docs/Rules/AvoidUsingArrayList.md @@ -20,7 +20,7 @@ that PowerShell is a weakly typed language. However, the `ArrayList` class does safety and performance benefits of generic collections. Instead of using an `ArrayList`, consider using either a [`System.Collections.Generic.List[Object]`](https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1) class or a fixed PowerShell array. -Besides, the `ArrayList.Add` method returns the index of the added element which often unintendedly pollutes the +Besides, the `ArrayList.Add` method returns the index of the added element which often unintentionally pollutes the PowerShell pipeline and therefore might cause unexpected issues. ## How to Fix From 7bc3dfad148607267983984ccaee1c22a962470b Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 13:35:26 +0200 Subject: [PATCH 07/15] Update Rules/AvoidUsingArrayList.cs Co-authored-by: Liam Peters --- Rules/AvoidUsingArrayList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index ca3ed2f98..9c4569997 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -55,7 +55,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) break; } } - if (ArrayListName == null) { ArrayListName = new Regex(@"^(System\.)?Collections\.ArrayList", RegexOptions.IgnoreCase); } + if (ArrayListName == null) { ArrayListName = new Regex(@"^(System\.)?Collections\.ArrayList$", RegexOptions.IgnoreCase); } // Find all type initializers that create a new instance of the ArrayList class. IEnumerable typeAsts = ast.FindAll(testAst => From b35becb93524dad85cc06bb1ed07b589168f9d9e Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 13:36:08 +0200 Subject: [PATCH 08/15] Update Tests/Rules/AvoidUsingArrayList.tests.ps1 Co-authored-by: Liam Peters --- Tests/Rules/AvoidUsingArrayList.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 index b9c51cf36..bcb6040c5 100644 --- a/Tests/Rules/AvoidUsingArrayList.tests.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -6,7 +6,7 @@ BeforeAll { $ruleMessage = "The ArrayList class is used in '*'. Consider using a generic collection or a fixed array instead." } -Describe "AvoidUsingWriteHost" { +Describe "AvoidArrayList" { Context "When there are violations" { $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) It "has ArrayList violations" { From 9ed13550f6c80a2457113f44c7eda0f200fc8cce Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 13:42:25 +0200 Subject: [PATCH 09/15] ArrayListName could be null --- Rules/AvoidUsingArrayList.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index 9c4569997..6e659acc5 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -105,6 +105,7 @@ testAst is CommandAst cmdAst && // Check for -TypeName parameter if ( bindingResult.BoundParameters.ContainsKey("TypeName") && + ArrayListName != null && ArrayListName.IsMatch(bindingResult.BoundParameters["TypeName"].ConstantValue as string) ) { From e1362e62c006d7bd5bfa97a2a9b5d05df659e31e Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 14:21:44 +0200 Subject: [PATCH 10/15] Resolved camelCase --- Rules/AvoidUsingArrayList.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index 6e659acc5..602d99bf4 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -39,7 +39,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // If there is an using statement for the Collections namespace, check for the full typename. // Otherwise also check for the bare ArrayList name. - Regex ArrayListName = null; + Regex arrayListName = null; var sbAst = ast as ScriptBlockAst; foreach (UsingStatementAst usingAst in sbAst.UsingStatements) { @@ -51,11 +51,11 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) ) ) { - ArrayListName = new Regex(@"^((System\.)?Collections\.)?ArrayList$", RegexOptions.IgnoreCase); + arrayListName = new Regex(@"^((System\.)?Collections\.)?ArrayList$", RegexOptions.IgnoreCase); break; } } - if (ArrayListName == null) { ArrayListName = new Regex(@"^(System\.)?Collections\.ArrayList$", RegexOptions.IgnoreCase); } + if (arrayListName == null) { arrayListName = new Regex(@"^(System\.)?Collections\.ArrayList$", RegexOptions.IgnoreCase); } // Find all type initializers that create a new instance of the ArrayList class. IEnumerable typeAsts = ast.FindAll(testAst => @@ -67,7 +67,7 @@ testAst is ConvertExpressionAst convertAst && ( testAst is TypeExpressionAst typeAst && typeAst.TypeName != null && - ArrayListName.IsMatch(typeAst.TypeName.Name) && + arrayListName.IsMatch(typeAst.TypeName.Name) && typeAst.Parent is InvokeMemberExpressionAst parentAst && parentAst.Member != null && parentAst.Member is StringConstantExpressionAst memberAst && @@ -105,8 +105,8 @@ testAst is CommandAst cmdAst && // Check for -TypeName parameter if ( bindingResult.BoundParameters.ContainsKey("TypeName") && - ArrayListName != null && - ArrayListName.IsMatch(bindingResult.BoundParameters["TypeName"].ConstantValue as string) + arrayListName != null && + arrayListName.IsMatch(bindingResult.BoundParameters["TypeName"].ConstantValue as string) ) { yield return new DiagnosticRecord( From b80f01c8d00821bb6ac3f8af2f884cb0c58ae4d6 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 14:23:44 +0200 Subject: [PATCH 11/15] Remove ComponentModel namespace --- Rules/AvoidUsingArrayList.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index 602d99bf4..37a32de9e 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -7,8 +7,6 @@ using System.Globalization; using System.Management.Automation.Language; using System.Text.RegularExpressions; -using System.ComponentModel; - #if !CORECLR using System.ComponentModel.Composition; From f913b1f058af6637f9ccccbc074859fad52bc4a8 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 16:42:33 +0200 Subject: [PATCH 12/15] Fixed tests --- Tests/Rules/AvoidUsingArrayList.ps1 | 1 + Tests/Rules/AvoidUsingArrayList.tests.ps1 | 32 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Tests/Rules/AvoidUsingArrayList.ps1 b/Tests/Rules/AvoidUsingArrayList.ps1 index 0644220e6..2430af6ed 100644 --- a/Tests/Rules/AvoidUsingArrayList.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.ps1 @@ -15,4 +15,5 @@ $List = [ArrayLIST]@(1,2,3) $List = [ArrayList]::new() $List = [Collections.ArrayList]::New() $List = [System.Collections.ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 index bcb6040c5..350fe26e6 100644 --- a/Tests/Rules/AvoidUsingArrayList.tests.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -1,20 +1,40 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +using namespace System.Management.Automation.Language + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param() + BeforeAll { - $ruleName = "PSAvoidArrayList" - $ruleMessage = "The ArrayList class is used in '*'. Consider using a generic collection or a fixed array instead." + $ruleName = "PSAvoidUsingArrayList" + $ruleMessage = "The ArrayList class is used in '{0}'. Consider using a generic collection or a fixed array instead." } Describe "AvoidArrayList" { Context "When there are violations" { - $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) - It "has ArrayList violations" { + + BeforeAll { + $violationFileName = "$PSScriptRoot\AvoidUsingArrayList.ps1" + $violations = Invoke-ScriptAnalyzer $violationFileName | Where-Object RuleName -eq $ruleName + + $violationExtents = @{} + [Parser]::ParseFile($violationFileName, [ref] $null, [ref] $null).FindAll({ + $Args[0] -is [AssignmentStatementAst] -and + $Args[0].Left.Extent.Text -eq '$List' + }, $false).Right.Extent.foreach{ $violationExtents[$_.StartLineNumber] = $_ } + } + + It "Should return 12 violations" { $violations.Count | Should -Be 12 } - It "has the correct description message" { - $violations[0].Message | Should -Like $ruleMessage + It "Should return a correct violation record" -ForEach $violations { + $expectedViolation = $violationExtents[$_.Line] + $_.Extent.Text | Should -Be $expectedViolation.Text + $_.Message | Should -Be ($ruleMessage -f $expectedViolation.Text) + $_.Severity | Should -Be Warning + $_.ScriptName | Should -Be AvoidUsingArrayList.ps1 } } From b3a5c3cb3639dab0e0e84e8b4fd2071b386f055e Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 19:07:27 +0200 Subject: [PATCH 13/15] Updated Tests --- Rules/AvoidUsingArrayList.cs | 4 ++-- Tests/Rules/AvoidUsingArrayList.tests.ps1 | 29 +++++++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index 37a32de9e..92784233a 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -81,7 +81,7 @@ parentAst.Member is StringConstantExpressionAst memberAst && CultureInfo.CurrentCulture, Strings.AvoidUsingArrayListError, typeAst.Parent.Extent.Text), - typeAst.Extent, + typeAst.Parent.Extent, GetName(), DiagnosticSeverity.Warning, fileName @@ -112,7 +112,7 @@ testAst is CommandAst cmdAst && CultureInfo.CurrentCulture, Strings.AvoidUsingArrayListError, cmd.Extent.Text), - bindingResult.BoundParameters["TypeName"].Value.Extent, + cmd.Extent, GetName(), DiagnosticSeverity.Warning, fileName diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 index 350fe26e6..b68695295 100644 --- a/Tests/Rules/AvoidUsingArrayList.tests.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -12,29 +12,34 @@ BeforeAll { } Describe "AvoidArrayList" { + + BeforeDiscovery { + $violationFileName = "$PSScriptRoot\AvoidUsingArrayList.ps1" + $violationExtents = [Parser]::ParseFile($violationFileName, [ref] $null, [ref] $null).FindAll({ + $Args[0] -is [AssignmentStatementAst] -and + $Args[0].Left.Extent.Text -eq '$List' + }, $false).Right.Extent + } + Context "When there are violations" { BeforeAll { $violationFileName = "$PSScriptRoot\AvoidUsingArrayList.ps1" $violations = Invoke-ScriptAnalyzer $violationFileName | Where-Object RuleName -eq $ruleName - - $violationExtents = @{} - [Parser]::ParseFile($violationFileName, [ref] $null, [ref] $null).FindAll({ - $Args[0] -is [AssignmentStatementAst] -and - $Args[0].Left.Extent.Text -eq '$List' - }, $false).Right.Extent.foreach{ $violationExtents[$_.StartLineNumber] = $_ } + $violationLines = @{} + foreach ($violation in $violations) { $violationLines[$violation.Line] = $violation } } It "Should return 12 violations" { $violations.Count | Should -Be 12 } - It "Should return a correct violation record" -ForEach $violations { - $expectedViolation = $violationExtents[$_.Line] - $_.Extent.Text | Should -Be $expectedViolation.Text - $_.Message | Should -Be ($ruleMessage -f $expectedViolation.Text) - $_.Severity | Should -Be Warning - $_.ScriptName | Should -Be AvoidUsingArrayList.ps1 + It "Each violation should contain" -ForEach $violationExtents { + $violation = $violationLines[$_.StartLineNumber] + $violation.Extent.Text | Should -Be $_.Text + $violation.Message | Should -Be ($ruleMessage -f $_.Text) + $violation.Severity | Should -Be Warning + $violation.ScriptName | Should -Be AvoidUsingArrayList.ps1 } } From 8aefe4c0189bd0cd0f2a0e0bb3ff9ee62bc2338b Mon Sep 17 00:00:00 2001 From: iRon7 Date: Thu, 16 Apr 2026 19:45:25 +0200 Subject: [PATCH 14/15] fixed and tested empty (dynamic) BoundParameter --- Rules/AvoidUsingArrayList.cs | 2 +- Tests/Rules/AvoidUsingArrayList.tests.ps1 | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidUsingArrayList.cs b/Rules/AvoidUsingArrayList.cs index 92784233a..092f14b14 100644 --- a/Rules/AvoidUsingArrayList.cs +++ b/Rules/AvoidUsingArrayList.cs @@ -103,7 +103,7 @@ testAst is CommandAst cmdAst && // Check for -TypeName parameter if ( bindingResult.BoundParameters.ContainsKey("TypeName") && - arrayListName != null && + bindingResult.BoundParameters["TypeName"] != null && arrayListName.IsMatch(bindingResult.BoundParameters["TypeName"].ConstantValue as string) ) { diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 index b68695295..ad4e0f8c6 100644 --- a/Tests/Rules/AvoidUsingArrayList.tests.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -41,6 +41,19 @@ Describe "AvoidArrayList" { $violation.Severity | Should -Be Warning $violation.ScriptName | Should -Be AvoidUsingArrayList.ps1 } + + + It "Dynamic types shouldn't error" { + # but aren't covered by the rule + + $scriptDefinition = { + $type = "System.Collections.ArrayList" + New-Object -TypeName '$type' + }.ToString() + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } } Context "When there are no violations" { From ad49041630c6dcda86781285e009b8fb237b0a66 Mon Sep 17 00:00:00 2001 From: iRon7 Date: Fri, 17 Apr 2026 14:18:48 +0200 Subject: [PATCH 15/15] Robuster Pester tests --- Rules/Strings.resx | 2 +- Tests/Rules/AvoidUsingArrayList.ps1 | 19 -- Tests/Rules/AvoidUsingArrayList.tests.ps1 | 213 +++++++++++++++--- .../Rules/AvoidUsingArrayListNoViolations.ps1 | 19 -- 4 files changed, 187 insertions(+), 66 deletions(-) delete mode 100644 Tests/Rules/AvoidUsingArrayList.ps1 delete mode 100644 Tests/Rules/AvoidUsingArrayListNoViolations.ps1 diff --git a/Rules/Strings.resx b/Rules/Strings.resx index f73b68d1e..ebbf8fe80 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -933,7 +933,7 @@ Line ends with a semicolon - + Avoid using the ArrayList class diff --git a/Tests/Rules/AvoidUsingArrayList.ps1 b/Tests/Rules/AvoidUsingArrayList.ps1 deleted file mode 100644 index 2430af6ed..000000000 --- a/Tests/Rules/AvoidUsingArrayList.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -using namespace system.collections - -# Using New-Object -$List = New-Object ArrayList -$List = New-Object 'ArrayList' -$List = New-Object "ArrayList" -$List = New-Object -Type ArrayList -$List = New-Object -TypeName ArrayLIST -$List = New-Object Collections.ArrayList -$List = New-Object System.Collections.ArrayList - -# Using type initializer -$List = [ArrayList](1,2,3) -$List = [ArrayLIST]@(1,2,3) -$List = [ArrayList]::new() -$List = [Collections.ArrayList]::New() -$List = [System.Collections.ArrayList]::new() - -1..3 | ForEach-Object { $null = $List.Add($_) } diff --git a/Tests/Rules/AvoidUsingArrayList.tests.ps1 b/Tests/Rules/AvoidUsingArrayList.tests.ps1 index ad4e0f8c6..46c7919b6 100644 --- a/Tests/Rules/AvoidUsingArrayList.tests.ps1 +++ b/Tests/Rules/AvoidUsingArrayList.tests.ps1 @@ -13,52 +13,211 @@ BeforeAll { Describe "AvoidArrayList" { - BeforeDiscovery { - $violationFileName = "$PSScriptRoot\AvoidUsingArrayList.ps1" - $violationExtents = [Parser]::ParseFile($violationFileName, [ref] $null, [ref] $null).FindAll({ - $Args[0] -is [AssignmentStatementAst] -and - $Args[0].Left.Extent.Text -eq '$List' - }, $false).Right.Extent - } - Context "When there are violations" { BeforeAll { - $violationFileName = "$PSScriptRoot\AvoidUsingArrayList.ps1" - $violations = Invoke-ScriptAnalyzer $violationFileName | Where-Object RuleName -eq $ruleName - $violationLines = @{} - foreach ($violation in $violations) { $violationLines[$violation.Line] = $violation } + $usingCollections = 'using namespace system.collections' + [Environment]::NewLine } - It "Should return 12 violations" { - $violations.Count | Should -Be 12 + It "Unquoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object ArrayList}) } - It "Each violation should contain" -ForEach $violationExtents { - $violation = $violationLines[$_.StartLineNumber] - $violation.Extent.Text | Should -Be $_.Text - $violation.Message | Should -Be ($ruleMessage -f $_.Text) - $violation.Severity | Should -Be Warning - $violation.ScriptName | Should -Be AvoidUsingArrayList.ps1 + It "Single quoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object 'ArrayList' + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object 'ArrayList'}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object 'ArrayList'}) } + It "Double quoted New-Object type" { + $scriptDefinition = $usingCollections + { + $List = New-Object "ArrayList" + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object "ArrayList"}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object "ArrayList"}) + } - It "Dynamic types shouldn't error" { - # but aren't covered by the rule + It "New-Object with full parameter name" { + $scriptDefinition = $usingCollections + { + $List = New-Object -TypeName ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -TypeName ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -TypeName ArrayList}) + } + + It "New-Object with abbreviated parameter name and odd casing" { + $scriptDefinition = $usingCollections + { + $List = New-Object -Type ArrayLIST + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -Type ArrayLIST}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -Type ArrayLIST}) + } + It "New-Object with full type name" { + $scriptDefinition = $usingCollections + { + $List = New-Object -TypeName System.Collections.ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object -TypeName System.Collections.ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object -TypeName System.Collections.ArrayList}) + } + + It "New-Object with semi full type name and odd casing" { + $scriptDefinition = $usingCollections + { + $List = New-Object COLLECTIONS.ArrayList + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {New-Object COLLECTIONS.ArrayList}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {New-Object COLLECTIONS.ArrayList}) + } + + It "Type initializer with 3 parameters" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList](1,2,3) + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList](1,2,3)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList](1,2,3)}) + } + + It "Type initializer with array parameters" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList]@(1,2,3) + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList]@(1,2,3)}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList]@(1,2,3)}) + } + + It "Type initializer with new constructor" { + $scriptDefinition = $usingCollections + { + $List = [ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[ArrayList]::new()}) + } + + It "Full type name initializer with new constructor" { + $scriptDefinition = $usingCollections + { + $List = [System.Collections.ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[System.Collections.ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[System.Collections.ArrayList]::new()}) + } + + It "Semi full type name initializer with new constructor and odd casing" { + $scriptDefinition = $usingCollections + { + $List = [COLLECTIONS.ArrayList]::new() + 1..3 | ForEach-Object { $null = $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations.Count | Should -Be 1 + $violations.Severity | Should -Be Warning + $violations.Extent.Text | Should -Be {[COLLECTIONS.ArrayList]::new()}.ToString() + $violations.Message | Should -Be ($ruleMessage -f {[COLLECTIONS.ArrayList]::new()}) + } + } + + Context "When there are no violations" { + + BeforeAll { + $usingGeneric = 'using namespace System.Collections.Generic' + [Environment]::NewLine + } + + It "New-Object List[Object]" { $scriptDefinition = { - $type = "System.Collections.ArrayList" - New-Object -TypeName '$type' + $List = New-Object List[Object] + 1..3 | ForEach-Object { $List.Add($_) } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + + It "[List[Object]]::new()" { + $scriptDefinition = { + $List = [List[Object]]::new() + 1..3 | ForEach-Object { $List.Add($_) } }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + It "Using the pipeline" { + $scriptDefinition = { + $List = 1..3 | ForEach-Object { $_ } + }.ToString() + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) + $violations | Should -BeNullOrEmpty + } + + It "Out of the namespace scope" { + $scriptDefinition = $usingGeneric + { + $List = New-Object ArrayList + $List = [ArrayList](1,2,3) + $List = [ArrayList]@(1,2,3) + $List = [ArrayList]::new() + }.ToString() $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) $violations | Should -BeNullOrEmpty } } - Context "When there are no violations" { - It "returns no violations" { - $noViolations.Count | Should -Be 0 + Context "Test for potential errors" { + + It "Dynamic types shouldn't error" { + $scriptDefinition = { + $type = "System.Collections.ArrayList" + New-Object -TypeName '$type' + }.ToString() + + $analyzer = { Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName) } + $analyzer | Should -Not -Throw # but won't violate either (too complex to cover) } } } diff --git a/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 b/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 deleted file mode 100644 index 469d8214a..000000000 --- a/Tests/Rules/AvoidUsingArrayListNoViolations.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -using namespace System.Collections.Generic - -# Using a generic List -$List = New-Object List[Object] -1..3 | ForEach-Object { $List.Add($_) } # This will not return anything - -$List = [List[Object]]::new() -1..3 | ForEach-Object { $List.Add($_) } # This will not return anything - -# Creating a fixed array by using the PowerShell pipeline -$List = 1..3 | ForEach-Object { $_ } - -# This should not violate because there isn't a -# `using namespace System.Collections` directive -# and ArrayList could belong to another namespace -$List = New-Object ArrayList -$List = [ArrayList](1,2,3) -$List = [ArrayList]@(1,2,3) -$List = [ArrayList]::new() \ No newline at end of file