Skip to content

Commit 3d3afc8

Browse files
committed
Add CLM rule ScriptsToProcess; Fix wildcard and dotsource bug
1 parent 650d050 commit 3d3afc8

File tree

4 files changed

+158
-28
lines changed

4 files changed

+158
-28
lines changed

Rules/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,9 @@
13111311
<data name="UseConstrainedLanguageModeScriptModuleError" xml:space="preserve">
13121312
<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>
13131313
</data>
1314+
<data name="UseConstrainedLanguageModeScriptsToProcessError" xml:space="preserve">
1315+
<value>Module manifest field 'ScriptsToProcess' contains script file '{0}' (.ps1). Scripts in ScriptsToProcess run in the caller's session state and are restricted in Constrained Language Mode. Consider moving this logic to module initialization code</value>
1316+
</data>
13141317
<data name="UseConstrainedLanguageModePSCustomObjectError" xml:space="preserve">
13151318
<value>[PSCustomObject]@{{}} syntax is not permitted in Constrained Language Mode. Use New-Object PSObject -Property @{{}} or plain hashtables instead.</value>
13161319
</data>

Rules/UseConstrainedLanguageMode.cs

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ private void CheckDotSourcing(Ast ast, string fileName, List<DiagnosticRecord> d
538538
// Check if the command extent starts with a dot followed by whitespace
539539
// This indicates dot-sourcing
540540
string extentText = cmdAst.Extent.Text.TrimStart();
541-
if (extentText.StartsWith(". ") || extentText.StartsWith(".\t"))
541+
if (extentText.StartsWith(".") && extentText.Length > 1 && char.IsWhiteSpace(extentText[1]))
542542
{
543543
diagnosticRecords.Add(
544544
new DiagnosticRecord(
@@ -800,8 +800,8 @@ private void CheckModuleManifest(Ast ast, string fileName, List<DiagnosticRecord
800800

801801
// Check for wildcard exports in FunctionsToExport, CmdletsToExport, AliasesToExport
802802
CheckWildcardExports(hashtableAst, fileName, diagnosticRecords);
803-
804-
// Check for .ps1 files in RootModule and NestedModules
803+
804+
// Check for .ps1 files in RootModule, NestedModules, and ScriptsToProcess
805805
CheckScriptModules(hashtableAst, fileName, diagnosticRecords);
806806
}
807807

@@ -810,8 +810,8 @@ private void CheckModuleManifest(Ast ast, string fileName, List<DiagnosticRecord
810810
/// </summary>
811811
private void CheckWildcardExports(HashtableAst hashtableAst, string fileName, List<DiagnosticRecord> diagnosticRecords)
812812
{
813-
//AliasesToExport and VariablesToExport can use wildcards in CLM, but it is not recommended for performance reasons. We will flag it as an informational message to encourage best practices.
814-
string[] exportFields = { "FunctionsToExport", "CmdletsToExport", "AliasesToExport", "VariablesToExport" };
813+
//AliasesToExport and VariablesToExport can use wildcards in CLM, but it is not recommended for performance reasons.
814+
string[] exportFields = { "FunctionsToExport", "CmdletsToExport"};
815815

816816
foreach (var kvp in hashtableAst.KeyValuePairs)
817817
{
@@ -868,9 +868,16 @@ private void CheckWildcardExports(HashtableAst hashtableAst, string fileName, Li
868868
}
869869
}
870870
}
871+
else if (expr is StringConstantExpressionAst strElement && strElement.Value == "*")
872+
{
873+
// Handle single-item array expressions like @('*')
874+
hasWildcard = true;
875+
wildcardExtent = strElement.Extent;
876+
break;
877+
}
871878
if (hasWildcard) break;
872879
}
873-
}
880+
}
874881
}
875882

876883
if (hasWildcard && wildcardExtent != null)
@@ -892,11 +899,11 @@ private void CheckWildcardExports(HashtableAst hashtableAst, string fileName, Li
892899
}
893900

894901
/// <summary>
895-
/// Checks for .ps1 files in RootModule and NestedModules which are not recommended for CLM.
902+
/// Checks for .ps1 files in RootModule, NestedModules, and ScriptsToProcess which are not recommended for CLM.
896903
/// </summary>
897904
private void CheckScriptModules(HashtableAst hashtableAst, string fileName, List<DiagnosticRecord> diagnosticRecords)
898905
{
899-
string[] moduleFields = { "RootModule", "ModuleToProcess", "NestedModules" };
906+
string[] moduleFields = { "RootModule", "NestedModules", "ScriptsToProcess" };
900907

901908
foreach (var kvp in hashtableAst.KeyValuePairs)
902909
{
@@ -928,6 +935,26 @@ private ExpressionAst GetExpressionFromStatement(StatementAst statement)
928935
return null;
929936
}
930937

938+
/// <summary>
939+
/// Helper method to get the appropriate error message for .ps1 file usage in module manifests.
940+
/// </summary>
941+
private string GetPs1FileErrorMessage(string fieldName, string scriptFileName)
942+
{
943+
if (fieldName.Equals("ScriptsToProcess", StringComparison.OrdinalIgnoreCase))
944+
{
945+
return String.Format(CultureInfo.CurrentCulture,
946+
Strings.UseConstrainedLanguageModeScriptsToProcessError,
947+
scriptFileName);
948+
}
949+
else
950+
{
951+
return String.Format(CultureInfo.CurrentCulture,
952+
Strings.UseConstrainedLanguageModeScriptModuleError,
953+
fieldName,
954+
scriptFileName);
955+
}
956+
}
957+
931958
/// <summary>
932959
/// Helper method to check if an expression contains .ps1 file references.
933960
/// </summary>
@@ -939,10 +966,7 @@ private void CheckForPs1Files(ExpressionAst valueAst, string fieldName, string f
939966
{
940967
diagnosticRecords.Add(
941968
new DiagnosticRecord(
942-
String.Format(CultureInfo.CurrentCulture,
943-
Strings.UseConstrainedLanguageModeScriptModuleError,
944-
fieldName,
945-
stringValue.Value),
969+
GetPs1FileErrorMessage(fieldName, stringValue.Value),
946970
stringValue.Extent,
947971
GetName(),
948972
GetDiagnosticSeverity(),
@@ -960,10 +984,7 @@ private void CheckForPs1Files(ExpressionAst valueAst, string fieldName, string f
960984
{
961985
diagnosticRecords.Add(
962986
new DiagnosticRecord(
963-
String.Format(CultureInfo.CurrentCulture,
964-
Strings.UseConstrainedLanguageModeScriptModuleError,
965-
fieldName,
966-
strElement.Value),
987+
GetPs1FileErrorMessage(fieldName, strElement.Value),
967988
strElement.Extent,
968989
GetName(),
969990
GetDiagnosticSeverity(),
@@ -990,10 +1011,7 @@ private void CheckForPs1Files(ExpressionAst valueAst, string fieldName, string f
9901011
{
9911012
diagnosticRecords.Add(
9921013
new DiagnosticRecord(
993-
String.Format(CultureInfo.CurrentCulture,
994-
Strings.UseConstrainedLanguageModeScriptModuleError,
995-
fieldName,
996-
strElement.Value),
1014+
GetPs1FileErrorMessage(fieldName, strElement.Value),
9971015
strElement.Extent,
9981016
GetName(),
9991017
GetDiagnosticSeverity(),
@@ -1002,6 +1020,19 @@ private void CheckForPs1Files(ExpressionAst valueAst, string fieldName, string f
10021020
}
10031021
}
10041022
}
1023+
else if (expr is StringConstantExpressionAst strElement &&
1024+
strElement.Value != null &&
1025+
strElement.Value.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
1026+
{
1027+
diagnosticRecords.Add(
1028+
new DiagnosticRecord(
1029+
GetPs1FileErrorMessage(fieldName, strElement.Value),
1030+
strElement.Extent,
1031+
GetName(),
1032+
GetDiagnosticSeverity(),
1033+
fileName
1034+
));
1035+
}
10051036
}
10061037
}
10071038
}

Tests/Rules/UseConstrainedLanguageMode.tests.ps1

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,36 @@ enum MyEnum {
235235
$matchingViolations[0].Message | Should -BeLike "*FunctionsToExport*wildcard*"
236236
}
237237

238-
It "Should flag wildcard in CmdletsToExport" {
239-
$manifestPath = Join-Path $tempPath "WildcardCmdlets.psd1"
238+
It "Should flag wildcard array in FunctionsToExport" {
239+
$manifestPath = Join-Path $tempPath "WildcardFunctions.psd1"
240240
$manifestContent = @'
241241
@{
242242
ModuleVersion = '1.0.0'
243243
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
244-
CmdletsToExport = '*'
244+
FunctionsToExport = @('*')
245245
}
246246
'@
247247
Set-Content -Path $manifestPath -Value $manifestContent
248248
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
249249
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
250250
$matchingViolations.Count | Should -BeGreaterThan 0
251-
$matchingViolations[0].Message | Should -BeLike "*CmdletsToExport*wildcard*"
251+
$matchingViolations[0].Message | Should -BeLike "*FunctionsToExport*wildcard*"
252252
}
253253

254-
It "Should flag wildcard in array of exports" {
255-
$manifestPath = Join-Path $tempPath "WildcardArray.psd1"
254+
It "Should flag wildcard in CmdletsToExport" {
255+
$manifestPath = Join-Path $tempPath "WildcardCmdlets.psd1"
256256
$manifestContent = @'
257257
@{
258258
ModuleVersion = '1.0.0'
259259
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
260-
AliasesToExport = @('Get-Foo', '*', 'Set-Bar')
260+
CmdletsToExport = '*'
261261
}
262262
'@
263263
Set-Content -Path $manifestPath -Value $manifestContent
264264
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
265265
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
266266
$matchingViolations.Count | Should -BeGreaterThan 0
267-
$matchingViolations[0].Message | Should -BeLike "*AliasesToExport*wildcard*"
267+
$matchingViolations[0].Message | Should -BeLike "*CmdletsToExport*wildcard*"
268268
}
269269

270270
It "Should NOT flag explicit list of exports" {
@@ -336,6 +336,73 @@ enum MyEnum {
336336
$scriptModuleViolations | Should -BeNullOrEmpty
337337
}
338338

339+
It "Should flag .ps1 file in ScriptsToProcess" {
340+
$manifestPath = Join-Path $tempPath "ScriptsToProcess.psd1"
341+
$manifestContent = @'
342+
@{
343+
ModuleVersion = '1.0.0'
344+
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
345+
ScriptsToProcess = @('Init.ps1', 'Setup.ps1')
346+
}
347+
'@
348+
Set-Content -Path $manifestPath -Value $manifestContent
349+
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
350+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
351+
$matchingViolations.Count | Should -BeGreaterThan 0
352+
$matchingViolations[0].Message | Should -BeLike "*ScriptsToProcess*Init.ps1*"
353+
}
354+
355+
It "Should use different error message for ScriptsToProcess" {
356+
$manifestPath = Join-Path $tempPath "ScriptsToProcessMessage.psd1"
357+
$manifestContent = @'
358+
@{
359+
ModuleVersion = '1.0.0'
360+
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
361+
ScriptsToProcess = 'Init.ps1'
362+
}
363+
'@
364+
Set-Content -Path $manifestPath -Value $manifestContent
365+
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
366+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
367+
$matchingViolations.Count | Should -Be 1
368+
# ScriptsToProcess should have a specific message about caller's session state
369+
$matchingViolations[0].Message | Should -BeLike "*caller*session state*"
370+
$matchingViolations[0].Message | Should -BeLike "*Init.ps1*"
371+
}
372+
373+
It "Should flag single-item array in ScriptsToProcess" {
374+
$manifestPath = Join-Path $tempPath "ScriptsToProcessArray.psd1"
375+
$manifestContent = @'
376+
@{
377+
ModuleVersion = '1.0.0'
378+
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
379+
ScriptsToProcess = @('Init.ps1')
380+
}
381+
'@
382+
Set-Content -Path $manifestPath -Value $manifestContent
383+
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
384+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
385+
$matchingViolations.Count | Should -Be 1
386+
$matchingViolations[0].Message | Should -BeLike "*ScriptsToProcess*Init.ps1*"
387+
}
388+
389+
It "Should NOT flag .psm1 files in ScriptsToProcess" {
390+
$manifestPath = Join-Path $tempPath "ScriptsToProcessPsm1.psd1"
391+
$manifestContent = @'
392+
@{
393+
ModuleVersion = '1.0.0'
394+
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
395+
ScriptsToProcess = @('Init.psm1')
396+
}
397+
'@
398+
Set-Content -Path $manifestPath -Value $manifestContent
399+
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
400+
$scriptViolations = $violations | Where-Object {
401+
$_.RuleName -eq $violationName -and $_.Message -like "*ScriptsToProcess*"
402+
}
403+
$scriptViolations | Should -BeNullOrEmpty
404+
}
405+
339406
It "Should flag both wildcard and .ps1 issues in same manifest" {
340407
$manifestPath = Join-Path $tempPath "MultipleIssues.psd1"
341408
$manifestContent = @'
@@ -353,6 +420,33 @@ enum MyEnum {
353420
# Should have at least 3 violations: RootModule .ps1, FunctionsToExport *, CmdletsToExport *
354421
$matchingViolations.Count | Should -BeGreaterOrEqual 3
355422
}
423+
424+
It "Should flag ScriptsToProcess and RootModule with different messages" {
425+
$manifestPath = Join-Path $tempPath "MixedScriptFields.psd1"
426+
$manifestContent = @'
427+
@{
428+
ModuleVersion = '1.0.0'
429+
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
430+
RootModule = 'MyModule.ps1'
431+
ScriptsToProcess = @('Init.ps1')
432+
}
433+
'@
434+
Set-Content -Path $manifestPath -Value $manifestContent
435+
$violations = Invoke-ScriptAnalyzer -Path $manifestPath -Settings $settings
436+
$matchingViolations = $violations | Where-Object { $_.RuleName -eq $violationName }
437+
$matchingViolations.Count | Should -Be 2
438+
439+
# Check that we have both types of messages
440+
$scriptsToProcessMsg = $matchingViolations | Where-Object { $_.Message -like "*caller*session state*" }
441+
$rootModuleMsg = $matchingViolations | Where-Object { $_.Message -like "*RootModule*" -and $_.Message -notlike "*caller*session state*" }
442+
443+
$scriptsToProcessMsg.Count | Should -Be 1
444+
$rootModuleMsg.Count | Should -Be 1
445+
446+
# Verify the specific field names are mentioned
447+
$scriptsToProcessMsg[0].Message | Should -BeLike "*Init.ps1*"
448+
$rootModuleMsg[0].Message | Should -BeLike "*MyModule.ps1*"
449+
}
356450
}
357451

358452
Context "Rule severity" {
@@ -628,6 +722,7 @@ class MyClass {
628722
$scriptPath = Join-Path $tempPath "SignedWithDotSource.ps1"
629723
$scriptContent = @'
630724
. .\Helper.ps1
725+
. .\U tility.ps1
631726
632727
# SIG # Begin signature block
633728
# MIIFFAYJKoZIhvcNAQcCoIIFBTCCBQECAQExCzAJ...

docs/Rules/UseConstrainedLanguageMode.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Use modules with Import-Module instead of dot-sourcing when possible.
137137

138138
- Replace wildcard exports (`*`) with explicit lists
139139
- Use `.psm1` or `.dll` instead of `.ps1` for RootModule/NestedModules
140+
- Don't use ScriptsToProcess as it loads in the caller's scope and will be blocked.
140141

141142
## Examples
142143

0 commit comments

Comments
 (0)