Skip to content
Merged
44 changes: 39 additions & 5 deletions Rules/AvoidAssignmentToAutomaticVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
public class AvoidAssignmentToAutomaticVariable : IScriptRule
{
private static readonly IList<string> _readOnlyAutomaticVariables = new List<string>()
{
// Attempting to assign to any of those read-only variable would result in an error at runtime
"?", "true", "false", "Host", "PSCulture", "Error", "ExecutionContext", "Home", "PID", "PSEdition", "PSHome", "PSUICulture", "PSVersionTable", "ShellId"
};
{
// Attempting to assign to any of those read-only variable would result in an error at runtime
"?", "true", "false", "Host", "PSCulture", "Error", "ExecutionContext", "Home", "PID", "PSEdition", "PSHome", "PSUICulture", "PSVersionTable", "ShellId"
};

private static readonly IList<string> _readOnlyAutomaticVariablesIntroducedInVersion6_0 = new List<string>()
{
// Attempting to assign to any of those read-only variable will result in an error at runtime
"IsCoreCLR", "IsLinux", "IsMacOS", "IsWindows"
};

/// <summary>
/// Checks for assignment to automatic variables.
Expand All @@ -47,6 +53,13 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName),
variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName);
}

if (_readOnlyAutomaticVariablesIntroducedInVersion6_0.Contains(variableName, StringComparer.OrdinalIgnoreCase))
{
var severity = IsPowerShellVersion6OrGreater() ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error, variableName),
variableExpressionAst.Extent, GetName(), severity, fileName);
}
}

IEnumerable<Ast> parameterAsts = ast.FindAll(testAst => testAst is ParameterAst, searchNestedScriptBlocks: true);
Expand All @@ -55,12 +68,33 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
var variableExpressionAst = parameterAst.Find(testAst => testAst is VariableExpressionAst, searchNestedScriptBlocks: false) as VariableExpressionAst;
var variableName = variableExpressionAst.VariablePath.UserPath;
// also check the parent to exclude parameter attributes such as '[Parameter(Mandatory=$true)]' where the read-only variable $true appears.
if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase) && !(variableExpressionAst.Parent is NamedAttributeArgumentAst))
if (variableExpressionAst.Parent is NamedAttributeArgumentAst)
{
continue;
}

if (_readOnlyAutomaticVariables.Contains(variableName, StringComparer.OrdinalIgnoreCase))
{
yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableError, variableName),
variableExpressionAst.Extent, GetName(), DiagnosticSeverity.Error, fileName);
}
if (_readOnlyAutomaticVariablesIntroducedInVersion6_0.Contains(variableName, StringComparer.OrdinalIgnoreCase))
{
var severity = IsPowerShellVersion6OrGreater() ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
yield return new DiagnosticRecord(DiagnosticRecordHelper.FormatError(Strings.AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error, variableName),
variableExpressionAst.Extent, GetName(), severity, fileName);
}
}
}

