Skip to content

Commit a4a4a93

Browse files
committed
Detect and flag [PSCustomObject]@{} in CLM scripts
1 parent 17f1a24 commit a4a4a93

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

Rules/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,4 +1284,7 @@
12841284
<data name="UseConstrainedLanguageModeScriptModuleError" xml:space="preserve">
12851285
<value>Module manifest field '{0}' contains script file '{1}' (.ps1). Use a module file (.psm1) or a binary module (.dll) instead for Constrained Language Mode compatibility.</value>
12861286
</data>
1287+
<data name="UseConstrainedLanguageModePSCustomObjectError" xml:space="preserve">
1288+
<value>[PSCustomObject]@{{}} syntax is not permitted in Constrained Language Mode. Use New-Object PSObject -Property @{{}} or plain hashtables instead.</value>
1289+
</data>
12871290
</root>

Rules/UseConstrainedLanguageMode.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,25 @@ testAst is TypeConstraintAst typeConstraint &&
442442
foreach (ConvertExpressionAst convertExpr in convertExpressions)
443443
{
444444
var typeName = convertExpr.Type.TypeName.FullName;
445+
446+
// Special case: [PSCustomObject]@{} is not allowed in CLM
447+
// Even though PSCustomObject is an allowed type for parameters,
448+
// the type cast syntax with hashtable literal is blocked in CLM
449+
if (typeName.Equals("PSCustomObject", StringComparison.OrdinalIgnoreCase) &&
450+
convertExpr.Child is HashtableAst)
451+
{
452+
diagnosticRecords.Add(
453+
new DiagnosticRecord(
454+
String.Format(CultureInfo.CurrentCulture,
455+
Strings.UseConstrainedLanguageModePSCustomObjectError),
456+
convertExpr.Extent,
457+
GetName(),
458+
GetDiagnosticSeverity(),
459+
fileName
460+
));
461+
continue; // Already flagged, skip general type check
462+
}
463+
445464
if (!IsTypeAllowed(typeName))
446465
{
447466
diagnosticRecords.Add(

Tests/Rules/UseConstrainedLanguageMode.tests.ps1

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ Add-Type -TypeDefinition @"
7676
}
7777

7878
It "Should flag New-Object with disallowed TypeName" {
79-
$def = 'New-Object -TypeName System.Net.WebClient'
79+
$def = 'New-Object -TypeName System.IO.File'
8080
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
8181
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
8282
$matchingViolations.Count | Should -Be 1
83-
$matchingViolations[0].Message | Should -BeLike "*System.Net.WebClient*not permitted*"
83+
$matchingViolations[0].Message | Should -BeLike "*System.IO.File*not permitted*"
8484
}
8585
}
8686

@@ -172,11 +172,11 @@ enum MyEnum {
172172

173173
Context "When type expressions are used" {
174174
It "Should flag static type reference with new()" {
175-
$def = '$instance = [System.Net.WebClient]::new()'
175+
$def = '$instance = [System.IO.Directory]::new()'
176176
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
177177
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
178178
$matchingViolations.Count | Should -BeGreaterThan 0
179-
$matchingViolations[0].Message | Should -BeLike "*System.Net.WebClient*"
179+
$matchingViolations[0].Message | Should -BeLike "*System.IO.Directory*"
180180
}
181181

182182
It "Should flag static method call on disallowed type" {
@@ -388,9 +388,9 @@ enum MyEnum {
388388
It "Should flag multiple type issues in same script" {
389389
$def = @'
390390
function Test {
391-
param([System.Net.WebClient]$Client)
392-
[System.Net.Sockets.TcpClient]$tcp = $null
393-
$web = [System.Net.WebClient]::new()
391+
param([System.IO.File]$FileHelper)
392+
[System.IO.Directory]$dirHelper = $null
393+
$pathHelper = [System.IO.Path]::new()
394394
}
395395
'@
396396
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
@@ -401,41 +401,89 @@ function Test {
401401
}
402402
}
403403

404+
Context "When PSCustomObject type cast is used" {
405+
It "Should flag [PSCustomObject]@{} syntax" {
406+
$def = '$obj = [PSCustomObject]@{ Name = "Test"; Value = 42 }'
407+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
408+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
409+
$matchingViolations.Count | Should -BeGreaterThan 0
410+
$matchingViolations[0].Message | Should -BeLike "*PSCustomObject*"
411+
}
412+
413+
It "Should flag multiple [PSCustomObject]@{} instances" {
414+
$def = @'
415+
$obj1 = [PSCustomObject]@{ Name = "Test1" }
416+
$obj2 = [PSCustomObject]@{ Name = "Test2" }
417+
'@
418+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
419+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
420+
$matchingViolations.Count | Should -Be 2
421+
}
422+
423+
It "Should NOT flag PSCustomObject as parameter type" {
424+
$def = 'function Test { param([PSCustomObject]$InputObject) }'
425+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
426+
$violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty
427+
}
428+
429+
It "Should NOT flag New-Object PSObject" {
430+
$def = '$obj = New-Object PSObject -Property @{ Name = "Test" }'
431+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
432+
$violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty
433+
}
434+
435+
It "Should NOT flag plain hashtables" {
436+
$def = '$obj = @{ Name = "Test"; Value = 42 }'
437+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
438+
$violations | Where-Object { $_.RuleName -eq $violationName } | Should -BeNullOrEmpty
439+
}
440+
441+
It "Should NOT flag [PSCustomObject] with variable (not hashtable literal)" {
442+
$def = '$hash = @{}; $obj = [PSCustomObject]$hash'
443+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
444+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
445+
# This is a type cast but not the @{} literal pattern
446+
# Since PSCustomObject is in allowed list, this won't be flagged
447+
$matchingViolations | Should -BeNullOrEmpty
448+
}
449+
450+
}
451+
404452
Context "When instance methods are invoked on disallowed types" {
405453
It "Should flag method invocation on parameter with disallowed type constraint" {
406454
$def = @'
407-
function Download-File {
408-
param([System.Net.WebClient]$Client, [string]$Url)
409-
$Client.DownloadString($Url)
455+
function Read-File {
456+
param([System.IO.File]$FileHelper, [string]$Path)
457+
$FileHelper.ReadAllText($Path)
410458
}
411459
'@
412460
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
413461
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
414462
# Should flag both the type constraint AND the member access
415463
$matchingViolations.Count | Should -BeGreaterThan 1
416-
# At least one violation should mention DownloadString
417-
($matchingViolations.Message | Where-Object { $_ -like "*DownloadString*" }).Count | Should -BeGreaterThan 0
464+
# At least one violation should mention ReadAllText
465+
($matchingViolations.Message | Where-Object { $_ -like "*ReadAllText*" }).Count | Should -BeGreaterThan 0
418466
}
419467

420468
It "Should flag property access on variable with disallowed type constraint" {
421469
$def = @'
422470
function Test {
423-
param([System.Net.WebClient]$Client)
424-
$baseAddr = $Client.BaseAddress
471+
param([System.IO.FileInfo]$FileHelper)
472+
$fullPath = $FileHelper.FullName
425473
}
426474
'@
427475
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
428476
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
429477
# Should flag both the type constraint AND the member access
430478
$matchingViolations.Count | Should -BeGreaterThan 1
431-
# At least one violation should mention BaseAddress
432-
($matchingViolations.Message | Where-Object { $_ -like "*BaseAddress*" }).Count | Should -BeGreaterThan 0
479+
# At least one violation should mention FullName
480+
($matchingViolations.Message | Where-Object { $_ -like "*FullName*" }).Count | Should -BeGreaterThan 0
433481
}
434482

435483
It "Should flag method invocation on typed variable assignment" {
436484
$def = @'
437-
[System.Net.WebClient]$client = $null
438-
$result = $client.DownloadString("http://example.com")
485+
[System.IO.File]$fileHelper = $null
486+
$result = $fileHelper.ReadAllText("C:\test.txt")
439487
'@
440488
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
441489
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }

0 commit comments

Comments
 (0)