diff --git a/Indented.ScriptAnalyzerRules/Indented.ScriptAnalyzerRules.psd1 b/Indented.ScriptAnalyzerRules/Indented.ScriptAnalyzerRules.psd1 index 58a317b..0d47a17 100644 Binary files a/Indented.ScriptAnalyzerRules/Indented.ScriptAnalyzerRules.psd1 and b/Indented.ScriptAnalyzerRules/Indented.ScriptAnalyzerRules.psd1 differ diff --git a/Indented.ScriptAnalyzerRules/public/helper/Resolve-ParameterSet.ps1 b/Indented.ScriptAnalyzerRules/public/helper/Resolve-ParameterSet.ps1 index a913065..128410a 100644 --- a/Indented.ScriptAnalyzerRules/public/helper/Resolve-ParameterSet.ps1 +++ b/Indented.ScriptAnalyzerRules/public/helper/Resolve-ParameterSet.ps1 @@ -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', diff --git a/Indented.ScriptAnalyzerRules/public/rules/AvoidOutOfScopeVariables.ps1 b/Indented.ScriptAnalyzerRules/public/rules/AvoidOutOfScopeVariables.ps1 new file mode 100644 index 0000000..b550908 --- /dev/null +++ b/Indented.ScriptAnalyzerRules/public/rules/AvoidOutOfScopeVariables.ps1 @@ -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' + } + } +} diff --git a/Indented.ScriptAnalyzerRules/tests/unit/rules/AvoidOutOfScopeVariables.tests.ps1 b/Indented.ScriptAnalyzerRules/tests/unit/rules/AvoidOutOfScopeVariables.tests.ps1 new file mode 100644 index 0000000..bbc5ea1 --- /dev/null +++ b/Indented.ScriptAnalyzerRules/tests/unit/rules/AvoidOutOfScopeVariables.tests.ps1 @@ -0,0 +1,68 @@ +Describe AvoidOutOfScopeVariables { + BeforeAll { + $ruleName = @{ RuleName = 'AvoidOutOfScopeVariables' } + } + + It 'Triggers when is used in "