private bool IsPowerShellVersion6OrGreater()
{
var psVersion = Helper.Instance.GetPSVersion();
if (psVersion.Major >= 6)
{
return true;
}
return false;
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions Rules/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Rules/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,9 @@
<data name="AvoidAssignmentToAutomaticVariableName" xml:space="preserve">
<value>AvoidAssignmentToAutomaticVariable</value>
</data>
<data name="AvoidAssignmentToReadOnlyAutomaticVariableIntroducedInPowerShell6_0Error" xml:space="preserve">
<value>Starting from PowerShell 6.0, the Variable '{0}' cannot be assigned any more since it is a readonly automatic variable that is built into PowerShell, please use a different name.</value>
</data>
<data name="AvoidUsingCmdletAliasesMissingGetPrefixError" xml:space="preserve">
<value>'{0}' is implicitly aliasing '{1}' because it is missing the 'Get-' prefix. This can introduce possible problems and make scripts hard to maintain. Please consider changing command to its full name.</value>
</data>
Expand Down
80 changes: 50 additions & 30 deletions Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,75 @@
Describe "AvoidAssignmentToAutomaticVariables" {
Context "ReadOnly Variables" {

$readOnlyVariableSeverity = "Error"
$excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Warning'
if ($PSVersionTable.PSVersion.Major -ge 6)
{
$excpectedSeverityForAutomaticVariablesInPowerShell6 = 'Error'
}

$testCases_ReadOnlyVariables = @(
@{ VariableName = '?' }
@{ VariableName = 'Error' }
@{ VariableName = 'ExecutionContext' }
@{ VariableName = 'false' }
@{ VariableName = 'Home' }
@{ VariableName = 'Host' }
@{ VariableName = 'PID' }
@{ VariableName = 'PSCulture' }
@{ VariableName = 'PSEdition' }
@{ VariableName = 'PSHome' }
@{ VariableName = 'PSUICulture' }
@{ VariableName = 'PSVersionTable' }
@{ VariableName = 'ShellId' }
@{ VariableName = 'true' }
@{ VariableName = '?'; ExpectedSeverity = 'Error'; }
@{ VariableName = 'Error' ; ExpectedSeverity = 'Error' }
@{ VariableName = 'ExecutionContext'; ExpectedSeverity = 'Error' }
@{ VariableName = 'false'; ExpectedSeverity = 'Error' }
@{ VariableName = 'Home'; ExpectedSeverity = 'Error' }
@{ VariableName = 'Host'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PID'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PSCulture'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PSEdition'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PSHome'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PSUICulture'; ExpectedSeverity = 'Error' }
@{ VariableName = 'PSVersionTable'; ExpectedSeverity = 'Error' }
@{ VariableName = 'ShellId'; ExpectedSeverity = 'Error' }
@{ VariableName = 'true'; ExpectedSeverity = 'Error' }
# Variables introuced only in PowerShell 6.0 have a Severity of Warning only
@{ VariableName = 'IsCoreCLR'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true }
@{ VariableName = 'IsLinux'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true }
@{ VariableName = 'IsMacOS'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true }
@{ VariableName = 'IsWindows'; ExpectedSeverity = $excpectedSeverityForAutomaticVariablesInPowerShell6; OnlyPresentInCoreClr = $true }
)

It "Variable '<VariableName>' produces warning of severity error" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName)
It "Variable <VariableName> produces warning of Severity <ExpectedSeverity>" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName, $ExpectedSeverity)

$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "`$${VariableName} = 'foo'" | Where-Object { $_.RuleName -eq $ruleName }
$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "`$${VariableName} = 'foo'" -ExcludeRule PSUseDeclaredVarsMoreThanAssignments
$warnings.Count | Should -Be 1
$warnings.Severity | Should -Be $readOnlyVariableSeverity
$warnings.Severity | Should -Be $ExpectedSeverity
$warnings.RuleName | Should -Be $ruleName
}

It "Using Variable '<VariableName>' as parameter name produces warning of severity error" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName)
It "Using Variable <VariableName> as parameter name produces warning of Severity error" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName, $ExpectedSeverity)

[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo{Param(`$$VariableName)}" | Where-Object {$_.RuleName -eq $ruleName }
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo{Param(`$$VariableName)}"
$warnings.Count | Should -Be 1
$warnings.Severity | Should -Be $readOnlyVariableSeverity
$warnings.Severity | Should -Be $ExpectedSeverity
$warnings.RuleName | Should -Be $ruleName
}

It "Using Variable '<VariableName>' as parameter name in param block produces warning of severity error" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName)
It "Using Variable <VariableName> as parameter name in param block produces warning of Severity error" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName, $ExpectedSeverity)

[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo(`$$VariableName){}" | Where-Object {$_.RuleName -eq $ruleName }
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo(`$$VariableName){}"
$warnings.Count | Should -Be 1
$warnings.Severity | Should -Be $readOnlyVariableSeverity
$warnings.Severity | Should -Be $ExpectedSeverity
$warnings.RuleName | Should -Be $ruleName
}

It "Does not flag parameter attributes" {
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'function foo{Param([Parameter(Mandatory=$true)]$param1)}' | Where-Object { $_.RuleName -eq $ruleName }
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'function foo{Param([Parameter(Mandatory=$true)]$param1)}'
$warnings.Count | Should -Be 0
}

It "Setting Variable '<VariableName>' throws exception to verify the variables is read-only" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName)
It "Setting Variable <VariableName> throws exception in applicable PowerShell version to verify the variables is read-only" -TestCases $testCases_ReadOnlyVariables {
param ($VariableName, $ExpectedSeverity, $OnlyPresentInCoreClr)

if ($OnlyPresentInCoreClr -and !$IsCoreCLR)
{
# In this special case we expect it to not throw
Set-Variable -Name $VariableName -Value 'foo'
continue
}

# Setting the $Error variable has the side effect of the ErrorVariable to contain only the exception message string, therefore exclude this case.
# For the library test in WMF 4, assigning a value $PSEdition does not seem to throw an error, therefore this special case is excluded as well.
Expand Down