Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Indented.ScriptAnalyzerRules/Indented.ScriptAnalyzerRules.psd1
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ function Resolve-ParameterSet {

$errorRecord = [ErrorRecord]::new(
[InvalidOperationException]::new(
('{0}: Ambiguous parameter set: {1}' -f
$CommandInfo.Name,
($candidateSets.Name -join ', ')
(
'{0}: Ambiguous parameter set: {1}' -f @(
$CommandInfo.Name
$candidateSets.Name -join ', '
)
)
),
'AmbiguousParameterSet',
Expand Down
151 changes: 151 additions & 0 deletions Indented.ScriptAnalyzerRules/public/rules/AvoidOutOfScopeVariables.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
using namespace System.Management.Automation.Language
using namespace System.Collections.Generic

function AvoidOutOfScopeVariables {
<#
.SYNOPSIS
AvoidOutOfScopeVariables

.DESCRIPTION
Functions should not use out of scope variables unless a scope modifier is explicitly used to indicate source scope.
#>

[CmdletBinding()]
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])]
param (
[FunctionDefinitionAst]$ast
)

# Special variables:
# [PowerShell].Assembly.GetType('System.Management.Automation.SpecialVariables').GetFields('Static,NonPublic') |
# Where-Object FieldType -eq ([string]) |
# ForEach-Object GetValue($null)
$specialVariables = [HashSet[string]]::new([IEqualityComparer[string]][StringComparer]::OrginalIgnoreCase)
$whitelist = @(
'_'
'?'
'^'
'$'
'args'
'ConfirmPreference'
'CurrentlyExecutingCommand'
'DebugPreference'
'EnabledExperimentalFeatures'
'env:PATHEXT'
'error'
'ErrorActionPreference'
'ErrorView'
'ExecutionContext'
'false'
'foreach'
'HOME'
'Host'
'InformationPreference'
'input'
'IsCoreCLR'
'IsLinux'
'IsMacOS'
'IsWindows'
'LASTEXITCODE'
'LogCommandHealthEvent'
'LogCommandLifecycleEvent'
'LogEngineHealthEvent'
'LogEngineLifecycleEvent'
'LogProviderHealthEvent'
'LogProviderLifecycleEvent'
'LogSettingsEvent'
'Matches'
'MaximumHistoryCount'
'MyInvocation'
'NestedPromptLevel'
'null'
'OFS'
'OutputEncoding'
'PID'
'ProgressPreference'
'PSBoundParameters'
'PSCmdlet'
'PSCommandPath'
'PSCulture'
'PSDebugContext'
'PSDefaultParameterValues'
'PSEdition'
'PSEmailServer'
'PSHOME'
'PSItem'
'PSLogUserData'
'PSModuleAutoLoadingPreference'
'PSNativeCommandArgumentPassing'
'PSScriptRoot'
'PSSenderInfo'
'PSSessionApplicationName'
'PSSessionConfigurationName'
'PSStyle'
'PSUICulture'
'PSVersionTable'
'PWD'
'ShellId'
'StackTrace'
'switch'
'this'
'true'
'VerboseHelpErrors'
'VerbosePreference'
'WarningPreference'
'WhatIfPreference'
)
foreach ($name in $whitelist) {
$null = $specialVariables.Add($name)
}

$declaredVariables = @{}
$ast.Body.FindAll(
{
param (
$ast
)

$ast -is [AssignmentStatementAst] -and
$ast.Left.VariablePath.IsUnqualified
},
$true
) | ForEach-Object {
$userPath = $_.Left.VariablePath.UserPath
if ($declaredVariables.Contains($userPath)) {
if ($_.Extent.StartOffset -lt $declaredVariables[$userPath]) {
$declaredVariables[$userPath] = $_.Extent.StartOffset
}
} else {
$declaredVariables[$userPath] = $_.Extent.StartOffset
}
}

$ast.Body.FindAll(
{
param (
$ast
)

$ast -is [VariableExpressionAst] -and
$ast.VariablePath.IsUnqualified -and
$ast.Parent -isnot [ForEachStatementAst] -and
-not $specialVariables.Contains($ast.VariablePath.UserPath) -and
(
(
$declaredVariables.Contains($ast.VariablePath.UserPath) -and
$ast.Extent.StartOffset -lt $declaredVariables[$ast.VariablePath.UserPath]
) -or
-not $declaredVariables.Contains($ast.VariablePath.UserPath)
)
},
$true
) | ForEach-Object {
[DiagnosticRecord]@{
Message = 'The function {0} contains an out of scope variable: ${1}' -f $ast.Name, $_.VariablePath.UserPath
Extent = $_.Extent
RuleName = $myinvocation.MyCommand.Name
Severity = 'Warning'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Describe AvoidOutOfScopeVariables {
BeforeAll {
$ruleName = @{ RuleName = 'AvoidOutOfScopeVariables' }
}

It 'Triggers when <Why> is used in "<Script>"' -TestCases @(
@{
Why = 'a script uses a variable from a parent scope'
Script = {
function testFunction {
if ($variable) { }
}
}
}
@{
Why = 'a variable is used before it is declared'
Script = {
function testFunction {
if ($variable) { }
$variable = $true
}
}
}
) {
Invoke-CustomScriptAnalyzerRule -ScriptBlock $script @ruleName | Should -Not -BeNullOrEmpty
}

It 'Does not trigger when <Why> is used in "<Script>"' -TestCases @(
@{
Why = 'the variable is assigned prior to use'
Script = {
function testFunction {
$variable = $true
if ($variable) { }
}
}
}
@{
Why = 'a built-in variable'
Script = {
function testFunction {
if ($PSVersionTable.PSVersion) { }
}
}
}
@{
Why = 'an explicit variable scope'
Script = {
function testFunction {
if ($Script:variable) { }
}
}
}
@{
Why = 'a loop variable is used'
Script = {
function testFunction {
$array = 1..5
foreach ($value in $array) {

}
}
}
}
) {
Invoke-CustomScriptAnalyzerRule -ScriptBlock $script @ruleName | Should -BeNullOrEmpty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Describe AvoidThrowOutsideOfTry {
It 'Triggers when throw is used outside of a try block in "<Script>"' -TestCases @(
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -16,7 +16,7 @@ Describe AvoidThrowOutsideOfTry {
}
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -31,7 +31,7 @@ Describe AvoidThrowOutsideOfTry {
}
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -52,7 +52,7 @@ Describe AvoidThrowOutsideOfTry {
It 'Does not trigger when used inside of a try block in "<Script>"' -TestCases @(
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -66,7 +66,7 @@ Describe AvoidThrowOutsideOfTry {
}
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -82,7 +82,7 @@ Describe AvoidThrowOutsideOfTry {
}
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand All @@ -98,7 +98,7 @@ Describe AvoidThrowOutsideOfTry {
}
@{
Script = {
function name {
function testFunction {
[CmdletBinding()]
param ( )

Expand Down
Loading