From 8b9f11586019b2868d427796581b0a25a9a43e08 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sun, 16 Feb 2020 16:36:59 +0100 Subject: [PATCH 01/55] Add tests for AvoidUnInitializedVarsInNewRunspaces --- ...dUnInitializedVarsInNewRunspaces.tests.ps1 | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 diff --git a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 new file mode 100644 index 000000000..39564de54 --- /dev/null +++ b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 @@ -0,0 +1,90 @@ +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$testRootDirectory = Split-Path -Parent $directory + +Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") + +$ruleName = "PSAvoidUnInitializedVarsInNewRunspaces" + +$settings = @{ + IncludeRules = @($ruleName) +} + +Describe "AvoidUnInitializedVarsInNewRunspaces" { + Context "Should detect something" { + $testCases = @( + @{ + Description = "Foreach-Object -Parallel with undeclared var" + ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel { $var } + }' + } + @{ + Description = "alias foreach -parallel with undeclared var" + ScriptBlock = '{ + 1..2 | ForEach -Parallel { $var } + }' + } + @{ + Description = "alias % -parallel with undeclared var" + ScriptBlock = '{ + 1..2 | % -Parallel { $var } + }' + } + @{ + Description = "abbreviated param Foreach-Object -pa with undeclared var" + ScriptBlock = '{ + 1..2 | foreach-object -pa { $var } + }' + } + @{ + Description = "Nested Foreach-Object -Parallel with undeclared var" + ScriptBlock = '{ + $myNestedScriptBlock = { + 1..2 | ForEach-Object -Parallel { $var } + } + }' + } + ) + + it "should emit for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 1 + } + } + + Context "Should not detect anything" { + $testCases = @( + @{ + Description = "Foreach-Object with uninitialized var inside" + ScriptBlock = '{ + 1..2 | ForEach-Object { $var } + }' + } + @{ + Description = "Foreach-Object -Parallel with uninitialized `$using: var" + ScriptBlock = '{ + 1..2 | foreach-object -Parallel { $using:var } + }' + } + @{ + Description = "Foreach-Object -Parallel with var assigned locally" + ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel { $var="somevalue" } + }' + } + @{ + Description = "Foreach-Object -Parallel with built-in var '`$Args' inside" + ScriptBlock = '{ + 1..2 | ForEach-Object { $Args[0] } -ArgumentList "a" -Parallel + }' + } + ) + + it "should not emit anything for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 0 + } + } +} \ No newline at end of file From 0ab5fbf96321947bff75d031bb15e830fd321a6d Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sun, 16 Feb 2020 16:37:59 +0100 Subject: [PATCH 02/55] Add strings for AvoidUnInitializedVarsInNewRunspaces --- Rules/Strings.Designer.cs | 27 + Rules/Strings.resx | 2257 +++++++++++++++++++------------------ 2 files changed, 1160 insertions(+), 1124 deletions(-) diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index c0cb02ba6..ab5676251 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -618,6 +618,33 @@ internal static string AvoidTrailingWhitespaceName { } } + /// + /// Looks up a localized string similar to Use $Using: directive in runspace scriptblocks. + /// + internal static string AvoidUnInitializedVarsInNewRunspacesCommonName { + get { + return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock.. + /// + internal static string AvoidUnInitializedVarsInNewRunspacesDescription { + get { + return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AvoidUnInitializedVarsInNewRunspaces. + /// + internal static string AvoidUnInitializedVarsInNewRunspacesName { + get { + return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Module Must Be Loadable. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index fc47fc6bd..70a7d3f93 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1,1125 +1,1134 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability. - - - Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. - - - Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks. - - - Avoid Using Empty Catch Block - - - The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please remove Invoke-Expression from script and find other options instead. - - - Avoid Using Invoke-Expression - - - Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. - - - Avoid Using Positional Parameters - - - Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment. - - - The cmdlet '{0}' does not have a help comment. - - - Basic Comment Help - - - Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices. - - - The cmdlet '{0}' uses an unapproved verb. - - - Cmdlet Verbs - - - Ensure declared variables are used elsewhere in the script and not just during assignment. - - - The variable '{0}' is assigned but never used. - - - Extra Variables - - - Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems. - - - Found global variable '{0}'. - - - No Global Variables - - - Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. - - - $null should be on the left side of equality comparisons. - - - Null Comparison - - - Checks that cmdlets and parameters have more than one character. - - - The cmdlet name '{0}' only has one character. - - - The cmdlet '{0}' has a parameter '{1}' that only has one character. - - - A script block has a parameter '{0}' that only has one character. - - - One Char - - - For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. - - - The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. - - - The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. - - - Use PSCredential type. - - - Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors. - - - The cmdlet '{0}' uses a reserved char in its name. - - - Reserved Cmdlet Chars - - - The cmdlet '{0}' - - - Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs. - - - '{0}' defines the reserved common parameter '{1}'. - - - Reserved Parameters - - - The script - - - #,(){}[]&/\\$^;:\"'<>|?@`*%+=~ - - - Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior. - - - '{0}' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. - - - A script block has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. - - - '{0}' calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. - - - A script block calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. - - - Should Process - - - PS - - - It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed. - - - There is no call to Write-Verbose in DSC function '{0}'. If you are using Write-Verbose in a helper function, suppress this rule application. - - - Use verbose message in DSC resource - - - Some fields of the module manifest (such as ModuleVersion) are required. - - - Module Manifest Fields - - - If a script file is in a PowerShell module folder, then that folder must be loadable. - - - Cannot load the module '{0}' that file '{1}' is in. - - - Module Must Be Loadable - - - Error Message is Null. - - - Password parameters that take in plaintext will expose passwords and compromise the security of your system. - - - Parameter '{0}' should use SecureString, otherwise this will expose sensitive information. See ConvertTo-SecureString for more information. - - - Avoid Using Plain Text For Password Parameter - - - Using ConvertTo-SecureString with plain text will expose secure information. - - - File '{0}' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. - - - Avoid Using SecureString With Plain Text - - - Switch parameter should not default to true. - - - File '{0}' has a switch parameter default to true. - - - Switch Parameters Should Not Default To True - - - Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it. - - - Function '{0}' in file '{1}' uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt - - - Avoid Using ShouldContinue Without Boolean Force Parameter - - - Using Clear-Host is not recommended because the cmdlet may not work in some hosts or there may even be no hosts at all. - - - File '{0}' uses Clear-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all. - - - Avoid Using Clear-Host - - - File '{0}' uses Console.'{1}'. Using Console to write is not recommended because it may not work in all hosts or there may even be no hosts at all. Use Write-Output instead. - - - Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it. - - - File '{0}' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. - - - Avoid Using Write-Host - - - Cmdlet should use singular instead of plural nouns. - - - The cmdlet '{0}' uses a plural noun. A singular noun should be used instead. - - - Cmdlet Singular Noun - - - AvoidUsingCmdletAliases - - - AvoidDefaultValueSwitchParameter - - - AvoidGlobalVars - - - AvoidShouldContinueWithoutForce - - - AvoidUnloadableModule - - - AvoidUsingClearHost - - - AvoidUsingConvertToSecureStringWithPlainText - - - AvoidUsingEmptyCatchBlock - - - AvoidUsingInvokeExpression - - - AvoidUsingPlainTextForPassword - - - AvoidUsingPositionalParameters - - - AvoidUsingWriteHost - - - OneChar - - - PossibleIncorrectComparisonWithNull - - - ProvideCommentHelp - - - ReservedCmdletChar - - - ReservedParams - - - ShouldProcess - - - UseApprovedVerbs - - - UseDeclaredVarsMoreThanAssignments - - - UsePSCredentialType - - - UseSingularNouns - - - MissingModuleManifestField - - - UseVerboseMessageInDSCResource - - - Command Not Found - - - Commands that are undefined or do not exist should not be used. - - - Command '{0}' Is Not Found - - - CommandNotFound - - - Type Not Found - - - Undefined type should not be used - - - Type '{0}' is not found. Please check that it is defined. - - - TypeNotFound - - - Use Cmdlet Correctly - - - Cmdlet should be called with the mandatory parameters. - - - Cmdlet '{0}' may be used incorrectly. Please check that all mandatory parameters are supplied. - - - UseCmdletCorrectly - - - Use Type At Variable Assignment - - - Types should be specified at variable assignments to maintain readability and maintainability of script. - - - Specify type at the assignment of variable '{0}' - - - UseTypeAtVariableAssignment - - - Avoid Using Username and Password Parameters - - - Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString. - - - Function '{0}' has both Username and Password parameters. Either set the type of the Password parameter to SecureString or replace the Username and Password parameters with a Credential parameter of type PSCredential. If using a Credential parameter in PowerShell 4.0 or earlier, please define a credential transformation attribute after the PSCredential type attribute. - - - AvoidUsingUsernameAndPasswordParams - - - Avoid Invoking Empty Members - - - Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant. - - - '{0}' has non-constant members. Invoking non-constant members may cause bugs in the script. - - - AvoidInvokingEmptyMembers - - - Avoid Using ComputerName Hardcoded - - - The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system. - - - The ComputerName parameter of cmdlet '{0}' is hardcoded. This will expose sensitive information about the system if the script is shared. - - - AvoidUsingComputerNameHardcoded - - - Empty catch block is used. Please use Write-Error or throw statements in catch blocks. - - - '{0}' is an alias of '{1}'. Alias can introduce possible problems and make scripts hard to maintain. Please consider changing alias to its full content. - - - Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead. - - - Cmdlet '{0}' has positional parameter. Please use named parameters instead of positional parameters when calling a command. - - - {0}{1} - - - Cannot process null Ast - - - Cannot process null CommandInfo - - - PSDSC - - - Use Standard Get/Set/Test TargetResource functions in DSC Resource - - - DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions. - - - Missing '{0}' function. DSC Resource must implement Get, Set and Test-TargetResource functions. - - - StandardDSCFunctionsInResource - - - Avoid Using Internal URLs - - - Using Internal URLs in the scripts may cause security problems. - - - '{0}' could be an internal URL. Using internal URL directly in the script may cause potential information disclosure. - - - AvoidUsingInternalURLs - - - www.sharepoint.com - - - Use Identical Parameters For DSC Test and Set Functions - - - The Test and Set-TargetResource functions of DSC Resource must have the same parameters. - - - The Test and Set-TargetResource functions of DSC Resource must have the same parameters. - - - UseIdenticalParametersForDSC - - - Missing '{0}' function. DSC Class must implement Get, Set and Test functions. - - - Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions - - - The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters. - - - The '{0}' parameter '{1}' is not present in '{2}' DSC resource function(s). - - - UseIdenticalMandatoryParametersForDSC - - - Not all code path in {0} function in DSC Class {1} returns a value - - - ReturnCorrectTypesForDSCFunctions - - - Return Correct Types For DSC Functions - - - Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean. - - - {0} function in DSC Class {1} should return object of type {2} - - - {0} function in DSC Class {1} should return object of type {2} instead of type {3} - - - Set function in DSC Class {0} should not return anything - - - {0} function in DSC Resource should return object of type {1} instead of {2} - - - Set-TargetResource function in DSC Resource should not output anything to the pipeline. - - - Use ShouldProcess For State Changing Functions - - - Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'. - - - Function '{0}' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'. - - - UseShouldProcessForStateChangingFunctions - - - Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance - - - Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets. - - - File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. - - - AvoidUsingWMICmdlet - - - Use OutputType Correctly - - - The return types of a cmdlet should be declared using the OutputType attribute. - - - The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute. - - - UseOutputTypeCorrectly - - - DscTestsPresent - - - Dsc tests are present - - - Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. - - - No tests found for resource '{0}' - - - DscExamplesPresent - - - DSC examples are present - - - Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title. - - - No examples found for resource '{0}' - - - Avoid Default Value For Mandatory Parameter - - - Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored.. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block. - - - Mandatory Parameter '{0}' is initialized in the Param block. To fix a violation of this rule, please leave it uninitialized. - - - AvoidDefaultValueForMandatoryParameter - - - Avoid Using Deprecated Manifest Fields - - - "ModuleToProcess" is obsolete in the latest PowerShell version. Please update with the latest field "RootModule" in manifest files to avoid PowerShell version inconsistency. - - - AvoidUsingDeprecatedManifestFields - - - Use UTF8 Encoding For Help File - - - PowerShell help file needs to use UTF8 Encoding. - - - File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file. - - - UseUTF8EncodingForHelpFile - - - Use BOM encoding for non-ASCII files - - - For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly. - - - Missing BOM encoding for non-ASCII encoded file '{0}' - - - UseBOMForUnicodeEncodedFile - - - Script definition has a switch parameter default to true. - - - Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt - - - Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. - - - Script definition uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. - - - Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. - - - ScriptDefinition - - - Misleading Backtick - - - Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. - - - MisleadingBacktick - - - This line has a backtick at the end trailed by a whitespace character. Did you mean for this to be a line continuation? - - - Avoid using null or empty HelpMessage parameter attribute. - - - Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function. - - - HelpMessage parameter attribute should not be null or empty. To fix a violation of this rule, please set its value to a non-empty string. - - - AvoidNullOrEmptyHelpMessageAttribute - - - Use the *ToExport module manifest fields. - - - In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module. - - - Do not use wildcard or $null in this field. Explicitly specify a list for {0}. - - - UseToExportFieldsInManifest - - - Replace {0} with {1} - - - Set {0} type to SecureString - - - Add {0} = {1} to the module manifest - - - Replace {0} with {1} - - - Create hashtables with literal initializers - - - Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default - - - Create hashtables with literal initliazers - - - UseLiteralInitializerForHashtable - - - UseCompatibleCmdlets - - - Use compatible cmdlets - - - Use cmdlets compatible with the given PowerShell version and edition and operating system - - - '{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}' - - - AvoidOverwritingBuiltInCmdlets - - - Avoid overwriting built in cmdlets - - - Do not overwrite the definition of a cmdlet that is included with PowerShell - - - '{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden - - - UseCompatibleCommands - - - Use compatible commands - - - Use commands compatible with the given PowerShell version and operating system - - - The command '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - The parameter '{0}' is not available for command '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - UseCompatibleTypes - - - Use compatible types - - - Use types compatible with the given PowerShell version and operating system - - - The type '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - The method '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - The member '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - UseCompatibleSyntax - - - Use compatible syntax - - - Use script syntax compatible with the given PowerShell versions - - - The {0} syntax '{1}' is not available by default in PowerShell versions {2} - - - Use the '{0}' syntax instead for compatibility with PowerShell versions {1} - - - The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - Avoid global functiosn and aliases - - - Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems. - - - Avoid creating functions with a Global scope. - - - AvoidGlobalFunctions - - - Avoid global aliases. - - - Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts. - - - Avoid creating aliases with a Global scope. - - - AvoidGlobalAliases - - - AvoidTrailingWhitespace - - - Avoid trailing whitespace - - - Each line should have no trailing whitespace. - - - Line has trailing whitespace - - - AvoidLongLines - - - Avoid long lines - - - Line lengths should be less than the configured maximum - - - Line exceeds the configured maximum length of {0} characters - - - PlaceOpenBrace - - - Place open braces consistently - - - Place open braces either on the same line as the preceding expression or on a new line. - - - Open brace not on same line as preceding keyword. It should be on the same line. - - - Open brace is not on a new line. - - - There is no new line after open brace. - - - PlaceCloseBrace - - - Place close braces - - - Close brace should be on a new line by itself. - - - Close brace is not on a new line. - - - Close brace does not follow a non-empty line. - - - Close brace does not follow a new line. - - - Close brace before a branch statement is followed by a new line. - - - UseConsistentIndentation - - - Use consistent indentation - - - Each statement block should have a consistent indenation. - - - Indentation not consistent - - - UseConsistentWhitespace - - - Use whitespaces - - - Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';') - - - Use space before open brace. - - - Use space before open parenthesis. - - - Use space before and after binary and assignment operators. - - - Use space after a comma. - - - Use space after a semicolon. - - - UseSupportsShouldProcess - - - Use SupportsShouldProcess - - - Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess. - - - Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. - - - AlignAssignmentStatement - - - Align assignment statement - - - Line up assignment statements such that the assignment operator are aligned. - - - Assignment statements are not aligned - - - '=' is not an assignment operator. Did you mean the equality operator '-eq'? - - - PossibleIncorrectUsageOfAssignmentOperator - - - Use a different variable name - - - Changing automtic variables might have undesired side effects - - - This automatic variables is built into PowerShell and readonly. - - - The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name. - - - AvoidAssignmentToAutomaticVariable - - - 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. - - - '{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. - - - '=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements. - - - Did you mean to use the assignment operator '='? The equality operator in PowerShell is 'eq'. - - - '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal). - - - When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell. - - - Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal). - - - PossibleIncorrectUsageOfRedirectionOperator - - - Use $null on the left hand side for safe comparison with $null. - - - Use space after open brace. - - - Use space before closing brace. - - - Use space after pipe. - - - Use space before pipe. - - - Use exact casing of cmdlet/function/parameter name. - - - For better readability and consistency, use the exact casing of the cmdlet/function/parameter. - - - Cmdlet/Function/Parameter does not match its exact casing '{0}'. - - - UseCorrectCasing - - - Use process block for command that accepts input from pipeline. - - - If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter. - - - Command accepts pipeline input but has not defined a process block. - - - UseProcessBlockForPipelineCommand - - - The Variable '{0}' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name. - - - Use only 1 whitespace between parameter names or values. - - - ReviewUnusedParameter - - - Ensure all parameters are used within the same script, scriptblock, or function where they are declared. - - - The parameter '{0}' has been declared but not used. - - - ReviewUnusedParameter - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability. + + + Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. + + + Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks. + + + Avoid Using Empty Catch Block + + + The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please remove Invoke-Expression from script and find other options instead. + + + Avoid Using Invoke-Expression + + + Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. + + + Avoid Using Positional Parameters + + + Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment. + + + The cmdlet '{0}' does not have a help comment. + + + Basic Comment Help + + + Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices. + + + The cmdlet '{0}' uses an unapproved verb. + + + Cmdlet Verbs + + + Ensure declared variables are used elsewhere in the script and not just during assignment. + + + The variable '{0}' is assigned but never used. + + + Extra Variables + + + Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems. + + + Found global variable '{0}'. + + + No Global Variables + + + Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. + + + $null should be on the left side of equality comparisons. + + + Null Comparison + + + Checks that cmdlets and parameters have more than one character. + + + The cmdlet name '{0}' only has one character. + + + The cmdlet '{0}' has a parameter '{1}' that only has one character. + + + A script block has a parameter '{0}' that only has one character. + + + One Char + + + For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. + + + The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. + + + The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. + + + Use PSCredential type. + + + Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors. + + + The cmdlet '{0}' uses a reserved char in its name. + + + Reserved Cmdlet Chars + + + The cmdlet '{0}' + + + Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs. + + + '{0}' defines the reserved common parameter '{1}'. + + + Reserved Parameters + + + The script + + + #,(){}[]&/\\$^;:\"'<>|?@`*%+=~ + + + Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior. + + + '{0}' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. + + + A script block has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. + + + '{0}' calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. + + + A script block calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. + + + Should Process + + + PS + + + It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed. + + + There is no call to Write-Verbose in DSC function '{0}'. If you are using Write-Verbose in a helper function, suppress this rule application. + + + Use verbose message in DSC resource + + + Some fields of the module manifest (such as ModuleVersion) are required. + + + Module Manifest Fields + + + If a script file is in a PowerShell module folder, then that folder must be loadable. + + + Cannot load the module '{0}' that file '{1}' is in. + + + Module Must Be Loadable + + + Error Message is Null. + + + Password parameters that take in plaintext will expose passwords and compromise the security of your system. + + + Parameter '{0}' should use SecureString, otherwise this will expose sensitive information. See ConvertTo-SecureString for more information. + + + Avoid Using Plain Text For Password Parameter + + + Using ConvertTo-SecureString with plain text will expose secure information. + + + File '{0}' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. + + + Avoid Using SecureString With Plain Text + + + Switch parameter should not default to true. + + + File '{0}' has a switch parameter default to true. + + + Switch Parameters Should Not Default To True + + + Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it. + + + Function '{0}' in file '{1}' uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt + + + Avoid Using ShouldContinue Without Boolean Force Parameter + + + Using Clear-Host is not recommended because the cmdlet may not work in some hosts or there may even be no hosts at all. + + + File '{0}' uses Clear-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all. + + + Avoid Using Clear-Host + + + File '{0}' uses Console.'{1}'. Using Console to write is not recommended because it may not work in all hosts or there may even be no hosts at all. Use Write-Output instead. + + + Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it. + + + File '{0}' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. + + + Avoid Using Write-Host + + + Cmdlet should use singular instead of plural nouns. + + + The cmdlet '{0}' uses a plural noun. A singular noun should be used instead. + + + Cmdlet Singular Noun + + + AvoidUsingCmdletAliases + + + AvoidDefaultValueSwitchParameter + + + AvoidGlobalVars + + + AvoidShouldContinueWithoutForce + + + AvoidUnloadableModule + + + AvoidUsingClearHost + + + AvoidUsingConvertToSecureStringWithPlainText + + + AvoidUsingEmptyCatchBlock + + + AvoidUsingInvokeExpression + + + AvoidUsingPlainTextForPassword + + + AvoidUsingPositionalParameters + + + AvoidUsingWriteHost + + + OneChar + + + PossibleIncorrectComparisonWithNull + + + ProvideCommentHelp + + + ReservedCmdletChar + + + ReservedParams + + + ShouldProcess + + + UseApprovedVerbs + + + UseDeclaredVarsMoreThanAssignments + + + UsePSCredentialType + + + UseSingularNouns + + + MissingModuleManifestField + + + UseVerboseMessageInDSCResource + + + Command Not Found + + + Commands that are undefined or do not exist should not be used. + + + Command '{0}' Is Not Found + + + CommandNotFound + + + Type Not Found + + + Undefined type should not be used + + + Type '{0}' is not found. Please check that it is defined. + + + TypeNotFound + + + Use Cmdlet Correctly + + + Cmdlet should be called with the mandatory parameters. + + + Cmdlet '{0}' may be used incorrectly. Please check that all mandatory parameters are supplied. + + + UseCmdletCorrectly + + + Use Type At Variable Assignment + + + Types should be specified at variable assignments to maintain readability and maintainability of script. + + + Specify type at the assignment of variable '{0}' + + + UseTypeAtVariableAssignment + + + Avoid Using Username and Password Parameters + + + Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString. + + + Function '{0}' has both Username and Password parameters. Either set the type of the Password parameter to SecureString or replace the Username and Password parameters with a Credential parameter of type PSCredential. If using a Credential parameter in PowerShell 4.0 or earlier, please define a credential transformation attribute after the PSCredential type attribute. + + + AvoidUsingUsernameAndPasswordParams + + + Avoid Invoking Empty Members + + + Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant. + + + '{0}' has non-constant members. Invoking non-constant members may cause bugs in the script. + + + AvoidInvokingEmptyMembers + + + Avoid Using ComputerName Hardcoded + + + The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system. + + + The ComputerName parameter of cmdlet '{0}' is hardcoded. This will expose sensitive information about the system if the script is shared. + + + AvoidUsingComputerNameHardcoded + + + Empty catch block is used. Please use Write-Error or throw statements in catch blocks. + + + '{0}' is an alias of '{1}'. Alias can introduce possible problems and make scripts hard to maintain. Please consider changing alias to its full content. + + + Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead. + + + Cmdlet '{0}' has positional parameter. Please use named parameters instead of positional parameters when calling a command. + + + {0}{1} + + + Cannot process null Ast + + + Cannot process null CommandInfo + + + PSDSC + + + Use Standard Get/Set/Test TargetResource functions in DSC Resource + + + DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions. + + + Missing '{0}' function. DSC Resource must implement Get, Set and Test-TargetResource functions. + + + StandardDSCFunctionsInResource + + + Avoid Using Internal URLs + + + Using Internal URLs in the scripts may cause security problems. + + + '{0}' could be an internal URL. Using internal URL directly in the script may cause potential information disclosure. + + + AvoidUsingInternalURLs + + + www.sharepoint.com + + + Use Identical Parameters For DSC Test and Set Functions + + + The Test and Set-TargetResource functions of DSC Resource must have the same parameters. + + + The Test and Set-TargetResource functions of DSC Resource must have the same parameters. + + + UseIdenticalParametersForDSC + + + Missing '{0}' function. DSC Class must implement Get, Set and Test functions. + + + Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions + + + The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters. + + + The '{0}' parameter '{1}' is not present in '{2}' DSC resource function(s). + + + UseIdenticalMandatoryParametersForDSC + + + Not all code path in {0} function in DSC Class {1} returns a value + + + ReturnCorrectTypesForDSCFunctions + + + Return Correct Types For DSC Functions + + + Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean. + + + {0} function in DSC Class {1} should return object of type {2} + + + {0} function in DSC Class {1} should return object of type {2} instead of type {3} + + + Set function in DSC Class {0} should not return anything + + + {0} function in DSC Resource should return object of type {1} instead of {2} + + + Set-TargetResource function in DSC Resource should not output anything to the pipeline. + + + Use ShouldProcess For State Changing Functions + + + Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'. + + + Function '{0}' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'. + + + UseShouldProcessForStateChangingFunctions + + + Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance + + + Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets. + + + File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. + + + AvoidUsingWMICmdlet + + + Use OutputType Correctly + + + The return types of a cmdlet should be declared using the OutputType attribute. + + + The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute. + + + UseOutputTypeCorrectly + + + DscTestsPresent + + + Dsc tests are present + + + Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. + + + No tests found for resource '{0}' + + + DscExamplesPresent + + + DSC examples are present + + + Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title. + + + No examples found for resource '{0}' + + + Avoid Default Value For Mandatory Parameter + + + Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored.. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block. + + + Mandatory Parameter '{0}' is initialized in the Param block. To fix a violation of this rule, please leave it uninitialized. + + + AvoidDefaultValueForMandatoryParameter + + + Avoid Using Deprecated Manifest Fields + + + "ModuleToProcess" is obsolete in the latest PowerShell version. Please update with the latest field "RootModule" in manifest files to avoid PowerShell version inconsistency. + + + AvoidUsingDeprecatedManifestFields + + + Use UTF8 Encoding For Help File + + + PowerShell help file needs to use UTF8 Encoding. + + + File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file. + + + UseUTF8EncodingForHelpFile + + + Use BOM encoding for non-ASCII files + + + For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly. + + + Missing BOM encoding for non-ASCII encoded file '{0}' + + + UseBOMForUnicodeEncodedFile + + + Script definition has a switch parameter default to true. + + + Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt + + + Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. + + + Script definition uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. + + + Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. + + + ScriptDefinition + + + Misleading Backtick + + + Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. + + + MisleadingBacktick + + + This line has a backtick at the end trailed by a whitespace character. Did you mean for this to be a line continuation? + + + Avoid using null or empty HelpMessage parameter attribute. + + + Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function. + + + HelpMessage parameter attribute should not be null or empty. To fix a violation of this rule, please set its value to a non-empty string. + + + AvoidNullOrEmptyHelpMessageAttribute + + + Use the *ToExport module manifest fields. + + + In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module. + + + Do not use wildcard or $null in this field. Explicitly specify a list for {0}. + + + UseToExportFieldsInManifest + + + Replace {0} with {1} + + + Set {0} type to SecureString + + + Add {0} = {1} to the module manifest + + + Replace {0} with {1} + + + Create hashtables with literal initializers + + + Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default + + + Create hashtables with literal initliazers + + + UseLiteralInitializerForHashtable + + + UseCompatibleCmdlets + + + Use compatible cmdlets + + + Use cmdlets compatible with the given PowerShell version and edition and operating system + + + '{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}' + + + AvoidOverwritingBuiltInCmdlets + + + Avoid overwriting built in cmdlets + + + Do not overwrite the definition of a cmdlet that is included with PowerShell + + + '{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden + + + UseCompatibleCommands + + + Use compatible commands + + + Use commands compatible with the given PowerShell version and operating system + + + The command '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + The parameter '{0}' is not available for command '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + UseCompatibleTypes + + + Use compatible types + + + Use types compatible with the given PowerShell version and operating system + + + The type '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + The method '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + The member '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + UseCompatibleSyntax + + + Use compatible syntax + + + Use script syntax compatible with the given PowerShell versions + + + The {0} syntax '{1}' is not available by default in PowerShell versions {2} + + + Use the '{0}' syntax instead for compatibility with PowerShell versions {1} + + + The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + Avoid global functiosn and aliases + + + Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems. + + + Avoid creating functions with a Global scope. + + + AvoidGlobalFunctions + + + Avoid global aliases. + + + Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts. + + + Avoid creating aliases with a Global scope. + + + AvoidGlobalAliases + + + AvoidTrailingWhitespace + + + Avoid trailing whitespace + + + Each line should have no trailing whitespace. + + + Line has trailing whitespace + + + AvoidLongLines + + + Avoid long lines + + + Line lengths should be less than the configured maximum + + + Line exceeds the configured maximum length of {0} characters + + + PlaceOpenBrace + + + Place open braces consistently + + + Place open braces either on the same line as the preceding expression or on a new line. + + + Open brace not on same line as preceding keyword. It should be on the same line. + + + Open brace is not on a new line. + + + There is no new line after open brace. + + + PlaceCloseBrace + + + Place close braces + + + Close brace should be on a new line by itself. + + + Close brace is not on a new line. + + + Close brace does not follow a non-empty line. + + + Close brace does not follow a new line. + + + Close brace before a branch statement is followed by a new line. + + + UseConsistentIndentation + + + Use consistent indentation + + + Each statement block should have a consistent indenation. + + + Indentation not consistent + + + UseConsistentWhitespace + + + Use whitespaces + + + Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';') + + + Use space before open brace. + + + Use space before open parenthesis. + + + Use space before and after binary and assignment operators. + + + Use space after a comma. + + + Use space after a semicolon. + + + UseSupportsShouldProcess + + + Use SupportsShouldProcess + + + Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess. + + + Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. + + + AlignAssignmentStatement + + + Align assignment statement + + + Line up assignment statements such that the assignment operator are aligned. + + + Assignment statements are not aligned + + + '=' is not an assignment operator. Did you mean the equality operator '-eq'? + + + PossibleIncorrectUsageOfAssignmentOperator + + + Use a different variable name + + + Changing automtic variables might have undesired side effects + + + This automatic variables is built into PowerShell and readonly. + + + The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name. + + + AvoidAssignmentToAutomaticVariable + + + 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. + + + '{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. + + + '=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements. + + + Did you mean to use the assignment operator '='? The equality operator in PowerShell is 'eq'. + + + '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal). + + + When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell. + + + Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal). + + + PossibleIncorrectUsageOfRedirectionOperator + + + Use $null on the left hand side for safe comparison with $null. + + + Use space after open brace. + + + Use space before closing brace. + + + Use space after pipe. + + + Use space before pipe. + + + Use exact casing of cmdlet/function/parameter name. + + + For better readability and consistency, use the exact casing of the cmdlet/function/parameter. + + + Cmdlet/Function/Parameter does not match its exact casing '{0}'. + + + UseCorrectCasing + + + Use process block for command that accepts input from pipeline. + + + If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter. + + + Command accepts pipeline input but has not defined a process block. + + + UseProcessBlockForPipelineCommand + + + The Variable '{0}' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name. + + + Use only 1 whitespace between parameter names or values. + + + ReviewUnusedParameter + + + Ensure all parameters are used within the same script, scriptblock, or function where they are declared. + + + The parameter '{0}' has been declared but not used. + + + ReviewUnusedParameter + + + Use $Using: directive in runspace scriptblocks + + + If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock. + + + AvoidUnInitializedVarsInNewRunspaces + \ No newline at end of file From 7182edbba6614e5071dcecc05a4ef7dac9826a44 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 17 Feb 2020 20:31:24 +0100 Subject: [PATCH 03/55] Add documentation --- .../AvoidUnInitializedVarsInNewRunspaces.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md diff --git a/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md b/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md new file mode 100644 index 000000000..bc45e10a0 --- /dev/null +++ b/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md @@ -0,0 +1,27 @@ +# AvoidUnInitializedVarsInNewRunspaces + +**Severity Level: Warning** + +## Description + +If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock. + +## How to Fix + +Within `Foreach-Object -Parallel {}`, instead of just using a variable from the parent scope, you have to use the `using:` directive: + +## Example + +### Wrong + +``````PowerShell +$var = "foo" +1..2 | ForEach-Object -Parallel { $var } +`````` + +### Correct + +``````PowerShell +$var = "foo" +1..2 | ForEach-Object -Parallel { $using:var } +`````` \ No newline at end of file From 068b1fce7400e463ef17595ab1316516f4cc24b9 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 17 Feb 2020 20:36:49 +0100 Subject: [PATCH 04/55] wrestling with Ast in C# - not working --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 Rules/AvoidUnInitializedVarsInNewRunspaces.cs diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs new file mode 100644 index 000000000..df86bfea8 --- /dev/null +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// AvoidUnInitializedVarsInNewRunspaces: Analyzes the ast to check that variables in script blocks that run in new run spaces are properly initialized or passed in with '$using:(varName)'. + /// +#if !CORECLR +[Export(typeof(IScriptRule))] +#endif + public class AvoidUnInitializedVarsInNewRunspaces : IScriptRule + { + /// + /// AnalyzeScript: Analyzes the ast to check variables in script blocks that will run in new runspaces are properly initialized or passed in with $using: + /// + /// The script's ast + /// The script's file name + /// A List of results from this rule + public IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException(Strings.NullAstErrorMessage); + } + + var scriptBlockAsts = ast.FindAll(x => x is ScriptBlockAst, true); + if (scriptBlockAsts == null) + { + yield break; + } + + foreach (var scriptBlockAst in scriptBlockAsts) + { + var sbAst = scriptBlockAst as ScriptBlockAst; + foreach (var diagnosticRecord in AnalyzeScriptBlockAst(sbAst, fileName)) + { + yield return diagnosticRecord; + } + } + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUnInitializedVarsInNewRunspacesName); + } + + /// + /// GetCommonName: Retrieves the common name of this rule. + /// + /// The common name of this rule + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUnInitializedVarsInNewRunspacesCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUnInitializedVarsInNewRunspacesDescription); + } + + /// + /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning of information. + /// + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// GetSourceName: Retrieves the module/assembly name the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Checks if a variable is initialized and referenced in either its assignment or children scopes + /// + /// Ast of type ScriptBlock + /// Name of file containing the ast + /// An enumerable containing diagnostic records + private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scriptBlockAst, string fileName) + { + var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); + + // Find all commandAst objects for `Foreach-Object -Parallel`. As for parametername matching, there are three + // parameters starting with a 'p': Parallel, PipelineVariable and Process, so we use startsWith 'pa' as the shortest unambiguous form. + // Because we are already going trough all ScriptBlockAst objects, we do not need to look for nested script blocks here. + if (!(scriptBlockAst.FindAll( + predicate: c => c is CommandAst commandAst && + foreachObjectCmdletNamesAndAliases.Contains(commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: true) is IEnumerable foreachObjectParallelAsts)) + { + yield break; + } + + foreach (var ast in foreachObjectParallelAsts) + { + var commandAst = ast as CommandAst; + + if (commandAst == null) + { + continue; + } + + var varsInAssignments = commandAst.FindAll( + predicate: a => a is AssignmentStatementAst assignment && + assignment.Left.FindAll( + predicate: aa => aa is VariableExpressionAst, + searchNestedScriptBlocks: true) != null, + searchNestedScriptBlocks: true); + + var commandElements = commandAst.CommandElements; + var nonAssignedNonUsingVars = new List() { }; + foreach (var cmdEl in commandElements) + { + nonAssignedNonUsingVars.AddRange( + cmdEl.FindAll( + predicate: aa => aa is VariableExpressionAst varAst && + !(varAst.Parent is UsingExpressionAst) && + !varsInAssignments.Contains(varAst), true)); + } + + foreach (var variableExpression in nonAssignedNonUsingVars) + { + var _temp = variableExpression as VariableExpressionAst; + + yield return new DiagnosticRecord( + message: string.Format(CultureInfo.CurrentCulture, + Strings.UseDeclaredVarsMoreThanAssignmentsError, _temp?.ToString()), + extent: _temp?.Extent, + ruleName: GetName(), + severity: DiagnosticSeverity.Warning, + scriptPath: fileName, + ruleId: _temp?.ToString()); + } + } + } + } +} From adbeff032a8878d25d931ac7831b5b21527f6ca1 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:44:26 +0100 Subject: [PATCH 05/55] Add sensible warning message --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 6 +++--- Rules/Strings.Designer.cs | 11 ++++++++++- Rules/Strings.resx | 5 ++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index df86bfea8..9e71fd655 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -160,8 +160,8 @@ private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scrip yield return new DiagnosticRecord( message: string.Format(CultureInfo.CurrentCulture, - Strings.UseDeclaredVarsMoreThanAssignmentsError, _temp?.ToString()), - extent: _temp?.Extent, + Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), + extent: variableExpression.Extent, ruleName: GetName(), severity: DiagnosticSeverity.Warning, scriptPath: fileName, diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index ab5676251..e4f5649fc 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -628,7 +628,7 @@ internal static string AvoidUnInitializedVarsInNewRunspacesCommonName { } /// - /// Looks up a localized string similar to If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock.. + /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the ScriptBlock.. /// internal static string AvoidUnInitializedVarsInNewRunspacesDescription { get { @@ -636,6 +636,15 @@ internal static string AvoidUnInitializedVarsInNewRunspacesDescription { } } + /// + /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' directive.. + /// + internal static string AvoidUnInitializedVarsInNewRunspacesError { + get { + return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesError", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidUnInitializedVarsInNewRunspaces. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 70a7d3f93..529b942bf 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1126,9 +1126,12 @@ Use $Using: directive in runspace scriptblocks - If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock. + If a ScriptBlock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the ScriptBlock. AvoidUnInitializedVarsInNewRunspaces + + The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' directive. + \ No newline at end of file From 23183d201f10da1271ae6d842cbfd066f97200f5 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:45:24 +0100 Subject: [PATCH 06/55] remove unnecessary boilerplate from test --- .../Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 index 39564de54..e8d4079b9 100644 --- a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 +++ b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 @@ -1,12 +1,5 @@ -$directory = Split-Path -Parent $MyInvocation.MyCommand.Path -$testRootDirectory = Split-Path -Parent $directory - -Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") - -$ruleName = "PSAvoidUnInitializedVarsInNewRunspaces" - $settings = @{ - IncludeRules = @($ruleName) + IncludeRules = "PSAvoidUnInitializedVarsInNewRunspaces" } Describe "AvoidUnInitializedVarsInNewRunspaces" { From 83394d6e31bcea2bed4ff6b6265147f1e44cdc08 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:45:49 +0100 Subject: [PATCH 07/55] clean up and finetune rule --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 70 ++++++++----------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index 9e71fd655..5e2cd4c63 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -111,53 +111,45 @@ public string GetSourceName() /// An enumerable containing diagnostic records private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scriptBlockAst, string fileName) { + // TODO: add other Cmdlets like invoke-command later? var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - // Find all commandAst objects for `Foreach-Object -Parallel`. As for parametername matching, there are three + // Find all commandAst objects for `Foreach-Object -Parallel`. As for parameter name matching, there are three // parameters starting with a 'p': Parallel, PipelineVariable and Process, so we use startsWith 'pa' as the shortest unambiguous form. // Because we are already going trough all ScriptBlockAst objects, we do not need to look for nested script blocks here. - if (!(scriptBlockAst.FindAll( + var foreachObjectParallelCommandAsts = scriptBlockAst.FindAll( predicate: c => c is CommandAst commandAst && - foreachObjectCmdletNamesAndAliases.Contains(commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: true) is IEnumerable foreachObjectParallelAsts)) + foreachObjectCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false).Select(a=>a as CommandAst); + + foreach (var commandAst in foreachObjectParallelCommandAsts) { - yield break; - } - - foreach (var ast in foreachObjectParallelAsts) - { - var commandAst = ast as CommandAst; - - if (commandAst == null) - { - continue; - } - + if (commandAst == null) + yield break; + + // Find all variables that are assigned within this ScriptBlock var varsInAssignments = commandAst.FindAll( - predicate: a => a is AssignmentStatementAst assignment && - assignment.Left.FindAll( - predicate: aa => aa is VariableExpressionAst, - searchNestedScriptBlocks: true) != null, - searchNestedScriptBlocks: true); - - var commandElements = commandAst.CommandElements; - var nonAssignedNonUsingVars = new List() { }; - foreach (var cmdEl in commandElements) - { - nonAssignedNonUsingVars.AddRange( - cmdEl.FindAll( - predicate: aa => aa is VariableExpressionAst varAst && - !(varAst.Parent is UsingExpressionAst) && - !varsInAssignments.Contains(varAst), true)); - } + predicate: a => a is VariableExpressionAst varExpr && + varExpr.Parent is AssignmentStatementAst assignment && + assignment.Left.Equals(varExpr), + searchNestedScriptBlocks: true). + Select(a => a as VariableExpressionAst); + + // Find all variables that are not locally assigned, and don't have $using: directive + var nonAssignedNonUsingVars = commandAst.CommandElements. + SelectMany(a => a.FindAll( + predicate: aa => aa is VariableExpressionAst varAst && + !(varAst.Parent is UsingExpressionAst) && + !varsInAssignments.Contains(varAst), + searchNestedScriptBlocks: true). + Select(aaa => aaa as VariableExpressionAst)); foreach (var variableExpression in nonAssignedNonUsingVars) { - var _temp = variableExpression as VariableExpressionAst; - yield return new DiagnosticRecord( message: string.Format(CultureInfo.CurrentCulture, Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), @@ -165,7 +157,7 @@ private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scrip ruleName: GetName(), severity: DiagnosticSeverity.Warning, scriptPath: fileName, - ruleId: _temp?.ToString()); + ruleId: variableExpression.ToString()); } } } From 99c2bea8feb34fcaddee2a8658a921549e9a202b Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:46:51 +0100 Subject: [PATCH 08/55] Add RuleToTest parameter to Test-ScriptAnalyzer to aid with test driven rule development --- build.psm1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/build.psm1 b/build.psm1 index 54b5d426b..8668e78f2 100644 --- a/build.psm1 +++ b/build.psm1 @@ -289,7 +289,14 @@ function Start-ScriptAnalyzerBuild function Test-ScriptAnalyzer { [CmdletBinding()] - param ( [Parameter()][switch]$InProcess, [switch]$ShowAll ) + param ( + [Parameter()] + [switch]$InProcess, + + [switch]$ShowAll, + + [string]$RuleToTest + ) END { # versions 3 and 4 don't understand versioned module paths, so we need to rename the directory of the version to @@ -319,7 +326,12 @@ function Test-ScriptAnalyzer $testModulePath = Join-Path "${projectRoot}" -ChildPath out } $testResultsFile = "'$(Join-Path ${projectRoot} -childPath TestResults.xml)'" - $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" + if ($RuleToTest) { + $testScripts = Get-Childitem -Path "${projectRoot}\Tests\Rules" -Filter *$RuleToTest*.tests.ps1 | Select-Object -ExpandProperty FullName + } + else { + $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" + } try { if ( $major -lt 5 ) { Rename-Item $script:destinationDir ${testModulePath} From 91e668950610ac00f5edd892478c0d475fd75d26 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:53:41 +0100 Subject: [PATCH 09/55] increment rule count to fix test --- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index eac588eb4..d94d419c7 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -59,7 +59,7 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 63 + $expectedNumRules = 64 if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4)) { # for PSv3 PSAvoidGlobalAliases is not shipped because From 337ac201c5efa85e75b058116be5f5d1c412a372 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 09:53:59 +0100 Subject: [PATCH 10/55] add reference to rule documentation --- RuleDocumentation/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RuleDocumentation/README.md b/RuleDocumentation/README.md index 78a0ab2e4..d8e11c7b3 100644 --- a/RuleDocumentation/README.md +++ b/RuleDocumentation/README.md @@ -16,6 +16,7 @@ |[AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | | |[AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | | |[AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | | +|[AvoidUnInitializedVarsInNewRunspaces](./AvoidUnInitializedVarsInNewRunspaces.md) | Warning | | |[AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | |[AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | | |[AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | | From 406d7eca43fe5aa960da7cc62733a6e0c4db65cd Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 20 Feb 2020 12:51:17 +0100 Subject: [PATCH 11/55] exclude built-in variables --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index 5e2cd4c63..745d94aa4 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -144,7 +144,8 @@ varExpr.Parent is AssignmentStatementAst assignment && SelectMany(a => a.FindAll( predicate: aa => aa is VariableExpressionAst varAst && !(varAst.Parent is UsingExpressionAst) && - !varsInAssignments.Contains(varAst), + !varsInAssignments.Contains(varAst) && + !Helper.Instance.HasSpecialVars(varAst.VariablePath.UserPath), searchNestedScriptBlocks: true). Select(aaa => aaa as VariableExpressionAst)); From 55b73169f56680e69758a7a8ca9e709837365bb3 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 24 Feb 2020 07:57:09 +0100 Subject: [PATCH 12/55] change using directive => scoope modifier --- Rules/Strings.Designer.cs | 6 +++--- Rules/Strings.resx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index e4f5649fc..3796dc50a 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -619,7 +619,7 @@ internal static string AvoidTrailingWhitespaceName { } /// - /// Looks up a localized string similar to Use $Using: directive in runspace scriptblocks. + /// Looks up a localized string similar to Use '$using:' scope modifier in runspace scriptblocks. /// internal static string AvoidUnInitializedVarsInNewRunspacesCommonName { get { @@ -628,7 +628,7 @@ internal static string AvoidUnInitializedVarsInNewRunspacesCommonName { } /// - /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the ScriptBlock.. + /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new runspace, variables inside it should use '$using:' scope modifier, or be initialized within the ScriptBlock.. /// internal static string AvoidUnInitializedVarsInNewRunspacesDescription { get { @@ -637,7 +637,7 @@ internal static string AvoidUnInitializedVarsInNewRunspacesDescription { } /// - /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' directive.. + /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' scope modifier.. /// internal static string AvoidUnInitializedVarsInNewRunspacesError { get { diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 529b942bf..37f959b1a 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1123,15 +1123,15 @@ ReviewUnusedParameter - Use $Using: directive in runspace scriptblocks + Use '$using:' scope modifier in runspace scriptblocks - If a ScriptBlock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the ScriptBlock. + If a ScriptBlock is intended to be run as a new runspace, variables inside it should use '$using:' scope modifier, or be initialized within the ScriptBlock. AvoidUnInitializedVarsInNewRunspaces - The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' directive. + The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' scope modifier. \ No newline at end of file From ef4b79a22cb21ed5dfe0721c7c79abcac1ed63e6 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 24 Feb 2020 07:58:01 +0100 Subject: [PATCH 13/55] add tests for InlineScript, Invoke-Command and Start-(Thread)Job --- ...dUnInitializedVarsInNewRunspaces.tests.ps1 | 126 +++++++++++++++++- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 index e8d4079b9..499718d08 100644 --- a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 +++ b/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 @@ -1,10 +1,12 @@ $settings = @{ IncludeRules = "PSAvoidUnInitializedVarsInNewRunspaces" + Severity = "warning" # because we need to prevent ParseErrors from being reported, so 'workflow' keyword will not be flagged when running test on Pwsh. } Describe "AvoidUnInitializedVarsInNewRunspaces" { Context "Should detect something" { $testCases = @( + # Foreach-Object -Parallel {} @{ Description = "Foreach-Object -Parallel with undeclared var" ScriptBlock = '{ @@ -12,31 +14,84 @@ Describe "AvoidUnInitializedVarsInNewRunspaces" { }' } @{ - Description = "alias foreach -parallel with undeclared var" + Description = "foreach -parallel alias with undeclared var" ScriptBlock = '{ 1..2 | ForEach -Parallel { $var } }' } @{ - Description = "alias % -parallel with undeclared var" + Description = "% -parallel alias with undeclared var" ScriptBlock = '{ 1..2 | % -Parallel { $var } }' } @{ - Description = "abbreviated param Foreach-Object -pa with undeclared var" + Description = "Foreach-Object -pa abbreviated param with undeclared var" ScriptBlock = '{ 1..2 | foreach-object -pa { $var } }' } @{ - Description = "Nested Foreach-Object -Parallel with undeclared var" + Description = "Foreach-Object -Parallel nested with undeclared var" ScriptBlock = '{ $myNestedScriptBlock = { 1..2 | ForEach-Object -Parallel { $var } } }' } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job without $using:' + ScriptBlock = '{ + $foo = "bar" + Start-Job {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob without $using:' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ + $foo = "bar" + workflow baz { InlineScript {$foo} } + }' + } + # Invoke-Command + @{ + Description = 'Invoke-Command with -ComputerName' + ScriptBlock = '{ + Invoke-Command -ScriptBlock {Write-Output $foo} -ComputerName "bar" + }' + } + @{ + Description = 'Invoke-Command with two different sessions, where var is declared in wrong session' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + $otherSession = new-PSSession -ComputerName "bar" + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + Invoke-Command -session $otherSession -ScriptBlock {Write-Output $foo} + }' + } ) it "should emit for: " -TestCases $testCases { @@ -67,9 +122,68 @@ Describe "AvoidUnInitializedVarsInNewRunspaces" { }' } @{ - Description = "Foreach-Object -Parallel with built-in var '`$Args' inside" + Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" + ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel{ $PSBoundParameters } + }' + } + @{ + Description = "Foreach-Object -Parallel with vars in other parameters" + ScriptBlock = '{ + $foo = "bar" + ForEach-Object -Parallel {$_} -InputObject $foo + }' + } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job with $using:' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with $using:' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ + $foo = "bar" + workflow baz { InlineScript {$using:foo} } + }' + } + # Invoke-Command + @{ + Description = 'Invoke-Command multiple, variable is declared in separate scriptblock, belonging to same session' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + Invoke-Command -session $session -ScriptBlock {Write-Output $foo} + }' + } + @{ + Description = 'Invoke-Command without -ComputerName' ScriptBlock = '{ - 1..2 | ForEach-Object { $Args[0] } -ArgumentList "a" -Parallel + Invoke-Command -ScriptBlock {Write-Output $foo} }' } ) From 36bb1fc1b3610b0a9b9263faa077f3b10322df38 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 24 Feb 2020 07:59:38 +0100 Subject: [PATCH 14/55] add rule implementation for InlineScript, Invoke-Command and Start-(Thread)Job --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 201 ++++++++++++++---- 1 file changed, 159 insertions(+), 42 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index 745d94aa4..cac7ccce1 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -44,6 +44,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) foreach (var scriptBlockAst in scriptBlockAsts) { var sbAst = scriptBlockAst as ScriptBlockAst; + foreach (var diagnosticRecord in AnalyzeScriptBlockAst(sbAst, fileName)) { yield return diagnosticRecord; @@ -57,7 +58,8 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) /// The name of this rule public string GetName() { - return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUnInitializedVarsInNewRunspacesName); + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), + Strings.AvoidUnInitializedVarsInNewRunspacesName); } /// @@ -111,56 +113,171 @@ public string GetSourceName() /// An enumerable containing diagnostic records private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scriptBlockAst, string fileName) { - // TODO: add other Cmdlets like invoke-command later? + var astsToProcess = new List(); + + var inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); + var jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); + jobCmdletNamesAndAliases.AddRange(Helper.Instance.CmdletNameAndAliases("Start-ThreadJob")); var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); + var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + + // - `Workflow bar {$foo = 'foo'; InlineScript {$using:foo} }` On Windows PowerShell only. (see get-help about_InlineScript) + astsToProcess.AddRange( + scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + inlineScriptCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase), + searchNestedScriptBlocks: false)); - // Find all commandAst objects for `Foreach-Object -Parallel`. As for parameter name matching, there are three + // - `Start - Job / Start - ThreadJob - scriptBlock {$using:foo <#'using' required#>} -InitializationScript {$bar <# no 'using' allowed #>}` + // Here the catch is, we need to be sure we check the right ScriptBlock. The rule does not apply to InitializationScript. + // Shortest unambiguous for for the parameter -InitializationScript is 'ini'. + astsToProcess.AddRange( + scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + jobCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + // we need to exclude the ScriptBlockExpression if it has a CommandParameterAst before it in the CommandElements collection which name starts with 'ini'. + !(commandAst.CommandElements[commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false)); + + // Find all ScriptBlockExpressionAst objects for `Foreach-Object -Parallel`. As for parameter name matching, there are three // parameters starting with a 'p': Parallel, PipelineVariable and Process, so we use startsWith 'pa' as the shortest unambiguous form. // Because we are already going trough all ScriptBlockAst objects, we do not need to look for nested script blocks here. - var foreachObjectParallelCommandAsts = scriptBlockAst.FindAll( - predicate: c => c is CommandAst commandAst && - foreachObjectCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false).Select(a=>a as CommandAst); + astsToProcess.AddRange( + scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + foreachObjectCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false)); + + //- `Invoke-Command -ComputerName "baz" -ScriptBlock {$using:foo}` + //The catch here, is that the `-ComputerName` parameter _has_ to be used, otherwise `$using` is prohibited. + //Also tricky; one can open a persistent session, and use subsequent `invoke-command` calls to that, where variables + //from previous calls are still valid: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_variables?view=powershell-7#using-remote-variables - foreach (var commandAst in foreachObjectParallelCommandAsts) + // Because -ComputerName and -Session parameters are in different parameter sets, we can treat them separately + // Shortest unambiguous for for the parameter -ComputerName is 'com'. + astsToProcess.AddRange(scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + invokeCommandCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false)); + + // Process invoke-command ScriptBlocks that belong to a session + var scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + invokeCommandCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("session", StringComparison.OrdinalIgnoreCase)), //TODO: find better way to discern between Session, SessionName and SessionOption parameters + searchNestedScriptBlocks: false); + + // Match ScriptBlocks together that belong to the same session + var sessionDictionary = new Dictionary>(); + foreach (var ast in scriptBlockExpressionAstsToAnalyze) { - if (commandAst == null) - yield break; - - // Find all variables that are assigned within this ScriptBlock - var varsInAssignments = commandAst.FindAll( - predicate: a => a is VariableExpressionAst varExpr && - varExpr.Parent is AssignmentStatementAst assignment && - assignment.Left.Equals(varExpr), - searchNestedScriptBlocks: true). - Select(a => a as VariableExpressionAst); - - // Find all variables that are not locally assigned, and don't have $using: directive - var nonAssignedNonUsingVars = commandAst.CommandElements. - SelectMany(a => a.FindAll( - predicate: aa => aa is VariableExpressionAst varAst && - !(varAst.Parent is UsingExpressionAst) && - !varsInAssignments.Contains(varAst) && - !Helper.Instance.HasSpecialVars(varAst.VariablePath.UserPath), - searchNestedScriptBlocks: true). - Select(aaa => aaa as VariableExpressionAst)); - - foreach (var variableExpression in nonAssignedNonUsingVars) + if (!(ast.Parent is CommandAst commandAst)) + continue; + + var sessionParameterAst = commandAst.CommandElements.First( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("session", StringComparison.OrdinalIgnoreCase)) as CommandParameterAst; + + if (sessionParameterAst == null) + continue; + + var sessionName = commandAst.CommandElements[commandAst.CommandElements.IndexOf(sessionParameterAst) + 1].Extent.Text.Trim(); + + if (sessionDictionary.ContainsKey(sessionName)) { - yield return new DiagnosticRecord( - message: string.Format(CultureInfo.CurrentCulture, - Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), - extent: variableExpression.Extent, - ruleName: GetName(), - severity: DiagnosticSeverity.Warning, - scriptPath: fileName, - ruleId: variableExpression.ToString()); + sessionDictionary[sessionName].Add(ast); + } + else + { + sessionDictionary.Add(sessionName, new List()); + sessionDictionary[sessionName].Add(ast); } } + + var nonAssignedNonUsingVars = new List(); + + foreach (var session in sessionDictionary) + { + // Find all variables that are assigned within these ScriptBlocks that are part of one session + List varsInAssignments = new List(); + foreach (var ast in session.Value) + { + varsInAssignments.AddRange(FindVarsInAssignmentAsts(ast)); + } + + // Find all variables that are not locally assigned, and don't have $using: scope modifier + foreach (var ast in session.Value) + { + nonAssignedNonUsingVars.AddRange(FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); + } + } + + + // Process rest of the Asts + foreach (var ast in astsToProcess) + { + if (ast == null) + continue; + + var varsInAssignments = FindVarsInAssignmentAsts(ast); + + nonAssignedNonUsingVars.AddRange(FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); + } + + foreach (var variableExpression in nonAssignedNonUsingVars) + { + if (variableExpression == null) + continue; + + yield return new DiagnosticRecord( + message: string.Format(CultureInfo.CurrentCulture, + Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), + extent: variableExpression.Extent, + ruleName: GetName(), + severity: DiagnosticSeverity.Warning, + scriptPath: fileName, + ruleId: variableExpression.ToString()); + } + } + + private static IEnumerable FindVarsInAssignmentAsts (Ast ast) + { + // Find all variables that are assigned within this ast + return ast.FindAll( + predicate: a => a is VariableExpressionAst varExpr && + varExpr.Parent is AssignmentStatementAst assignment && + assignment.Left.Equals(varExpr), + searchNestedScriptBlocks: true).Select(a => a as VariableExpressionAst); + } + + private static IEnumerable FindNonAssignedNonUsingVarAsts(Ast ast, IEnumerable varsInAssignments) + { + // Find all variables that are not locally assigned, and don't have $using: scope modifier + return ast.FindAll( + predicate: a => a is VariableExpressionAst varAst && + !(varAst.Parent is UsingExpressionAst) && + varsInAssignments.All(b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && + !Helper.Instance.HasSpecialVars(varAst.VariablePath.UserPath), + searchNestedScriptBlocks: true).Select(a => a as VariableExpressionAst); } } } From e7fd6ccf1c8810f486453ea15ced20e3f373cf5c Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 24 Feb 2020 18:53:19 +0100 Subject: [PATCH 15/55] Refactor and cleanup --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 269 +++++++++++------- 1 file changed, 173 insertions(+), 96 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index cac7ccce1..17376b2c7 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -114,68 +114,63 @@ public string GetSourceName() private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scriptBlockAst, string fileName) { var astsToProcess = new List(); + var nonAssignedNonUsingVars = new List(); - var inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); - var jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); - jobCmdletNamesAndAliases.AddRange(Helper.Instance.CmdletNameAndAliases("Start-ThreadJob")); - var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + astsToProcess.AddRange( + FindAllInlineScriptAsts(scriptBlockAst)); - // - `Workflow bar {$foo = 'foo'; InlineScript {$using:foo} }` On Windows PowerShell only. (see get-help about_InlineScript) astsToProcess.AddRange( - scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - inlineScriptCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase), - searchNestedScriptBlocks: false)); - - // - `Start - Job / Start - ThreadJob - scriptBlock {$using:foo <#'using' required#>} -InitializationScript {$bar <# no 'using' allowed #>}` - // Here the catch is, we need to be sure we check the right ScriptBlock. The rule does not apply to InitializationScript. - // Shortest unambiguous for for the parameter -InitializationScript is 'ini'. + FindAllStartJobAsts(scriptBlockAst)); + astsToProcess.AddRange( - scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - jobCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - // we need to exclude the ScriptBlockExpression if it has a CommandParameterAst before it in the CommandElements collection which name starts with 'ini'. - !(commandAst.CommandElements[commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false)); - - // Find all ScriptBlockExpressionAst objects for `Foreach-Object -Parallel`. As for parameter name matching, there are three - // parameters starting with a 'p': Parallel, PipelineVariable and Process, so we use startsWith 'pa' as the shortest unambiguous form. - // Because we are already going trough all ScriptBlockAst objects, we do not need to look for nested script blocks here. + FindAllForeachParallelAsts(scriptBlockAst)); + + // If -ComputerName or -Session is not specified, you cannot use a variable with a using: scope modifier. + // Also tricky; one can open a persistent session, and use subsequent `invoke-command` calls to that, where variables + // assigned in previous calls are still valid: + // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_variables?view=powershell-7#using-remote-variables + // Because -ComputerName and -Session parameters are in different parameter sets, we can treat them separately astsToProcess.AddRange( - scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - foreachObjectCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false)); - - //- `Invoke-Command -ComputerName "baz" -ScriptBlock {$using:foo}` - //The catch here, is that the `-ComputerName` parameter _has_ to be used, otherwise `$using` is prohibited. - //Also tricky; one can open a persistent session, and use subsequent `invoke-command` calls to that, where variables - //from previous calls are still valid: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_variables?view=powershell-7#using-remote-variables + FindAllInvokeCommandComputerAsts(scriptBlockAst)); + + // process Invoke-Command -Session separately + nonAssignedNonUsingVars.AddRange( + ProcessInvokeCommandSessionAsts(scriptBlockAst)); + + + foreach (var ast in astsToProcess) + { + if (ast == null) + continue; + + var varsInAssignments = FindVarsInAssignmentAsts(ast); + + nonAssignedNonUsingVars.AddRange( + FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); + } + + foreach (var variableExpression in nonAssignedNonUsingVars) + { + if (variableExpression == null) + continue; + + yield return new DiagnosticRecord( + message: string.Format(CultureInfo.CurrentCulture, + Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), + extent: variableExpression.Extent, + ruleName: GetName(), + severity: DiagnosticSeverity.Warning, + scriptPath: fileName, + ruleId: variableExpression.ToString()); + } + } + + private static IEnumerable ProcessInvokeCommandSessionAsts(ScriptBlockAst scriptBlockAst) + { + var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + var sessionDictionary = new Dictionary>(); + var result = new List(); - // Because -ComputerName and -Session parameters are in different parameter sets, we can treat them separately - // Shortest unambiguous for for the parameter -ComputerName is 'com'. - astsToProcess.AddRange(scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - invokeCommandCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false)); - - // Process invoke-command ScriptBlocks that belong to a session var scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && scriptBlockExpressionAst.Parent is CommandAst commandAst && @@ -183,24 +178,47 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("session", StringComparison.OrdinalIgnoreCase)), //TODO: find better way to discern between Session, SessionName and SessionOption parameters + parameterAst.ParameterName.StartsWith( + "session", //TODO: find better way to discern between Session, SessionName and SessionOption parameters + StringComparison + .OrdinalIgnoreCase)), searchNestedScriptBlocks: false); // Match ScriptBlocks together that belong to the same session - var sessionDictionary = new Dictionary>(); foreach (var ast in scriptBlockExpressionAstsToAnalyze) { if (!(ast.Parent is CommandAst commandAst)) continue; - - var sessionParameterAst = commandAst.CommandElements.First( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("session", StringComparison.OrdinalIgnoreCase)) as CommandParameterAst; - - if (sessionParameterAst == null) + + // Find session parameter + if (!(commandAst.CommandElements.First( + e => e is CommandParameterAst parameterAst && + parameterAst. + ParameterName. + StartsWith( + "session", //TODO: find better way to discern between Session, SessionName and SessionOption parameters + StringComparison + .OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) continue; - var sessionName = commandAst.CommandElements[commandAst.CommandElements.IndexOf(sessionParameterAst) + 1].Extent.Text.Trim(); + // Extract session name from session parameter + string sessionName; + try + { + sessionName = commandAst + .CommandElements[ + commandAst + .CommandElements + .IndexOf(sessionParameterAst) + 1] + .Extent + .Text + .Trim(); + } + catch + { + // When a session name is not present, something is definitely wrong. In any case, we don't want to analyze further. + continue; + } if (sessionDictionary.ContainsKey(sessionName)) { @@ -213,50 +231,102 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && } } - var nonAssignedNonUsingVars = new List(); foreach (var session in sessionDictionary) { // Find all variables that are assigned within these ScriptBlocks that are part of one session - List varsInAssignments = new List(); + var varsInAssignments = new List(); foreach (var ast in session.Value) { - varsInAssignments.AddRange(FindVarsInAssignmentAsts(ast)); + varsInAssignments.AddRange( + FindVarsInAssignmentAsts(ast)); } // Find all variables that are not locally assigned, and don't have $using: scope modifier foreach (var ast in session.Value) { - nonAssignedNonUsingVars.AddRange(FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); + result.AddRange( + FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); } } + return result; + } - // Process rest of the Asts - foreach (var ast in astsToProcess) - { - if (ast == null) - continue; + private static IEnumerable FindAllInvokeCommandComputerAsts(ScriptBlockAst scriptBlockAst) + { + var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - var varsInAssignments = FindVarsInAssignmentAsts(ast); - - nonAssignedNonUsingVars.AddRange(FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); - } + // Process Invoke-Command ScriptBlocks that do not belong to a session, but have the -ComputerName parameter. + // The shortest unambiguous for for the parameter -ComputerName is 'com'. + return scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + invokeCommandCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false); + } - foreach (var variableExpression in nonAssignedNonUsingVars) - { - if (variableExpression == null) - continue; + private static IEnumerable FindAllForeachParallelAsts(ScriptBlockAst scriptBlockAst) + { + var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - yield return new DiagnosticRecord( - message: string.Format(CultureInfo.CurrentCulture, - Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), - extent: variableExpression.Extent, - ruleName: GetName(), - severity: DiagnosticSeverity.Warning, - scriptPath: fileName, - ruleId: variableExpression.ToString()); - } + // The shortest unambiguous form for the parameter -Parallel is 'pa'. + return scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + foreachObjectCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), + searchNestedScriptBlocks: false); + } + + private static IEnumerable FindAllStartJobAsts(ScriptBlockAst scriptBlockAst) + { + var jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); + jobCmdletNamesAndAliases.AddRange(Helper.Instance.CmdletNameAndAliases("Start-ThreadJob")); + + // We need to be sure we check the right ScriptBlock. The rule does not apply to the -InitializationScript ScriptBlock. + // The shortest unambiguous for for the parameter -InitializationScript is 'ini + return scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + jobCmdletNamesAndAliases.Contains( + commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + // we need to exclude the ScriptBlockExpression if it has a CommandParameterAst before it in + // the CommandElements collection which name starts with 'ini'. + !(commandAst + .CommandElements[commandAst + .CommandElements + .IndexOf(scriptBlockExpressionAst) - 1] is CommandParameterAst parameterAst && + parameterAst + .ParameterName + .StartsWith( + "ini", + StringComparison + .OrdinalIgnoreCase)), + searchNestedScriptBlocks: false); + } + + private static IEnumerable FindAllInlineScriptAsts(ScriptBlockAst scriptBlockAst) + { + var inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); + + return scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && + scriptBlockExpressionAst.Parent is CommandAst commandAst && + inlineScriptCmdletNamesAndAliases + .Contains( + commandAst + .GetCommandName(), + StringComparer + .OrdinalIgnoreCase), + searchNestedScriptBlocks: false); } private static IEnumerable FindVarsInAssignmentAsts (Ast ast) @@ -265,8 +335,11 @@ private static IEnumerable FindVarsInAssignmentAsts (Ast return ast.FindAll( predicate: a => a is VariableExpressionAst varExpr && varExpr.Parent is AssignmentStatementAst assignment && - assignment.Left.Equals(varExpr), - searchNestedScriptBlocks: true).Select(a => a as VariableExpressionAst); + assignment + .Left + .Equals(varExpr), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst); } private static IEnumerable FindNonAssignedNonUsingVarAsts(Ast ast, IEnumerable varsInAssignments) @@ -275,9 +348,13 @@ private static IEnumerable FindNonAssignedNonUsingVarAsts return ast.FindAll( predicate: a => a is VariableExpressionAst varAst && !(varAst.Parent is UsingExpressionAst) && - varsInAssignments.All(b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && - !Helper.Instance.HasSpecialVars(varAst.VariablePath.UserPath), - searchNestedScriptBlocks: true).Select(a => a as VariableExpressionAst); + varsInAssignments.All( + b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && + !Helper + .Instance + .HasSpecialVars(varAst.VariablePath.UserPath), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst); } } } From 59ba18bca693dcd4d6c51c54d95b654ad24b6fef Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Mon, 24 Feb 2020 18:58:39 +0100 Subject: [PATCH 16/55] small cleanup --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index 17376b2c7..c4e660422 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -170,16 +170,17 @@ private static IEnumerable ProcessInvokeCommandSessionAst var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); var sessionDictionary = new Dictionary>(); var result = new List(); - + + // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. var scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && scriptBlockExpressionAst.Parent is CommandAst commandAst && - invokeCommandCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && + invokeCommandCmdletNamesAndAliases + .Contains(commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith( - "session", //TODO: find better way to discern between Session, SessionName and SessionOption parameters + parameterAst.ParameterName.Equals( + "session", StringComparison .OrdinalIgnoreCase)), searchNestedScriptBlocks: false); @@ -189,14 +190,15 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && { if (!(ast.Parent is CommandAst commandAst)) continue; - + // Find session parameter + // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. if (!(commandAst.CommandElements.First( e => e is CommandParameterAst parameterAst && parameterAst. ParameterName. - StartsWith( - "session", //TODO: find better way to discern between Session, SessionName and SessionOption parameters + Equals( + "session", StringComparison .OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) continue; @@ -342,7 +344,8 @@ varExpr.Parent is AssignmentStatementAst assignment && .Select(a => a as VariableExpressionAst); } - private static IEnumerable FindNonAssignedNonUsingVarAsts(Ast ast, IEnumerable varsInAssignments) + private static IEnumerable FindNonAssignedNonUsingVarAsts( + Ast ast, IEnumerable varsInAssignments) { // Find all variables that are not locally assigned, and don't have $using: scope modifier return ast.FindAll( From f64c81c1b52b75577b58b1c2228190335ff55668 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 27 Feb 2020 08:28:18 +0100 Subject: [PATCH 17/55] explain all applicable situations in documentation --- .../AvoidUnInitializedVarsInNewRunspaces.md | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md b/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md index bc45e10a0..71bc2aa61 100644 --- a/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md +++ b/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md @@ -4,24 +4,60 @@ ## Description -If a scriptblock is intended to be run as a new runspace, variables inside it should use $using: directive, or be initialized within the scriptblock. +If a ScriptBlock is intended to be run in a new RunSpace, variables inside it should use $using: scope modifier, or be initialized within the ScriptBlock. +This applies to: + +- Invoke-Command * +- Workflow { InlineScript {}} +- Foreach-Object ** +- Start-(Thread)Job + +\* Only with the -ComputerName or -Session parameter. +\*\* Only with the -Parallel parameter ## How to Fix -Within `Foreach-Object -Parallel {}`, instead of just using a variable from the parent scope, you have to use the `using:` directive: +Within the ScriptBlock, instead of just using a variable from the parent scope, you have to add the `using:` scope modifier to it. ## Example ### Wrong -``````PowerShell +```PowerShell $var = "foo" 1..2 | ForEach-Object -Parallel { $var } -`````` +``` ### Correct -``````PowerShell +```PowerShell $var = "foo" 1..2 | ForEach-Object -Parallel { $using:var } -`````` \ No newline at end of file +``` + +## More correct examples + +```powershell +$bar = "bar" +Invoke-Command -ComputerName "foo" -ScriptBlock { $using:bar } +``` + +```powershell +$bar = "bar" +$s = New-PSSession -ComputerName "foo" +Invoke-Command -Session $s -ScriptBlock { $using:bar } +``` + +```powershell +# Remark: Workflow is supported on Windows PowerShell only +Workflow { + $foo = "foo" + InlineScript { $using:foo } +} +``` + +```powershell +$foo = "foo" +Start-ThreadJob -ScriptBlock { $using:foo } +Start-Job -ScriptBlock {$using:foo } +``` \ No newline at end of file From db80c8fae21d219472cef651f0ed1ab377412033 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 27 Feb 2020 08:30:18 +0100 Subject: [PATCH 18/55] simplify code for adding sessions to dict --- Rules/AvoidUnInitializedVarsInNewRunspaces.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs index c4e660422..122f4f732 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/AvoidUnInitializedVarsInNewRunspaces.cs @@ -222,15 +222,10 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && continue; } - if (sessionDictionary.ContainsKey(sessionName)) - { - sessionDictionary[sessionName].Add(ast); - } - else - { - sessionDictionary.Add(sessionName, new List()); - sessionDictionary[sessionName].Add(ast); - } + if (!sessionDictionary.ContainsKey(sessionName)) + sessionDictionary.Add(sessionName, new List()); + + sessionDictionary[sessionName].Add(ast); } From d1f8df87ccf0885ca8831c7a8f9596767979617d Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 27 Feb 2020 09:12:12 +0100 Subject: [PATCH 19/55] Revert "Add RuleToTest parameter to Test-ScriptAnalyzer to aid with test driven rule development" This reverts commit 99c2bea8feb34fcaddee2a8658a921549e9a202b. Undo -RuleToTest param --- build.psm1 | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/build.psm1 b/build.psm1 index 8668e78f2..54b5d426b 100644 --- a/build.psm1 +++ b/build.psm1 @@ -289,14 +289,7 @@ function Start-ScriptAnalyzerBuild function Test-ScriptAnalyzer { [CmdletBinding()] - param ( - [Parameter()] - [switch]$InProcess, - - [switch]$ShowAll, - - [string]$RuleToTest - ) + param ( [Parameter()][switch]$InProcess, [switch]$ShowAll ) END { # versions 3 and 4 don't understand versioned module paths, so we need to rename the directory of the version to @@ -326,12 +319,7 @@ function Test-ScriptAnalyzer $testModulePath = Join-Path "${projectRoot}" -ChildPath out } $testResultsFile = "'$(Join-Path ${projectRoot} -childPath TestResults.xml)'" - if ($RuleToTest) { - $testScripts = Get-Childitem -Path "${projectRoot}\Tests\Rules" -Filter *$RuleToTest*.tests.ps1 | Select-Object -ExpandProperty FullName - } - else { - $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" - } + $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" try { if ( $major -lt 5 ) { Rename-Item $script:destinationDir ${testModulePath} From c53b65d8f750e397efcd46d54e83260d106911a5 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 27 Feb 2020 10:39:54 +0100 Subject: [PATCH 20/55] Rename rule to UseUsingScopeModifierInNewRunspaces Because this has a posive ring to it and points the user to the solution --- RuleDocumentation/README.md | 2 +- ...=> UseUsingScopeModifierInNewRunspaces.md} | 2 +- Rules/Strings.Designer.cs | 72 +++++++++---------- Rules/Strings.resx | 16 ++--- ...=> UseUsingScopeModifierInNewRunspaces.cs} | 12 ++-- ...singScopeModifierInNewRunspaces.tests.ps1} | 4 +- 6 files changed, 54 insertions(+), 54 deletions(-) rename RuleDocumentation/{AvoidUnInitializedVarsInNewRunspaces.md => UseUsingScopeModifierInNewRunspaces.md} (92%) rename Rules/{AvoidUnInitializedVarsInNewRunspaces.cs => UseUsingScopeModifierInNewRunspaces.cs} (94%) rename Tests/Rules/{AvoidUnInitializedVarsInNewRunspaces.tests.ps1 => UseUsingScopeModifierInNewRunspaces.tests.ps1} (96%) diff --git a/RuleDocumentation/README.md b/RuleDocumentation/README.md index d8e11c7b3..2ed360e88 100644 --- a/RuleDocumentation/README.md +++ b/RuleDocumentation/README.md @@ -16,7 +16,7 @@ |[AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | | |[AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | | |[AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | | -|[AvoidUnInitializedVarsInNewRunspaces](./AvoidUnInitializedVarsInNewRunspaces.md) | Warning | | +|[UseUsingScopeModifierInNewRunspaces](./UseUsingScopeModifierInNewRunspaces.md) | Warning | | |[AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | |[AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | | |[AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | | diff --git a/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md b/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md similarity index 92% rename from RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md rename to RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md index 71bc2aa61..e8590d075 100644 --- a/RuleDocumentation/AvoidUnInitializedVarsInNewRunspaces.md +++ b/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md @@ -1,4 +1,4 @@ -# AvoidUnInitializedVarsInNewRunspaces +# UseUsingScopeModifierInNewRunspaces **Severity Level: Warning** diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 3796dc50a..857f1c7ba 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -618,42 +618,6 @@ internal static string AvoidTrailingWhitespaceName { } } - /// - /// Looks up a localized string similar to Use '$using:' scope modifier in runspace scriptblocks. - /// - internal static string AvoidUnInitializedVarsInNewRunspacesCommonName { - get { - return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesCommonName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new runspace, variables inside it should use '$using:' scope modifier, or be initialized within the ScriptBlock.. - /// - internal static string AvoidUnInitializedVarsInNewRunspacesDescription { - get { - return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' scope modifier.. - /// - internal static string AvoidUnInitializedVarsInNewRunspacesError { - get { - return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AvoidUnInitializedVarsInNewRunspaces. - /// - internal static string AvoidUnInitializedVarsInNewRunspacesName { - get { - return ResourceManager.GetString("AvoidUnInitializedVarsInNewRunspacesName", resourceCulture); - } - } - /// /// Looks up a localized string similar to Module Must Be Loadable. /// @@ -3039,6 +3003,42 @@ internal static string UseTypeAtVariableAssignmentName { } } + /// + /// Looks up a localized string similar to Use 'Using:' scope modifier in RunSpace ScriptBlocks. + /// + internal static string UseUsingScopeModifierInNewRunspacesCommonName { + get { + return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock.. + /// + internal static string UseUsingScopeModifierInNewRunspacesDescription { + get { + return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier.. + /// + internal static string UseUsingScopeModifierInNewRunspacesError { + get { + return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UseUsingScopeModifierInNewRunspaces. + /// + internal static string UseUsingScopeModifierInNewRunspacesName { + get { + return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use UTF8 Encoding For Help File. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 37f959b1a..9e238c439 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1122,16 +1122,16 @@ ReviewUnusedParameter - - Use '$using:' scope modifier in runspace scriptblocks + + Use 'Using:' scope modifier in RunSpace ScriptBlocks - - If a ScriptBlock is intended to be run as a new runspace, variables inside it should use '$using:' scope modifier, or be initialized within the ScriptBlock. + + If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock. - - AvoidUnInitializedVarsInNewRunspaces + + UseUsingScopeModifierInNewRunspaces - - The variable '{0}' is not declared within this ScriptBlock, and is missing the '$using:' scope modifier. + + The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier. \ No newline at end of file diff --git a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs similarity index 94% rename from Rules/AvoidUnInitializedVarsInNewRunspaces.cs rename to Rules/UseUsingScopeModifierInNewRunspaces.cs index 122f4f732..74da04064 100644 --- a/Rules/AvoidUnInitializedVarsInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -15,12 +15,12 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules { /// - /// AvoidUnInitializedVarsInNewRunspaces: Analyzes the ast to check that variables in script blocks that run in new run spaces are properly initialized or passed in with '$using:(varName)'. + /// UseUsingScopeModifierInNewRunspaces: Analyzes the ast to check that variables in script blocks that run in new run spaces are properly initialized or passed in with '$using:(varName)'. /// #if !CORECLR [Export(typeof(IScriptRule))] #endif - public class AvoidUnInitializedVarsInNewRunspaces : IScriptRule + public class UseUsingScopeModifierInNewRunspaces : IScriptRule { /// /// AnalyzeScript: Analyzes the ast to check variables in script blocks that will run in new runspaces are properly initialized or passed in with $using: @@ -59,7 +59,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) public string GetName() { return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), - Strings.AvoidUnInitializedVarsInNewRunspacesName); + Strings.UseUsingScopeModifierInNewRunspacesName); } /// @@ -68,7 +68,7 @@ public string GetName() /// The common name of this rule public string GetCommonName() { - return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUnInitializedVarsInNewRunspacesCommonName); + return string.Format(CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesCommonName); } /// @@ -77,7 +77,7 @@ public string GetCommonName() /// The description of this rule public string GetDescription() { - return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUnInitializedVarsInNewRunspacesDescription); + return string.Format(CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesDescription); } /// @@ -156,7 +156,7 @@ private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scrip yield return new DiagnosticRecord( message: string.Format(CultureInfo.CurrentCulture, - Strings.AvoidUnInitializedVarsInNewRunspacesError, variableExpression.ToString()), + Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), extent: variableExpression.Extent, ruleName: GetName(), severity: DiagnosticSeverity.Warning, diff --git a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 similarity index 96% rename from Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 rename to Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 499718d08..20d90930b 100644 --- a/Tests/Rules/AvoidUnInitializedVarsInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -1,9 +1,9 @@ $settings = @{ - IncludeRules = "PSAvoidUnInitializedVarsInNewRunspaces" + IncludeRules = "PSUseUsingScopeModifierInNewRunspaces" Severity = "warning" # because we need to prevent ParseErrors from being reported, so 'workflow' keyword will not be flagged when running test on Pwsh. } -Describe "AvoidUnInitializedVarsInNewRunspaces" { +Describe "UseUsingScopeModifierInNewRunspaces" { Context "Should detect something" { $testCases = @( # Foreach-Object -Parallel {} From 15bd608525c0e0e0462d38861001dd40dad005d2 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 27 Feb 2020 14:39:23 +0100 Subject: [PATCH 21/55] refactor grouping of script blocks by session name --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 30 +++++++------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 74da04064..f1ba2c4a4 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -203,28 +203,20 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && .OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) continue; - // Extract session name from session parameter - string sessionName; - try - { - sessionName = commandAst - .CommandElements[ - commandAst - .CommandElements - .IndexOf(sessionParameterAst) + 1] - .Extent - .Text - .Trim(); - } - catch - { - // When a session name is not present, something is definitely wrong. In any case, we don't want to analyze further. + // Group script blocks by session name in a dictionary + var sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); + if (commandAst.CommandElements.Count <= sessionParamAstIndex) continue; - } + + var sessionName = commandAst + .CommandElements[sessionParamAstIndex + 1] + .Extent + .Text + .Trim(); if (!sessionDictionary.ContainsKey(sessionName)) - sessionDictionary.Add(sessionName, new List()); - + sessionDictionary.Add(sessionName, new List()); + sessionDictionary[sessionName].Add(ast); } From 37a5cb12fe36d6c345ea2a017d06bb9998e5117e Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Wed, 4 Mar 2020 20:24:13 +0100 Subject: [PATCH 22/55] Add suggested correction implementation --- Rules/Strings.Designer.cs | 9 +++++++ Rules/Strings.resx | 3 +++ Rules/UseUsingScopeModifierInNewRunspaces.cs | 27 +++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 857f1c7ba..4b253c71a 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -3012,6 +3012,15 @@ internal static string UseUsingScopeModifierInNewRunspacesCommonName { } } + /// + /// Looks up a localized string similar to Replace {0} with {1}. + /// + internal static string UseUsingScopeModifierInNewRunspacesCorrectionDescription { + get { + return ResourceManager.GetString("UseUsingScopeModifierInNewRunspacesCorrectionDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock.. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 9e238c439..7cfe0186b 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1134,4 +1134,7 @@ The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier. + + Replace {0} with {1} + \ No newline at end of file diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index f1ba2c4a4..a362480ed 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -161,10 +161,35 @@ private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scrip ruleName: GetName(), severity: DiagnosticSeverity.Warning, scriptPath: fileName, - ruleId: variableExpression.ToString()); + ruleId: variableExpression.ToString(), + suggestedCorrections: GetSuggestedCorrections(ast: variableExpression)); } } + private static IEnumerable GetSuggestedCorrections(Ast ast) + { + var varWithUsing = "$using:" + ast.Extent.Text.TrimStart('$'); + var description = string.Format( + CultureInfo.CurrentCulture, + Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, + ast.Extent.Text, + varWithUsing); + var corrections = new List() + { + new CorrectionExtent( + startLineNumber: ast.Extent.StartLineNumber, + endLineNumber: ast.Extent.EndLineNumber, + startColumnNumber: ast.Extent.StartColumnNumber, + endColumnNumber: ast.Extent.EndColumnNumber, + text: varWithUsing, + file: ast.Extent.File, + description: description + ) + }; + + return corrections; + } + private static IEnumerable ProcessInvokeCommandSessionAsts(ScriptBlockAst scriptBlockAst) { var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); From 6024c504ba5894822a884b8230f28e3153dea1da Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 7 Mar 2020 21:27:12 +0100 Subject: [PATCH 23/55] Add test for suggested corrections, fix typos --- .../UseUsingScopeModifierInNewRunspaces.tests.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 20d90930b..2688d4c97 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -94,11 +94,20 @@ Describe "UseUsingScopeModifierInNewRunspaces" { } ) - it "should emit for: " -TestCases $testCases { + It "should emit for: " -TestCases $testCases { param($Description, $ScriptBlock) [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings $warnings.Count | Should -Be 1 } + + It "should emit suggested correction" { + $ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel { $var } + }' + $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + + $warnings[0].SuggestedCorrections[0].Text | Should -Be '$using:var' + } } Context "Should not detect anything" { @@ -188,7 +197,7 @@ Describe "UseUsingScopeModifierInNewRunspaces" { } ) - it "should not emit anything for: " -TestCases $testCases { + It "should not emit anything for: " -TestCases $testCases { param($Description, $ScriptBlock) [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings $warnings.Count | Should -Be 0 From b12d16f37eebcdf9b17cf9ec396627af33273124 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 7 Mar 2020 21:29:11 +0100 Subject: [PATCH 24/55] Refactor to AstVisitor/AstVisitor2 WIP TODO: finish refactor for `Invoke-Command -Session` logic --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 307 +++++++++---------- 1 file changed, 138 insertions(+), 169 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a362480ed..33f6aa85b 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -30,26 +30,10 @@ public class UseUsingScopeModifierInNewRunspaces : IScriptRule /// A List of results from this rule public IEnumerable AnalyzeScript(Ast ast, string fileName) { - if (ast == null) - { - throw new ArgumentNullException(Strings.NullAstErrorMessage); - } - - var scriptBlockAsts = ast.FindAll(x => x is ScriptBlockAst, true); - if (scriptBlockAsts == null) - { - yield break; - } - - foreach (var scriptBlockAst in scriptBlockAsts) - { - var sbAst = scriptBlockAst as ScriptBlockAst; - - foreach (var diagnosticRecord in AnalyzeScriptBlockAst(sbAst, fileName)) - { - yield return diagnosticRecord; - } - } + + var visitor = new SyntaxCompatibilityVisitor(this, fileName); + ast.Visit(visitor); + return visitor.GetDiagnosticRecords(); } /// @@ -104,71 +88,159 @@ public string GetSourceName() { return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); } + } - /// - /// Checks if a variable is initialized and referenced in either its assignment or children scopes - /// - /// Ast of type ScriptBlock - /// Name of file containing the ast - /// An enumerable containing diagnostic records - private IEnumerable AnalyzeScriptBlockAst(ScriptBlockAst scriptBlockAst, string fileName) - { - var astsToProcess = new List(); - var nonAssignedNonUsingVars = new List(); +#if !(PSV3 || PSV4) + internal class SyntaxCompatibilityVisitor : AstVisitor2 +#else + private class SyntaxCompatibilityVisitor : AstVisitor +#endif + { + // Is there a way to make sure this is only called when needed? + private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); - astsToProcess.AddRange( - FindAllInlineScriptAsts(scriptBlockAst)); + private readonly IEnumerable _threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); - astsToProcess.AddRange( - FindAllStartJobAsts(scriptBlockAst)); + private readonly IEnumerable _inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); - astsToProcess.AddRange( - FindAllForeachParallelAsts(scriptBlockAst)); + private readonly IEnumerable _foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - // If -ComputerName or -Session is not specified, you cannot use a variable with a using: scope modifier. - // Also tricky; one can open a persistent session, and use subsequent `invoke-command` calls to that, where variables - // assigned in previous calls are still valid: - // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_variables?view=powershell-7#using-remote-variables - // Because -ComputerName and -Session parameters are in different parameter sets, we can treat them separately - astsToProcess.AddRange( - FindAllInvokeCommandComputerAsts(scriptBlockAst)); + private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - // process Invoke-Command -Session separately - nonAssignedNonUsingVars.AddRange( - ProcessInvokeCommandSessionAsts(scriptBlockAst)); + private readonly UseUsingScopeModifierInNewRunspaces _rule; + private readonly List _diagnosticAccumulator; + + private readonly List _nonAssignedNonUsingVars = new List(); + + private readonly string _analyzedFilePath; - foreach (var ast in astsToProcess) - { - if (ast == null) - continue; + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; - var varsInAssignments = FindVarsInAssignmentAsts(ast); - - nonAssignedNonUsingVars.AddRange( - FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); - } + + public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, string analyzedScriptPath) + { + _diagnosticAccumulator = new List(); + _rule = rule; + _analyzedFilePath = analyzedScriptPath; + } + + public IEnumerable GetDiagnosticRecords() + { + return _diagnosticAccumulator; + } + + public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + if (!(scriptBlockExpressionAst.Parent is CommandAst commandAst)) + return AstVisitAction.Continue; + + var cmdName = commandAst.GetCommandName(); + var scriptBlockParameterAst = commandAst.CommandElements[ + commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as + CommandParameterAst; + + if (!IsInlineScriptBlock(cmdName) && + !IsJobScriptBlock(cmdName, scriptBlockParameterAst) && + !IsForeachScriptBlock(cmdName, scriptBlockParameterAst) && + !IsInvokeCommandComputerScriptBlock(cmdName, commandAst) && + !IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) + return AstVisitAction.Continue; + + AnalyzeScriptBlock(scriptBlockExpressionAst); - foreach (var variableExpression in nonAssignedNonUsingVars) + return AstVisitAction.SkipChildren; + } + + private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) + { + //TODO: finish refactor of invoke-command -session stuff + + //nonAssignedNonUsingVarAsts.AddRange( + // ProcessInvokeCommandSessionAsts(scriptBlockExpressionAst)); + return false; + } + + private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) + { + return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); + } + + private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + (scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); + } + + private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || + _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && + !(scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); + } + + private bool IsInlineScriptBlock(string cmdName) + { + return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); + } + + private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + var nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); + + foreach (var variableExpression in nonAssignedNonUsingVarAsts) { if (variableExpression == null) continue; - yield return new DiagnosticRecord( + _diagnosticAccumulator.Add(new DiagnosticRecord( message: string.Format(CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), extent: variableExpression.Extent, - ruleName: GetName(), - severity: DiagnosticSeverity.Warning, - scriptPath: fileName, + ruleName: _rule.GetName(), + severity: Severity, + scriptPath: _analyzedFilePath, ruleId: variableExpression.ToString(), - suggestedCorrections: GetSuggestedCorrections(ast: variableExpression)); + suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); } } - private static IEnumerable GetSuggestedCorrections(Ast ast) + private static IEnumerable FindVarsInAssignmentAsts(Ast ast) + { + // Find all variables that are assigned within this ast + return ast.FindAll( + predicate: a => a is VariableExpressionAst varExpr && + varExpr.Parent is AssignmentStatementAst assignment && + assignment + .Left + .Equals(varExpr), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst); + } + + private static IEnumerable FindNonAssignedNonUsingVarAsts( + Ast ast, IEnumerable varsInAssignments) + { + // Find all variables that are not locally assigned, and don't have $using: scope modifier + return ast.FindAll( + predicate: a => a is VariableExpressionAst varAst && + !(varAst.Parent is UsingExpressionAst) && + varsInAssignments.All( + b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && + !Helper + .Instance + .HasSpecialVars(varAst.VariablePath.UserPath), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst); + } + + private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) { - var varWithUsing = "$using:" + ast.Extent.Text.TrimStart('$'); + var varWithUsing = "$using:" + ast.VariablePath.UserPath; var description = string.Format( CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, @@ -190,8 +262,10 @@ private static IEnumerable GetSuggestedCorrections(Ast ast) return corrections; } - private static IEnumerable ProcessInvokeCommandSessionAsts(ScriptBlockAst scriptBlockAst) + private static IEnumerable ProcessInvokeCommandSessionAsts(Ast scriptBlockAst) { + // TODO: finish refactor to reanimate this logic + var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); var sessionDictionary = new Dictionary>(); var result = new List(); @@ -266,110 +340,5 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && return result; } - - private static IEnumerable FindAllInvokeCommandComputerAsts(ScriptBlockAst scriptBlockAst) - { - var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - - // Process Invoke-Command ScriptBlocks that do not belong to a session, but have the -ComputerName parameter. - // The shortest unambiguous for for the parameter -ComputerName is 'com'. - return scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - invokeCommandCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false); - } - - private static IEnumerable FindAllForeachParallelAsts(ScriptBlockAst scriptBlockAst) - { - var foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - - // The shortest unambiguous form for the parameter -Parallel is 'pa'. - return scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - foreachObjectCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)), - searchNestedScriptBlocks: false); - } - - private static IEnumerable FindAllStartJobAsts(ScriptBlockAst scriptBlockAst) - { - var jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); - jobCmdletNamesAndAliases.AddRange(Helper.Instance.CmdletNameAndAliases("Start-ThreadJob")); - - // We need to be sure we check the right ScriptBlock. The rule does not apply to the -InitializationScript ScriptBlock. - // The shortest unambiguous for for the parameter -InitializationScript is 'ini - return scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - jobCmdletNamesAndAliases.Contains( - commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - // we need to exclude the ScriptBlockExpression if it has a CommandParameterAst before it in - // the CommandElements collection which name starts with 'ini'. - !(commandAst - .CommandElements[commandAst - .CommandElements - .IndexOf(scriptBlockExpressionAst) - 1] is CommandParameterAst parameterAst && - parameterAst - .ParameterName - .StartsWith( - "ini", - StringComparison - .OrdinalIgnoreCase)), - searchNestedScriptBlocks: false); - } - - private static IEnumerable FindAllInlineScriptAsts(ScriptBlockAst scriptBlockAst) - { - var inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); - - return scriptBlockAst.FindAll( - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - inlineScriptCmdletNamesAndAliases - .Contains( - commandAst - .GetCommandName(), - StringComparer - .OrdinalIgnoreCase), - searchNestedScriptBlocks: false); - } - - private static IEnumerable FindVarsInAssignmentAsts (Ast ast) - { - // Find all variables that are assigned within this ast - return ast.FindAll( - predicate: a => a is VariableExpressionAst varExpr && - varExpr.Parent is AssignmentStatementAst assignment && - assignment - .Left - .Equals(varExpr), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst); - } - - private static IEnumerable FindNonAssignedNonUsingVarAsts( - Ast ast, IEnumerable varsInAssignments) - { - // Find all variables that are not locally assigned, and don't have $using: scope modifier - return ast.FindAll( - predicate: a => a is VariableExpressionAst varAst && - !(varAst.Parent is UsingExpressionAst) && - varsInAssignments.All( - b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && - !Helper - .Instance - .HasSpecialVars(varAst.VariablePath.UserPath), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst); - } } } From 9bc219507017491bfa1d9da950f2d692b841a4d0 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 7 Mar 2020 21:57:13 +0100 Subject: [PATCH 25/55] Update Rules/UseUsingScopeModifierInNewRunspaces.cs Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 33f6aa85b..a6f9330d6 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -266,7 +266,8 @@ private static IEnumerable ProcessInvokeCommandSessionAst { // TODO: finish refactor to reanimate this logic - var invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + IEnumerable invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + var sessionDictionary = new Dictionary>(); var result = new List(); From f8fa89a0dea35cd4c99cc2fbd8484de8b1932416 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 7 Mar 2020 21:57:30 +0100 Subject: [PATCH 26/55] Update Rules/UseUsingScopeModifierInNewRunspaces.cs Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a6f9330d6..f0eb31161 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -272,7 +272,8 @@ private static IEnumerable ProcessInvokeCommandSessionAst var result = new List(); // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. - var scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( + IEnumerable scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( + predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && scriptBlockExpressionAst.Parent is CommandAst commandAst && invokeCommandCmdletNamesAndAliases From 6ff34053e1b345a8fc1bec3d1f894cd93df03076 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 7 Mar 2020 22:09:20 +0100 Subject: [PATCH 27/55] Process review comments - Always use braces with if statements - Use ast.VariablePath.UserPath instead of ast.Extent.Text - simplify return list of corrections - add TODO's fo invoke-command-session code - Add TODO for commandAst.GetCommandName() can be null - Invert if for readability --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 60 +++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 33f6aa85b..ba06c353d 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -133,23 +133,28 @@ public IEnumerable GetDiagnosticRecords() public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { if (!(scriptBlockExpressionAst.Parent is CommandAst commandAst)) + { return AstVisitAction.Continue; + } + + //note: commandAst.GetCommandName() could be null, which will cause a crash here + // for example: { & $commandName }.Ast.EndBlock.Statements[0].PipelineElements[0].GetCommandName() ==> turn into test. var cmdName = commandAst.GetCommandName(); var scriptBlockParameterAst = commandAst.CommandElements[ - commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as - CommandParameterAst; - - if (!IsInlineScriptBlock(cmdName) && - !IsJobScriptBlock(cmdName, scriptBlockParameterAst) && - !IsForeachScriptBlock(cmdName, scriptBlockParameterAst) && - !IsInvokeCommandComputerScriptBlock(cmdName, commandAst) && - !IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) - return AstVisitAction.Continue; - - AnalyzeScriptBlock(scriptBlockExpressionAst); - - return AstVisitAction.SkipChildren; + commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; + + if (IsInlineScriptBlock(cmdName) || + IsJobScriptBlock(cmdName, scriptBlockParameterAst) || + IsForeachScriptBlock(cmdName, scriptBlockParameterAst) || + IsInvokeCommandComputerScriptBlock(cmdName, commandAst) || + IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) + { + AnalyzeScriptBlock(scriptBlockExpressionAst); + return AstVisitAction.SkipChildren; + } + + return AstVisitAction.Continue; } private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) @@ -172,14 +177,16 @@ private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst comma private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - (scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); + (scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); } private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && - !(scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); + !(scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); } private bool IsInlineScriptBlock(string cmdName) @@ -195,7 +202,9 @@ private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAs foreach (var variableExpression in nonAssignedNonUsingVarAsts) { if (variableExpression == null) + { continue; + } _diagnosticAccumulator.Add(new DiagnosticRecord( message: string.Format(CultureInfo.CurrentCulture, @@ -204,7 +213,7 @@ private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAs ruleName: _rule.GetName(), severity: Severity, scriptPath: _analyzedFilePath, - ruleId: variableExpression.ToString(), + ruleId: _rule.GetName(), suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); } } @@ -240,13 +249,14 @@ private static IEnumerable FindNonAssignedNonUsingVarAsts private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) { - var varWithUsing = "$using:" + ast.VariablePath.UserPath; + var varWithUsing = $"$using:{ast.VariablePath.UserPath}"; var description = string.Format( CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, ast.Extent.Text, varWithUsing); - var corrections = new List() + + return new[] { new CorrectionExtent( startLineNumber: ast.Extent.StartLineNumber, @@ -258,8 +268,6 @@ private static IEnumerable GetSuggestedCorrections(VariableExp description: description ) }; - - return corrections; } private static IEnumerable ProcessInvokeCommandSessionAsts(Ast scriptBlockAst) @@ -270,6 +278,10 @@ private static IEnumerable ProcessInvokeCommandSessionAst var sessionDictionary = new Dictionary>(); var result = new List(); + + //TODO: turn into a Visitor as well: + //note: commandAst.GetCommandName() can be null, which will cause a crash here + // for example: { & $commandName }.Ast.EndBlock.Statements[0].PipelineElements[0].GetCommandName() // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. var scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && @@ -288,7 +300,9 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && foreach (var ast in scriptBlockExpressionAstsToAnalyze) { if (!(ast.Parent is CommandAst commandAst)) + { continue; + } // Find session parameter // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. @@ -300,12 +314,16 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && "session", StringComparison .OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) + { continue; + } // Group script blocks by session name in a dictionary var sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); if (commandAst.CommandElements.Count <= sessionParamAstIndex) + { continue; + } var sessionName = commandAst .CommandElements[sessionParamAstIndex + 1] @@ -314,7 +332,9 @@ scriptBlockExpressionAst.Parent is CommandAst commandAst && .Trim(); if (!sessionDictionary.ContainsKey(sessionName)) + { sessionDictionary.Add(sessionName, new List()); + } sessionDictionary[sessionName].Add(ast); } From 675874877271dad239263d04103bf815b098e0fd Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Tue, 10 Mar 2020 07:06:15 +0100 Subject: [PATCH 28/55] Add tests for command name and icm -session --- ...eUsingScopeModifierInNewRunspaces.tests.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 2688d4c97..48fd800de 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -92,6 +92,14 @@ Describe "UseUsingScopeModifierInNewRunspaces" { Invoke-Command -session $otherSession -ScriptBlock {Write-Output $foo} }' } + @{ + Description = 'Invoke-Command with session, where var is declared after use' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + Invoke-Command -session $session -ScriptBlock {Write-Output $foo} + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + }' + } ) It "should emit for: " -TestCases $testCases { @@ -182,7 +190,7 @@ Describe "UseUsingScopeModifierInNewRunspaces" { } # Invoke-Command @{ - Description = 'Invoke-Command multiple, variable is declared in separate scriptblock, belonging to same session' + Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' ScriptBlock = '{ $session = new-PSSession -ComputerName "baz" Invoke-Command -session $session -ScriptBlock {$foo = "foo" } @@ -195,6 +203,14 @@ Describe "UseUsingScopeModifierInNewRunspaces" { Invoke-Command -ScriptBlock {Write-Output $foo} }' } + # Unsupported scenarios + @{ + Description = 'Rule should skip analysis when Command Name cannot be resolved' + ScriptBlock = '{ + $commandName = "Invoke-Command" + & $commandName -ComputerName -ScriptBlock { $foo } + }' + } ) It "should not emit anything for: " -TestCases $testCases { From e5e6a580cf6cbfe51346765fa0e435108a0bcbe7 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Tue, 10 Mar 2020 07:08:33 +0100 Subject: [PATCH 29/55] Add icm -session logic to visitor and tidy up --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 345 +++++++++++-------- 1 file changed, 196 insertions(+), 149 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 6ede262ff..71644d5fb 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; #if !CORECLR using System.ComponentModel.Composition; @@ -96,7 +95,8 @@ internal class SyntaxCompatibilityVisitor : AstVisitor2 private class SyntaxCompatibilityVisitor : AstVisitor #endif { - // Is there a way to make sure this is only called when needed? + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); private readonly IEnumerable _threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); @@ -107,17 +107,14 @@ private class SyntaxCompatibilityVisitor : AstVisitor private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - private readonly UseUsingScopeModifierInNewRunspaces _rule; - + private readonly Dictionary> _varsDeclaredPerSession = new Dictionary>(); + private readonly List _diagnosticAccumulator; - private readonly List _nonAssignedNonUsingVars = new List(); + private readonly UseUsingScopeModifierInNewRunspaces _rule; private readonly string _analyzedFilePath; - - private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; - - + public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, string analyzedScriptPath) { _diagnosticAccumulator = new List(); @@ -125,11 +122,22 @@ public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, stri _analyzedFilePath = analyzedScriptPath; } + /// + /// GetDiagnosticRecords: Retrieves all Diagnostic Records that were generated during visiting + /// public IEnumerable GetDiagnosticRecords() { return _diagnosticAccumulator; } + /// + /// VisitScriptBlockExpression: When a ScriptBlockExpression is visited, see if it belongs to a command that needs its variables + /// prefixed with the 'Using' scope modifier. If so, analyze the block and generate diagnostic records for variables where it is missing. + /// + /// + /// + /// AstVisitAction.Continue or AstVisitAction.SkipChildren, depending on what we found. Diagnostic records are saved in `_diagnosticAccumulator`. + /// public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { if (!(scriptBlockExpressionAst.Parent is CommandAst commandAst)) @@ -137,87 +145,51 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - //note: commandAst.GetCommandName() could be null, which will cause a crash here - // for example: { & $commandName }.Ast.EndBlock.Statements[0].PipelineElements[0].GetCommandName() ==> turn into test. - var cmdName = commandAst.GetCommandName(); + if (cmdName == null) + { + // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` + return AstVisitAction.Continue; + } + var scriptBlockParameterAst = commandAst.CommandElements[ commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; if (IsInlineScriptBlock(cmdName) || IsJobScriptBlock(cmdName, scriptBlockParameterAst) || IsForeachScriptBlock(cmdName, scriptBlockParameterAst) || - IsInvokeCommandComputerScriptBlock(cmdName, commandAst) || - IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) + IsInvokeCommandComputerScriptBlock(cmdName, commandAst)) { AnalyzeScriptBlock(scriptBlockExpressionAst); return AstVisitAction.SkipChildren; } - return AstVisitAction.Continue; - } - - private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) - { - //TODO: finish refactor of invoke-command -session stuff - - //nonAssignedNonUsingVarAsts.AddRange( - // ProcessInvokeCommandSessionAsts(scriptBlockExpressionAst)); - return false; - } - - private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) - { - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); - } - - private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) - { - return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - (scriptBlockParameterAst != null && - scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); - } - - private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) - { - return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || - _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && - !(scriptBlockParameterAst != null && - scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); - } - - private bool IsInlineScriptBlock(string cmdName) - { - return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); - } - - private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) - { - var nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, - FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); - - foreach (var variableExpression in nonAssignedNonUsingVarAsts) + if (IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) { - if (variableExpression == null) + var sessionName = GetSessionName(commandAst); + + var varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + if (varsInLocalAssignments != null) { - continue; + AddAssignedVarsToSession(sessionName, varsInLocalAssignments); } - _diagnosticAccumulator.Add(new DiagnosticRecord( - message: string.Format(CultureInfo.CurrentCulture, - Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), - extent: variableExpression.Extent, - ruleName: _rule.GetName(), - severity: Severity, - scriptPath: _analyzedFilePath, - ruleId: _rule.GetName(), - suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); + GenerateDiagnosticRecords( + FindNonAssignedNonUsingVarAsts( + scriptBlockExpressionAst, + GetAssignedVarsInSession(sessionName))); + + return AstVisitAction.SkipChildren; } + + return AstVisitAction.Continue; } + /// + /// FindVarsInAssignmentAsts: Retrieves all assigned variables from an Ast: + /// Example: `$foo = "foo"` ==> the VariableExpressionAst for $foo is returned + /// + /// private static IEnumerable FindVarsInAssignmentAsts(Ast ast) { // Find all variables that are assigned within this ast @@ -231,7 +203,15 @@ varExpr.Parent is AssignmentStatementAst assignment && .Select(a => a as VariableExpressionAst); } - private static IEnumerable FindNonAssignedNonUsingVarAsts( + /// + /// FindNonAssignedNonUsingVarAsts: Retrieve variables that are: + /// - not assigned before + /// - not prefixed with the 'Using' scope modifier + /// - not a PowerShell special variable + /// + /// + /// + private static List FindNonAssignedNonUsingVarAsts( Ast ast, IEnumerable varsInAssignments) { // Find all variables that are not locally assigned, and don't have $using: scope modifier @@ -244,9 +224,13 @@ private static IEnumerable FindNonAssignedNonUsingVarAsts .Instance .HasSpecialVars(varAst.VariablePath.UserPath), searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst); + .Select(a => a as VariableExpressionAst).ToList(); } + /// + /// GetSuggestedCorrections: Retrieves a CorrectionExtent for a given variable + /// + /// private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) { var varWithUsing = $"$using:{ast.VariablePath.UserPath}"; @@ -270,97 +254,160 @@ private static IEnumerable GetSuggestedCorrections(VariableExp }; } - private static IEnumerable ProcessInvokeCommandSessionAsts(Ast scriptBlockAst) + /// + /// GetSessionName: Retrieves the name of the session (that Invoke-Command is run with). + /// + /// + private static string GetSessionName(CommandAst commandAst) { - // TODO: finish refactor to reanimate this logic - - IEnumerable invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - - var sessionDictionary = new Dictionary>(); - var result = new List(); - - - //TODO: turn into a Visitor as well: - //note: commandAst.GetCommandName() can be null, which will cause a crash here - // for example: { & $commandName }.Ast.EndBlock.Statements[0].PipelineElements[0].GetCommandName() - // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. - IEnumerable scriptBlockExpressionAstsToAnalyze = scriptBlockAst.FindAll( - - predicate: a => a is ScriptBlockExpressionAst scriptBlockExpressionAst && - scriptBlockExpressionAst.Parent is CommandAst commandAst && - invokeCommandCmdletNamesAndAliases - .Contains(commandAst.GetCommandName(), StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.Equals( - "session", - StringComparison - .OrdinalIgnoreCase)), - searchNestedScriptBlocks: false); - - // Match ScriptBlocks together that belong to the same session - foreach (var ast in scriptBlockExpressionAstsToAnalyze) + if (!(commandAst.CommandElements.First( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.Equals( + "session", StringComparison.OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) { - if (!(ast.Parent is CommandAst commandAst)) - { - continue; - } - - // Find session parameter - // The shortest unambiguous name for parameter -Session is 'session' (SessionName and SessionOption) also exist. - if (!(commandAst.CommandElements.First( - e => e is CommandParameterAst parameterAst && - parameterAst. - ParameterName. - Equals( - "session", - StringComparison - .OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) - { - continue; - } + return ""; + } - // Group script blocks by session name in a dictionary - var sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); - if (commandAst.CommandElements.Count <= sessionParamAstIndex) - { - continue; - } + var sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); - var sessionName = commandAst - .CommandElements[sessionParamAstIndex + 1] - .Extent - .Text - .Trim(); + return commandAst + .CommandElements[sessionParamAstIndex + 1] + .Extent + .Text + .Trim(); + } - if (!sessionDictionary.ContainsKey(sessionName)) - { - sessionDictionary.Add(sessionName, new List()); - } + /// + /// GetAssignedVarsInSession: Retrieves all previously declared vars for a given session (as in Invoke-Command -Session $session). + /// + /// + private IEnumerable GetAssignedVarsInSession(string sessionName) + { + return _varsDeclaredPerSession[sessionName]; + } - sessionDictionary[sessionName].Add(ast); + /// + /// AddAssignedVarsToSession: Adds variables to the list of assigned variables for a given Invoke-Command session. + /// + /// + /// + private void AddAssignedVarsToSession(string sessionName, IEnumerable variablesToAdd) + { + if (!_varsDeclaredPerSession.ContainsKey(sessionName)) + { + _varsDeclaredPerSession.Add(sessionName, new List()); } + _varsDeclaredPerSession[sessionName].AddRange(variablesToAdd); + } - foreach (var session in sessionDictionary) + /// + /// AnalyzeScriptBlock: Generate a Diagnostic Record for each incorrectly used variable inside a given ScriptBlock. + /// + /// + private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + var nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); + + GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); + } + + /// + /// GenerateDiagnosticRecords: Add Diagnostic Records to the internal list for each given variable + /// + /// + private void GenerateDiagnosticRecords(IEnumerable nonAssignedNonUsingVarAsts) + { + foreach (var variableExpression in nonAssignedNonUsingVarAsts) { - // Find all variables that are assigned within these ScriptBlocks that are part of one session - var varsInAssignments = new List(); - foreach (var ast in session.Value) + if (variableExpression == null) { - varsInAssignments.AddRange( - FindVarsInAssignmentAsts(ast)); + continue; } - // Find all variables that are not locally assigned, and don't have $using: scope modifier - foreach (var ast in session.Value) - { - result.AddRange( - FindNonAssignedNonUsingVarAsts(ast, varsInAssignments)); - } + _diagnosticAccumulator.Add(new DiagnosticRecord( + message: string.Format(CultureInfo.CurrentCulture, + Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), + extent: variableExpression.Extent, + ruleName: _rule.GetName(), + severity: Severity, + scriptPath: _analyzedFilePath, + ruleId: _rule.GetName(), + suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); } + } + + /// + /// IsInvokeCommandSessionScriptBlock: Returns true if: + /// - command is 'Invoke-Command' (or alias) + /// - parameter '-Session' is present + /// + /// + /// + private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) + { + return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.Equals("session", StringComparison.OrdinalIgnoreCase)); + } - return result; + /// + /// IsInvokeCommandComputerScriptBlock: Returns true if: + /// - command is 'Invoke-Command' (or alias) + /// - parameter '-Computer' is present + /// + /// + /// + private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) + { + // 'com' is the shortest unambiguous form for the '-Computer' parameter + return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// IsForeachScriptBlock: Returns true if: + /// - command is 'Foreach-Object' (or alias) + /// - parameter '-Parallel' is present + /// + /// + /// + private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + // 'pa' is the shortest unambiguous form for the '-Parallel' parameter + return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + (scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// IsJobScriptBlock: Returns true if: + /// - command is 'Start-Job' or 'Start-ThreadJob' (or alias) + /// - parameter name for this ScriptBlock not '-InitializationScript' + /// + /// + /// + private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + // 'ini' is the shortest unambiguous form for the '-InitializationScript' parameter + return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || + _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && + !(scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// IsForeachScriptBlock: Returns true if: + /// - command is 'InlineScript' (or alias) + /// + /// + private bool IsInlineScriptBlock(string cmdName) + { + return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); } } } From 921c1d2b59a15eef57d11327ccdd2ef577152c78 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Tue, 10 Mar 2020 08:16:34 +0100 Subject: [PATCH 30/55] fix build for windows powershell --- .vscode/extensions.json | 2 +- .vscode/launch.json | 10 +++++++-- Rules/UseUsingScopeModifierInNewRunspaces.cs | 2 +- build.psm1 | 23 ++++++++++++++++++-- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9be92f563..098ed86e6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,7 @@ // for the documentation about the extensions.json format "recommendations": [ "ms-vscode.PowerShell", - "ms-vscode.csharp", + "ms-dotnettools.csharp", "ms-vscode-remote.remote-containers" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 9beb418df..f4f6177b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,13 @@ { "version": "0.2.0", "configurations": [ - + { + "name": "PowerShell Launch Current File", + "type": "PowerShell", + "request": "launch", + "script": "${file}", + "cwd": "${file}" + }, { "name": ".NET Core Launch (console)", "type": "coreclr", @@ -27,4 +33,4 @@ "processId": "${command:pickProcess}" } ] -} +} \ No newline at end of file diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 71644d5fb..9e7a08eac 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -92,7 +92,7 @@ public string GetSourceName() #if !(PSV3 || PSV4) internal class SyntaxCompatibilityVisitor : AstVisitor2 #else - private class SyntaxCompatibilityVisitor : AstVisitor + internal class SyntaxCompatibilityVisitor : AstVisitor #endif { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; diff --git a/build.psm1 b/build.psm1 index 54b5d426b..bfd1633b5 100644 --- a/build.psm1 +++ b/build.psm1 @@ -289,7 +289,12 @@ function Start-ScriptAnalyzerBuild function Test-ScriptAnalyzer { [CmdletBinding()] - param ( [Parameter()][switch]$InProcess, [switch]$ShowAll ) + param( + [Parameter()] + [switch]$InProcess, + [switch]$ShowAll, + [String]$RuleToTest + ) END { # versions 3 and 4 don't understand versioned module paths, so we need to rename the directory of the version to @@ -319,7 +324,12 @@ function Test-ScriptAnalyzer $testModulePath = Join-Path "${projectRoot}" -ChildPath out } $testResultsFile = "'$(Join-Path ${projectRoot} -childPath TestResults.xml)'" - $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" + if ($RuleToTest) { + $testScripts = "${projectRoot}\Tests\Rules\$RuleToTest" + } + else { + $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" + } try { if ( $major -lt 5 ) { Rename-Item $script:destinationDir ${testModulePath} @@ -704,3 +714,12 @@ function Copy-CrossCompatibilityModule } } } + +Function RuleNameCompleter { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter) + + Get-Childitem -Path "${projectRoot}\Tests\Rules" -Filter "*$wordToComplete*.tests.ps1" -Name | + ForEach-Object { New-Object System.Management.Automation.CompletionResult $_ } +} + +Register-ArgumentCompleter -CommandName 'Test-ScriptAnalyzer' -ParameterName 'RuleToTest' -ScriptBlock $Function:RuleNameCompleter \ No newline at end of file From f57c610391acffe8b7190ad8618924d05504f47a Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Wed, 11 Mar 2020 16:44:38 +0100 Subject: [PATCH 31/55] Revert "fix build for windows powershell" This reverts commit 921c1d2b59a15eef57d11327ccdd2ef577152c78. revert fix, because multiple unintended changes were pushed along with it. --- .vscode/extensions.json | 2 +- .vscode/launch.json | 10 ++------- Rules/UseUsingScopeModifierInNewRunspaces.cs | 2 +- build.psm1 | 23 ++------------------ 4 files changed, 6 insertions(+), 31 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 098ed86e6..9be92f563 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,7 @@ // for the documentation about the extensions.json format "recommendations": [ "ms-vscode.PowerShell", - "ms-dotnettools.csharp", + "ms-vscode.csharp", "ms-vscode-remote.remote-containers" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f4f6177b7..9beb418df 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,13 +1,7 @@ { "version": "0.2.0", "configurations": [ - { - "name": "PowerShell Launch Current File", - "type": "PowerShell", - "request": "launch", - "script": "${file}", - "cwd": "${file}" - }, + { "name": ".NET Core Launch (console)", "type": "coreclr", @@ -33,4 +27,4 @@ "processId": "${command:pickProcess}" } ] -} \ No newline at end of file +} diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 9e7a08eac..71644d5fb 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -92,7 +92,7 @@ public string GetSourceName() #if !(PSV3 || PSV4) internal class SyntaxCompatibilityVisitor : AstVisitor2 #else - internal class SyntaxCompatibilityVisitor : AstVisitor + private class SyntaxCompatibilityVisitor : AstVisitor #endif { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; diff --git a/build.psm1 b/build.psm1 index bfd1633b5..54b5d426b 100644 --- a/build.psm1 +++ b/build.psm1 @@ -289,12 +289,7 @@ function Start-ScriptAnalyzerBuild function Test-ScriptAnalyzer { [CmdletBinding()] - param( - [Parameter()] - [switch]$InProcess, - [switch]$ShowAll, - [String]$RuleToTest - ) + param ( [Parameter()][switch]$InProcess, [switch]$ShowAll ) END { # versions 3 and 4 don't understand versioned module paths, so we need to rename the directory of the version to @@ -324,12 +319,7 @@ function Test-ScriptAnalyzer $testModulePath = Join-Path "${projectRoot}" -ChildPath out } $testResultsFile = "'$(Join-Path ${projectRoot} -childPath TestResults.xml)'" - if ($RuleToTest) { - $testScripts = "${projectRoot}\Tests\Rules\$RuleToTest" - } - else { - $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" - } + $testScripts = "'${projectRoot}\Tests\Engine','${projectRoot}\Tests\Rules','${projectRoot}\Tests\Documentation'" try { if ( $major -lt 5 ) { Rename-Item $script:destinationDir ${testModulePath} @@ -714,12 +704,3 @@ function Copy-CrossCompatibilityModule } } } - -Function RuleNameCompleter { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParmeter) - - Get-Childitem -Path "${projectRoot}\Tests\Rules" -Filter "*$wordToComplete*.tests.ps1" -Name | - ForEach-Object { New-Object System.Management.Automation.CompletionResult $_ } -} - -Register-ArgumentCompleter -CommandName 'Test-ScriptAnalyzer' -ParameterName 'RuleToTest' -ScriptBlock $Function:RuleNameCompleter \ No newline at end of file From 7956edada150134301748121741a6d61ad04d1d9 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Wed, 11 Mar 2020 16:47:22 +0100 Subject: [PATCH 32/55] Change private class to internal class for Windows PowerShell --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 71644d5fb..9e7a08eac 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -92,7 +92,7 @@ public string GetSourceName() #if !(PSV3 || PSV4) internal class SyntaxCompatibilityVisitor : AstVisitor2 #else - private class SyntaxCompatibilityVisitor : AstVisitor + internal class SyntaxCompatibilityVisitor : AstVisitor #endif { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; From 427461a604df2d9a1d25b9aad4647ca4e4b259d2 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 12 Mar 2020 08:59:09 +0100 Subject: [PATCH 33/55] Add logic to detect DSCScriptResource --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 9e7a08eac..a772efe38 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -97,6 +97,8 @@ internal class SyntaxCompatibilityVisitor : AstVisitor { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; + private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); private readonly IEnumerable _threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); @@ -158,7 +160,8 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA if (IsInlineScriptBlock(cmdName) || IsJobScriptBlock(cmdName, scriptBlockParameterAst) || IsForeachScriptBlock(cmdName, scriptBlockParameterAst) || - IsInvokeCommandComputerScriptBlock(cmdName, commandAst)) + IsInvokeCommandComputerScriptBlock(cmdName, commandAst) || + IsDSCScriptResource(cmdName, commandAst)) { AnalyzeScriptBlock(scriptBlockExpressionAst); return AstVisitAction.SkipChildren; @@ -401,7 +404,7 @@ private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockPar } /// - /// IsForeachScriptBlock: Returns true if: + /// IsInlineScriptBlock: Returns true if: /// - command is 'InlineScript' (or alias) /// /// @@ -409,5 +412,19 @@ private bool IsInlineScriptBlock(string cmdName) { return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); } + + + /// + /// IsDSCScriptResource: Returns true if: + /// - command is 'GetScript', 'TestScript' or 'SetScript' + /// + /// + private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) + { + // Inside DSC Script resource, GetScript is of the form 'Script foo { GetScript = {} }' + // If we reach this point in the code, we are sure there are + return s_dscScriptResourceCommandNames.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements[1].ToString() == "="; + } } } From 4f8bf36f5ba5641c9d95b298b159f99bf32b80e5 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 12 Mar 2020 08:59:44 +0100 Subject: [PATCH 34/55] Add tests for DSC Script resource --- ...UsingScopeModifierInNewRunspaces.tests.ps1 | 208 +++++++++++------- 1 file changed, 133 insertions(+), 75 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 48fd800de..48cc22b16 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -100,123 +100,181 @@ Describe "UseUsingScopeModifierInNewRunspaces" { Invoke-Command -session $session -ScriptBlock {$foo = "foo" } }' } - ) + # DSC Script resource + @{ + Description = 'DSC Script resource with GetScript {}' + ScriptBlock = 'Script ReturnFoo { + GetScript = { + return @{ "Result" = "$foo" } + } + }' + } + @{ + Description = 'DSC Script resource with TestScript {}' + ScriptBlock = 'Script TestFoo { + TestScript = { + return [bool]$foo + } + }' + } + @{ + Description = 'DSC Script resource with SetScript {}' + ScriptBlock = 'Script SetFoo { + SetScript = { + $foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + ) - It "should emit for: " -TestCases $testCases { - param($Description, $ScriptBlock) - [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings.Count | Should -Be 1 - } + It "should emit for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 1 + } - It "should emit suggested correction" { - $ScriptBlock = '{ + It "should emit suggested correction" { + $ScriptBlock = '{ 1..2 | ForEach-Object -Parallel { $var } }' - $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings[0].SuggestedCorrections[0].Text | Should -Be '$using:var' - } + $warnings[0].SuggestedCorrections[0].Text | Should -Be '$using:var' } +} - Context "Should not detect anything" { - $testCases = @( - @{ - Description = "Foreach-Object with uninitialized var inside" - ScriptBlock = '{ +Context "Should not detect anything" { + $testCases = @( + @{ + Description = "Foreach-Object with uninitialized var inside" + ScriptBlock = '{ 1..2 | ForEach-Object { $var } }' - } - @{ - Description = "Foreach-Object -Parallel with uninitialized `$using: var" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with uninitialized `$using: var" + ScriptBlock = '{ 1..2 | foreach-object -Parallel { $using:var } }' - } - @{ - Description = "Foreach-Object -Parallel with var assigned locally" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with var assigned locally" + ScriptBlock = '{ 1..2 | ForEach-Object -Parallel { $var="somevalue" } }' - } - @{ - Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" + ScriptBlock = '{ 1..2 | ForEach-Object -Parallel{ $PSBoundParameters } }' - } - @{ - Description = "Foreach-Object -Parallel with vars in other parameters" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with vars in other parameters" + ScriptBlock = '{ $foo = "bar" ForEach-Object -Parallel {$_} -InputObject $foo }' - } - # Start-Job / Start-ThreadJob - @{ - Description = 'Start-Job with $using:' - ScriptBlock = '{ + } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job with $using:' + ScriptBlock = '{ $foo = "bar" Start-Job -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-ThreadJob with $using:' - ScriptBlock = '{ + } + @{ + Description = 'Start-ThreadJob with $using:' + ScriptBlock = '{ $foo = "bar" Start-ThreadJob -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-Job with -InitializationScript with a variable' - ScriptBlock = '{ + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ $foo = "bar" Start-Job -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-ThreadJob with -InitializationScript with a variable' - ScriptBlock = '{ + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ $foo = "bar" Start-ThreadJob -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob }' - } - # workflow/inlinescript - @{ - Description = "Workflow/InlineScript" - ScriptBlock = '{ + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ $foo = "bar" workflow baz { InlineScript {$using:foo} } }' - } - # Invoke-Command - @{ - Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' - ScriptBlock = '{ + } + # Invoke-Command + @{ + Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' + ScriptBlock = '{ $session = new-PSSession -ComputerName "baz" Invoke-Command -session $session -ScriptBlock {$foo = "foo" } Invoke-Command -session $session -ScriptBlock {Write-Output $foo} }' - } - @{ - Description = 'Invoke-Command without -ComputerName' - ScriptBlock = '{ + } + @{ + Description = 'Invoke-Command without -ComputerName' + ScriptBlock = '{ Invoke-Command -ScriptBlock {Write-Output $foo} }' - } - # Unsupported scenarios - @{ - Description = 'Rule should skip analysis when Command Name cannot be resolved' - ScriptBlock = '{ + } + # Unsupported scenarios + @{ + Description = 'Rule should skip analysis when Command Name cannot be resolved' + ScriptBlock = '{ $commandName = "Invoke-Command" & $commandName -ComputerName -ScriptBlock { $foo } }' - } - ) - - It "should not emit anything for: " -TestCases $testCases { - param($Description, $ScriptBlock) - [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings.Count | Should -Be 0 } + # DSC Script resource + @{ + Description = 'DSC Script resource with GetScript {}' + ScriptBlock = 'Script ReturnFoo { + GetScript = { + return @{ "Result" = "$using:foo" } + } + }' + } + @{ + Description = 'DSC Script resource with TestScript {}' + ScriptBlock = 'Script TestFoo { + TestScript = { + return [bool]$using:foo + } + }' + } + @{ + Description = 'DSC Script resource with SetScript {}' + ScriptBlock = 'Script SetFoo { + SetScript = { + $using:foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + @{ + Description = 'Non-DSC function with the name SetScript {}' + ScriptBlock = '{ + SetScript -ScriptBlock { + $foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + ) + + It "should not emit anything for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 0 } +} } \ No newline at end of file From 0cb8f9ec5dca24c4d1e7527d2c6a5ac9cdb51a61 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:17:33 +0100 Subject: [PATCH 35/55] Enhance label test topic Co-Authored-By: Robert Holt --- Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 48cc22b16..ce5394b3c 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -6,7 +6,7 @@ $settings = @{ Describe "UseUsingScopeModifierInNewRunspaces" { Context "Should detect something" { $testCases = @( - # Foreach-Object -Parallel {} + # Test: Foreach-Object -Parallel {} @{ Description = "Foreach-Object -Parallel with undeclared var" ScriptBlock = '{ @@ -277,4 +277,4 @@ Context "Should not detect anything" { $warnings.Count | Should -Be 0 } } -} \ No newline at end of file +} From af2ceee705a9e36051d75c11a11a6f65c3e0cb99 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:22:14 +0100 Subject: [PATCH 36/55] repair test indentation and add newline at eof --- ...UsingScopeModifierInNewRunspaces.tests.ps1 | 184 +++++++++--------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 48cc22b16..efb8c6c7a 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -125,156 +125,156 @@ Describe "UseUsingScopeModifierInNewRunspaces" { } }' } - ) + ) - It "should emit for: " -TestCases $testCases { - param($Description, $ScriptBlock) - [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings.Count | Should -Be 1 - } + It "should emit for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 1 + } - It "should emit suggested correction" { - $ScriptBlock = '{ + It "should emit suggested correction" { + $ScriptBlock = '{ 1..2 | ForEach-Object -Parallel { $var } }' - $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings[0].SuggestedCorrections[0].Text | Should -Be '$using:var' + $warnings[0].SuggestedCorrections[0].Text | Should -Be '$using:var' + } } -} -Context "Should not detect anything" { - $testCases = @( - @{ - Description = "Foreach-Object with uninitialized var inside" - ScriptBlock = '{ + Context "Should not detect anything" { + $testCases = @( + @{ + Description = "Foreach-Object with uninitialized var inside" + ScriptBlock = '{ 1..2 | ForEach-Object { $var } }' - } - @{ - Description = "Foreach-Object -Parallel with uninitialized `$using: var" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with uninitialized `$using: var" + ScriptBlock = '{ 1..2 | foreach-object -Parallel { $using:var } }' - } - @{ - Description = "Foreach-Object -Parallel with var assigned locally" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with var assigned locally" + ScriptBlock = '{ 1..2 | ForEach-Object -Parallel { $var="somevalue" } }' - } - @{ - Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" + ScriptBlock = '{ 1..2 | ForEach-Object -Parallel{ $PSBoundParameters } }' - } - @{ - Description = "Foreach-Object -Parallel with vars in other parameters" - ScriptBlock = '{ + } + @{ + Description = "Foreach-Object -Parallel with vars in other parameters" + ScriptBlock = '{ $foo = "bar" ForEach-Object -Parallel {$_} -InputObject $foo }' - } - # Start-Job / Start-ThreadJob - @{ - Description = 'Start-Job with $using:' - ScriptBlock = '{ + } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job with $using:' + ScriptBlock = '{ $foo = "bar" Start-Job -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-ThreadJob with $using:' - ScriptBlock = '{ + } + @{ + Description = 'Start-ThreadJob with $using:' + ScriptBlock = '{ $foo = "bar" Start-ThreadJob -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-Job with -InitializationScript with a variable' - ScriptBlock = '{ + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ $foo = "bar" Start-Job -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob }' - } - @{ - Description = 'Start-ThreadJob with -InitializationScript with a variable' - ScriptBlock = '{ + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ $foo = "bar" Start-ThreadJob -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob }' - } - # workflow/inlinescript - @{ - Description = "Workflow/InlineScript" - ScriptBlock = '{ + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ $foo = "bar" workflow baz { InlineScript {$using:foo} } }' - } - # Invoke-Command - @{ - Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' - ScriptBlock = '{ + } + # Invoke-Command + @{ + Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' + ScriptBlock = '{ $session = new-PSSession -ComputerName "baz" Invoke-Command -session $session -ScriptBlock {$foo = "foo" } Invoke-Command -session $session -ScriptBlock {Write-Output $foo} }' - } - @{ - Description = 'Invoke-Command without -ComputerName' - ScriptBlock = '{ + } + @{ + Description = 'Invoke-Command without -ComputerName' + ScriptBlock = '{ Invoke-Command -ScriptBlock {Write-Output $foo} }' - } - # Unsupported scenarios - @{ - Description = 'Rule should skip analysis when Command Name cannot be resolved' - ScriptBlock = '{ + } + # Unsupported scenarios + @{ + Description = 'Rule should skip analysis when Command Name cannot be resolved' + ScriptBlock = '{ $commandName = "Invoke-Command" & $commandName -ComputerName -ScriptBlock { $foo } }' - } - # DSC Script resource - @{ - Description = 'DSC Script resource with GetScript {}' - ScriptBlock = 'Script ReturnFoo { + } + # DSC Script resource + @{ + Description = 'DSC Script resource with GetScript {}' + ScriptBlock = 'Script ReturnFoo { GetScript = { return @{ "Result" = "$using:foo" } } }' - } - @{ - Description = 'DSC Script resource with TestScript {}' - ScriptBlock = 'Script TestFoo { + } + @{ + Description = 'DSC Script resource with TestScript {}' + ScriptBlock = 'Script TestFoo { TestScript = { return [bool]$using:foo } }' - } - @{ - Description = 'DSC Script resource with SetScript {}' - ScriptBlock = 'Script SetFoo { + } + @{ + Description = 'DSC Script resource with SetScript {}' + ScriptBlock = 'Script SetFoo { SetScript = { $using:foo | Set-Content -path "~\nonexistent\foo.txt" } }' - } - @{ - Description = 'Non-DSC function with the name SetScript {}' - ScriptBlock = '{ + } + @{ + Description = 'Non-DSC function with the name SetScript {}' + ScriptBlock = '{ SetScript -ScriptBlock { $foo | Set-Content -path "~\nonexistent\foo.txt" } }' - } - ) + } + ) - It "should not emit anything for: " -TestCases $testCases { - param($Description, $ScriptBlock) - [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings - $warnings.Count | Should -Be 0 + It "should not emit anything for: " -TestCases $testCases { + param($Description, $ScriptBlock) + [System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptBlock -Settings $settings + $warnings.Count | Should -Be 0 + } } } -} \ No newline at end of file From 29176a8798ccc23d3f44db3ddfecdc7ade99beff Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:35:09 +0100 Subject: [PATCH 37/55] Move testcases to BeforeAll blocks --- ...UsingScopeModifierInNewRunspaces.tests.ps1 | 490 +++++++++--------- 1 file changed, 248 insertions(+), 242 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index f86b12ee2..0e7ea1ff8 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -5,127 +5,130 @@ $settings = @{ Describe "UseUsingScopeModifierInNewRunspaces" { Context "Should detect something" { - $testCases = @( - # Test: Foreach-Object -Parallel {} - @{ - Description = "Foreach-Object -Parallel with undeclared var" - ScriptBlock = '{ - 1..2 | ForEach-Object -Parallel { $var } - }' - } - @{ - Description = "foreach -parallel alias with undeclared var" - ScriptBlock = '{ - 1..2 | ForEach -Parallel { $var } - }' - } - @{ - Description = "% -parallel alias with undeclared var" - ScriptBlock = '{ - 1..2 | % -Parallel { $var } - }' - } - @{ - Description = "Foreach-Object -pa abbreviated param with undeclared var" - ScriptBlock = '{ - 1..2 | foreach-object -pa { $var } - }' - } - @{ - Description = "Foreach-Object -Parallel nested with undeclared var" - ScriptBlock = '{ - $myNestedScriptBlock = { + BeforeAll { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','')] + $testCases = @( + # Test: Foreach-Object -Parallel {} + @{ + Description = "Foreach-Object -Parallel with undeclared var" + ScriptBlock = '{ 1..2 | ForEach-Object -Parallel { $var } - } - }' - } - # Start-Job / Start-ThreadJob - @{ - Description = 'Start-Job without $using:' - ScriptBlock = '{ - $foo = "bar" - Start-Job {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-ThreadJob without $using:' - ScriptBlock = '{ - $foo = "bar" - Start-ThreadJob -ScriptBlock {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-Job with -InitializationScript with a variable' - ScriptBlock = '{ - $foo = "bar" - Start-Job -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-ThreadJob with -InitializationScript with a variable' - ScriptBlock = '{ - $foo = "bar" - Start-ThreadJob -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - # workflow/inlinescript - @{ - Description = "Workflow/InlineScript" - ScriptBlock = '{ - $foo = "bar" - workflow baz { InlineScript {$foo} } - }' - } - # Invoke-Command - @{ - Description = 'Invoke-Command with -ComputerName' - ScriptBlock = '{ - Invoke-Command -ScriptBlock {Write-Output $foo} -ComputerName "bar" - }' - } - @{ - Description = 'Invoke-Command with two different sessions, where var is declared in wrong session' - ScriptBlock = '{ - $session = new-PSSession -ComputerName "baz" - $otherSession = new-PSSession -ComputerName "bar" - Invoke-Command -session $session -ScriptBlock {$foo = "foo" } - Invoke-Command -session $otherSession -ScriptBlock {Write-Output $foo} - }' - } - @{ - Description = 'Invoke-Command with session, where var is declared after use' - ScriptBlock = '{ - $session = new-PSSession -ComputerName "baz" - Invoke-Command -session $session -ScriptBlock {Write-Output $foo} - Invoke-Command -session $session -ScriptBlock {$foo = "foo" } - }' - } - # DSC Script resource - @{ - Description = 'DSC Script resource with GetScript {}' - ScriptBlock = 'Script ReturnFoo { - GetScript = { - return @{ "Result" = "$foo" } - } }' - } - @{ - Description = 'DSC Script resource with TestScript {}' - ScriptBlock = 'Script TestFoo { - TestScript = { - return [bool]$foo - } + } + @{ + Description = "foreach -parallel alias with undeclared var" + ScriptBlock = '{ + 1..2 | ForEach -Parallel { $var } }' - } - @{ - Description = 'DSC Script resource with SetScript {}' - ScriptBlock = 'Script SetFoo { - SetScript = { - $foo | Set-Content -path "~\nonexistent\foo.txt" + } + @{ + Description = "% -parallel alias with undeclared var" + ScriptBlock = '{ + 1..2 | % -Parallel { $var } + }' + } + @{ + Description = "Foreach-Object -pa abbreviated param with undeclared var" + ScriptBlock = '{ + 1..2 | foreach-object -pa { $var } + }' + } + @{ + Description = "Foreach-Object -Parallel nested with undeclared var" + ScriptBlock = '{ + $myNestedScriptBlock = { + 1..2 | ForEach-Object -Parallel { $var } } }' - } - ) + } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job without $using:' + ScriptBlock = '{ + $foo = "bar" + Start-Job {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob without $using:' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ + $foo = "bar" + workflow baz { InlineScript {$foo} } + }' + } + # Invoke-Command + @{ + Description = 'Invoke-Command with -ComputerName' + ScriptBlock = '{ + Invoke-Command -ScriptBlock {Write-Output $foo} -ComputerName "bar" + }' + } + @{ + Description = 'Invoke-Command with two different sessions, where var is declared in wrong session' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + $otherSession = new-PSSession -ComputerName "bar" + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + Invoke-Command -session $otherSession -ScriptBlock {Write-Output $foo} + }' + } + @{ + Description = 'Invoke-Command with session, where var is declared after use' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + Invoke-Command -session $session -ScriptBlock {Write-Output $foo} + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + }' + } + # DSC Script resource + @{ + Description = 'DSC Script resource with GetScript {}' + ScriptBlock = 'Script ReturnFoo { + GetScript = { + return @{ "Result" = "$foo" } + } + }' + } + @{ + Description = 'DSC Script resource with TestScript {}' + ScriptBlock = 'Script TestFoo { + TestScript = { + return [bool]$foo + } + }' + } + @{ + Description = 'DSC Script resource with SetScript {}' + ScriptBlock = 'Script SetFoo { + SetScript = { + $foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + ) + } It "should emit for: " -TestCases $testCases { param($Description, $ScriptBlock) @@ -144,132 +147,135 @@ Describe "UseUsingScopeModifierInNewRunspaces" { } Context "Should not detect anything" { - $testCases = @( - @{ - Description = "Foreach-Object with uninitialized var inside" - ScriptBlock = '{ - 1..2 | ForEach-Object { $var } - }' - } - @{ - Description = "Foreach-Object -Parallel with uninitialized `$using: var" - ScriptBlock = '{ - 1..2 | foreach-object -Parallel { $using:var } - }' - } - @{ - Description = "Foreach-Object -Parallel with var assigned locally" - ScriptBlock = '{ - 1..2 | ForEach-Object -Parallel { $var="somevalue" } - }' - } - @{ - Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" - ScriptBlock = '{ - 1..2 | ForEach-Object -Parallel{ $PSBoundParameters } - }' - } - @{ - Description = "Foreach-Object -Parallel with vars in other parameters" - ScriptBlock = '{ - $foo = "bar" - ForEach-Object -Parallel {$_} -InputObject $foo - }' - } - # Start-Job / Start-ThreadJob - @{ - Description = 'Start-Job with $using:' - ScriptBlock = '{ - $foo = "bar" - Start-Job -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-ThreadJob with $using:' - ScriptBlock = '{ - $foo = "bar" - Start-ThreadJob -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-Job with -InitializationScript with a variable' - ScriptBlock = '{ - $foo = "bar" - Start-Job -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - @{ - Description = 'Start-ThreadJob with -InitializationScript with a variable' - ScriptBlock = '{ - $foo = "bar" - Start-ThreadJob -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob - }' - } - # workflow/inlinescript - @{ - Description = "Workflow/InlineScript" - ScriptBlock = '{ - $foo = "bar" - workflow baz { InlineScript {$using:foo} } - }' - } - # Invoke-Command - @{ - Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' - ScriptBlock = '{ - $session = new-PSSession -ComputerName "baz" - Invoke-Command -session $session -ScriptBlock {$foo = "foo" } - Invoke-Command -session $session -ScriptBlock {Write-Output $foo} - }' - } - @{ - Description = 'Invoke-Command without -ComputerName' - ScriptBlock = '{ - Invoke-Command -ScriptBlock {Write-Output $foo} - }' - } - # Unsupported scenarios - @{ - Description = 'Rule should skip analysis when Command Name cannot be resolved' - ScriptBlock = '{ - $commandName = "Invoke-Command" - & $commandName -ComputerName -ScriptBlock { $foo } - }' - } - # DSC Script resource - @{ - Description = 'DSC Script resource with GetScript {}' - ScriptBlock = 'Script ReturnFoo { - GetScript = { - return @{ "Result" = "$using:foo" } - } - }' - } - @{ - Description = 'DSC Script resource with TestScript {}' - ScriptBlock = 'Script TestFoo { - TestScript = { - return [bool]$using:foo - } - }' - } - @{ - Description = 'DSC Script resource with SetScript {}' - ScriptBlock = 'Script SetFoo { - SetScript = { - $using:foo | Set-Content -path "~\nonexistent\foo.txt" - } - }' - } - @{ - Description = 'Non-DSC function with the name SetScript {}' - ScriptBlock = '{ - SetScript -ScriptBlock { - $foo | Set-Content -path "~\nonexistent\foo.txt" - } - }' - } - ) + BeforeAll { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments','')] + $testCases = @( + @{ + Description = "Foreach-Object with uninitialized var inside" + ScriptBlock = '{ + 1..2 | ForEach-Object { $var } + }' + } + @{ + Description = "Foreach-Object -Parallel with uninitialized `$using: var" + ScriptBlock = '{ + 1..2 | foreach-object -Parallel { $using:var } + }' + } + @{ + Description = "Foreach-Object -Parallel with var assigned locally" + ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel { $var="somevalue" } + }' + } + @{ + Description = "Foreach-Object -Parallel with built-in var '`$PSBoundParameters' inside" + ScriptBlock = '{ + 1..2 | ForEach-Object -Parallel{ $PSBoundParameters } + }' + } + @{ + Description = "Foreach-Object -Parallel with vars in other parameters" + ScriptBlock = '{ + $foo = "bar" + ForEach-Object -Parallel {$_} -InputObject $foo + }' + } + # Start-Job / Start-ThreadJob + @{ + Description = 'Start-Job with $using:' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with $using:' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$using:foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-Job with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-Job -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + @{ + Description = 'Start-ThreadJob with -InitializationScript with a variable' + ScriptBlock = '{ + $foo = "bar" + Start-ThreadJob -ScriptBlock {$using:foo} -InitializationScript {$foo} | Receive-Job -Wait -AutoRemoveJob + }' + } + # workflow/inlinescript + @{ + Description = "Workflow/InlineScript" + ScriptBlock = '{ + $foo = "bar" + workflow baz { InlineScript {$using:foo} } + }' + } + # Invoke-Command + @{ + Description = 'Invoke-Command -Session, var declared in same session, other scriptblock' + ScriptBlock = '{ + $session = new-PSSession -ComputerName "baz" + Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + Invoke-Command -session $session -ScriptBlock {Write-Output $foo} + }' + } + @{ + Description = 'Invoke-Command without -ComputerName' + ScriptBlock = '{ + Invoke-Command -ScriptBlock {Write-Output $foo} + }' + } + # Unsupported scenarios + @{ + Description = 'Rule should skip analysis when Command Name cannot be resolved' + ScriptBlock = '{ + $commandName = "Invoke-Command" + & $commandName -ComputerName -ScriptBlock { $foo } + }' + } + # DSC Script resource + @{ + Description = 'DSC Script resource with GetScript {}' + ScriptBlock = 'Script ReturnFoo { + GetScript = { + return @{ "Result" = "$using:foo" } + } + }' + } + @{ + Description = 'DSC Script resource with TestScript {}' + ScriptBlock = 'Script TestFoo { + TestScript = { + return [bool]$using:foo + } + }' + } + @{ + Description = 'DSC Script resource with SetScript {}' + ScriptBlock = 'Script SetFoo { + SetScript = { + $using:foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + @{ + Description = 'Non-DSC function with the name SetScript {}' + ScriptBlock = '{ + SetScript -ScriptBlock { + $foo | Set-Content -path "~\nonexistent\foo.txt" + } + }' + } + ) + } It "should not emit anything for: " -TestCases $testCases { param($Description, $ScriptBlock) From b1bee39a872768d7ce4887cdcf3d5ee053f6ef32 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:38:19 +0100 Subject: [PATCH 38/55] Add documentation that DSC Script resource is supported --- RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md b/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md index e8590d075..d1ba06cf8 100644 --- a/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md +++ b/RuleDocumentation/UseUsingScopeModifierInNewRunspaces.md @@ -10,7 +10,9 @@ This applies to: - Invoke-Command * - Workflow { InlineScript {}} - Foreach-Object ** -- Start-(Thread)Job +- Start-Job +- Start-ThreadJob +- The `Script` resource in DSC configurations, specifically for the `GetScript`, `TestScript` and `SetScript` properties \* Only with the -ComputerName or -Session parameter. \*\* Only with the -Parallel parameter From f6de07d6449c345c92e1c93318a5102cc08accdc Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:49:31 +0100 Subject: [PATCH 39/55] change string[] to IReadOnlyList Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a772efe38..375dde467 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -97,7 +97,8 @@ internal class SyntaxCompatibilityVisitor : AstVisitor { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; - private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; + private static readonly IReadOnlyList s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; + private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); From 5f6bae0a59c803a45eb3916fdfb5c7bb31123eef Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 10:50:45 +0100 Subject: [PATCH 40/55] extract scriptBlockPosition as a variable for readability Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 375dde467..d17075a48 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -155,8 +155,9 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - var scriptBlockParameterAst = commandAst.CommandElements[ - commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; + int scriptBlockPosition = commandAst.CommandElements.IndexOf(scriptBlockExpressionAst); + var scriptBlockParameterAst = commandAst.CommandElements[scriptBlockPosition - 1] as CommandParameterAst; + if (IsInlineScriptBlock(cmdName) || IsJobScriptBlock(cmdName, scriptBlockParameterAst) || From bcaba8306624a524c7bc0f1c42456e2afd109c86 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 11:03:21 +0100 Subject: [PATCH 41/55] put arguments on their own line for readability --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a772efe38..3e06bbacf 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -41,7 +41,10 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) /// The name of this rule public string GetName() { - return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), Strings.UseUsingScopeModifierInNewRunspacesName); } From 1d6b55e104a246c2c8a9dbea685288bb97fb0583 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 11:05:42 +0100 Subject: [PATCH 42/55] make visitor class a private, nested class in rule --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 3e06bbacf..dcfcaa5e9 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -90,12 +90,11 @@ public string GetSourceName() { return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); } - } #if !(PSV3 || PSV4) - internal class SyntaxCompatibilityVisitor : AstVisitor2 + private class SyntaxCompatibilityVisitor : AstVisitor2 #else - internal class SyntaxCompatibilityVisitor : AstVisitor + private class SyntaxCompatibilityVisitor : AstVisitor #endif { private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; @@ -431,3 +430,4 @@ private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) } } } +} From 92f19c970c9ec0c64ec71a411a5a0d278c672116 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 11:07:14 +0100 Subject: [PATCH 43/55] change 'var' to type name for method calls --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 30 ++++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index dcfcaa5e9..25e21942d 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -101,17 +101,23 @@ private class SyntaxCompatibilityVisitor : AstVisitor private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; - private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); + private readonly IEnumerable _jobCmdletNamesAndAliases = + Helper.Instance.CmdletNameAndAliases("Start-Job"); - private readonly IEnumerable _threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); + private readonly IEnumerable _threadJobCmdletNamesAndAliases = + Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); - private readonly IEnumerable _inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); + private readonly IEnumerable _inlineScriptCmdletNamesAndAliases = + Helper.Instance.CmdletNameAndAliases("InlineScript"); - private readonly IEnumerable _foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); + private readonly IEnumerable _foreachObjectCmdletNamesAndAliases = + Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); + private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = + Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - private readonly Dictionary> _varsDeclaredPerSession = new Dictionary>(); + private readonly Dictionary> _varsDeclaredPerSession = + new Dictionary>(); private readonly List _diagnosticAccumulator; @@ -149,7 +155,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - var cmdName = commandAst.GetCommandName(); + string cmdName = commandAst.GetCommandName(); if (cmdName == null) { // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` @@ -171,9 +177,9 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA if (IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) { - var sessionName = GetSessionName(commandAst); + string sessionName = GetSessionName(commandAst); - var varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); if (varsInLocalAssignments != null) { AddAssignedVarsToSession(sessionName, varsInLocalAssignments); @@ -273,7 +279,7 @@ private static string GetSessionName(CommandAst commandAst) return ""; } - var sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); + int sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); return commandAst .CommandElements[sessionParamAstIndex + 1] @@ -312,7 +318,7 @@ private void AddAssignedVarsToSession(string sessionName, IEnumerable private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) { - var nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + List nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); From 79436255f629c3ee114bc1eb4f81f6aed9b534f1 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 11:17:32 +0100 Subject: [PATCH 44/55] fix indentation for nested visitor class --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 548 +++++++++---------- 1 file changed, 274 insertions(+), 274 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 25e21942d..903e2bbd2 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -96,10 +96,10 @@ private class SyntaxCompatibilityVisitor : AstVisitor2 #else private class SyntaxCompatibilityVisitor : AstVisitor #endif - { - private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; + { + private const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; - private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; + private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; private readonly IEnumerable _jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); @@ -119,321 +119,321 @@ private class SyntaxCompatibilityVisitor : AstVisitor private readonly Dictionary> _varsDeclaredPerSession = new Dictionary>(); - private readonly List _diagnosticAccumulator; + private readonly List _diagnosticAccumulator; - private readonly UseUsingScopeModifierInNewRunspaces _rule; + private readonly UseUsingScopeModifierInNewRunspaces _rule; - private readonly string _analyzedFilePath; + private readonly string _analyzedFilePath; - public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, string analyzedScriptPath) - { - _diagnosticAccumulator = new List(); - _rule = rule; - _analyzedFilePath = analyzedScriptPath; - } - - /// - /// GetDiagnosticRecords: Retrieves all Diagnostic Records that were generated during visiting - /// - public IEnumerable GetDiagnosticRecords() - { - return _diagnosticAccumulator; - } - - /// - /// VisitScriptBlockExpression: When a ScriptBlockExpression is visited, see if it belongs to a command that needs its variables - /// prefixed with the 'Using' scope modifier. If so, analyze the block and generate diagnostic records for variables where it is missing. - /// - /// - /// - /// AstVisitAction.Continue or AstVisitAction.SkipChildren, depending on what we found. Diagnostic records are saved in `_diagnosticAccumulator`. - /// - public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) - { - if (!(scriptBlockExpressionAst.Parent is CommandAst commandAst)) + public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, string analyzedScriptPath) { - return AstVisitAction.Continue; + _diagnosticAccumulator = new List(); + _rule = rule; + _analyzedFilePath = analyzedScriptPath; } - string cmdName = commandAst.GetCommandName(); - if (cmdName == null) + /// + /// GetDiagnosticRecords: Retrieves all Diagnostic Records that were generated during visiting + /// + public IEnumerable GetDiagnosticRecords() { - // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` - return AstVisitAction.Continue; - } - - var scriptBlockParameterAst = commandAst.CommandElements[ - commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; - - if (IsInlineScriptBlock(cmdName) || - IsJobScriptBlock(cmdName, scriptBlockParameterAst) || - IsForeachScriptBlock(cmdName, scriptBlockParameterAst) || - IsInvokeCommandComputerScriptBlock(cmdName, commandAst) || - IsDSCScriptResource(cmdName, commandAst)) - { - AnalyzeScriptBlock(scriptBlockExpressionAst); - return AstVisitAction.SkipChildren; + return _diagnosticAccumulator; } - if (IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) + /// + /// VisitScriptBlockExpression: When a ScriptBlockExpression is visited, see if it belongs to a command that needs its variables + /// prefixed with the 'Using' scope modifier. If so, analyze the block and generate diagnostic records for variables where it is missing. + /// + /// + /// + /// AstVisitAction.Continue or AstVisitAction.SkipChildren, depending on what we found. Diagnostic records are saved in `_diagnosticAccumulator`. + /// + public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { - string sessionName = GetSessionName(commandAst); + if (!(scriptBlockExpressionAst.Parent is CommandAst commandAst)) + { + return AstVisitAction.Continue; + } - IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); - if (varsInLocalAssignments != null) + string cmdName = commandAst.GetCommandName(); + if (cmdName == null) + { + // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` + return AstVisitAction.Continue; + } + + var scriptBlockParameterAst = commandAst.CommandElements[ + commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; + + if (IsInlineScriptBlock(cmdName) || + IsJobScriptBlock(cmdName, scriptBlockParameterAst) || + IsForeachScriptBlock(cmdName, scriptBlockParameterAst) || + IsInvokeCommandComputerScriptBlock(cmdName, commandAst) || + IsDSCScriptResource(cmdName, commandAst)) { - AddAssignedVarsToSession(sessionName, varsInLocalAssignments); + AnalyzeScriptBlock(scriptBlockExpressionAst); + return AstVisitAction.SkipChildren; } - GenerateDiagnosticRecords( - FindNonAssignedNonUsingVarAsts( - scriptBlockExpressionAst, - GetAssignedVarsInSession(sessionName))); + if (IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) + { + string sessionName = GetSessionName(commandAst); - return AstVisitAction.SkipChildren; - } - - return AstVisitAction.Continue; - } + IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + if (varsInLocalAssignments != null) + { + AddAssignedVarsToSession(sessionName, varsInLocalAssignments); + } - /// - /// FindVarsInAssignmentAsts: Retrieves all assigned variables from an Ast: - /// Example: `$foo = "foo"` ==> the VariableExpressionAst for $foo is returned - /// - /// - private static IEnumerable FindVarsInAssignmentAsts(Ast ast) - { - // Find all variables that are assigned within this ast - return ast.FindAll( - predicate: a => a is VariableExpressionAst varExpr && - varExpr.Parent is AssignmentStatementAst assignment && - assignment - .Left - .Equals(varExpr), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst); - } + GenerateDiagnosticRecords( + FindNonAssignedNonUsingVarAsts( + scriptBlockExpressionAst, + GetAssignedVarsInSession(sessionName))); - /// - /// FindNonAssignedNonUsingVarAsts: Retrieve variables that are: - /// - not assigned before - /// - not prefixed with the 'Using' scope modifier - /// - not a PowerShell special variable - /// - /// - /// - private static List FindNonAssignedNonUsingVarAsts( - Ast ast, IEnumerable varsInAssignments) - { - // Find all variables that are not locally assigned, and don't have $using: scope modifier - return ast.FindAll( - predicate: a => a is VariableExpressionAst varAst && - !(varAst.Parent is UsingExpressionAst) && - varsInAssignments.All( - b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && - !Helper - .Instance - .HasSpecialVars(varAst.VariablePath.UserPath), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst).ToList(); - } + return AstVisitAction.SkipChildren; + } + + return AstVisitAction.Continue; + } - /// - /// GetSuggestedCorrections: Retrieves a CorrectionExtent for a given variable - /// - /// - private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) - { - var varWithUsing = $"$using:{ast.VariablePath.UserPath}"; - var description = string.Format( - CultureInfo.CurrentCulture, - Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, - ast.Extent.Text, - varWithUsing); + /// + /// FindVarsInAssignmentAsts: Retrieves all assigned variables from an Ast: + /// Example: `$foo = "foo"` ==> the VariableExpressionAst for $foo is returned + /// + /// + private static IEnumerable FindVarsInAssignmentAsts(Ast ast) + { + // Find all variables that are assigned within this ast + return ast.FindAll( + predicate: a => a is VariableExpressionAst varExpr && + varExpr.Parent is AssignmentStatementAst assignment && + assignment + .Left + .Equals(varExpr), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst); + } - return new[] + /// + /// FindNonAssignedNonUsingVarAsts: Retrieve variables that are: + /// - not assigned before + /// - not prefixed with the 'Using' scope modifier + /// - not a PowerShell special variable + /// + /// + /// + private static List FindNonAssignedNonUsingVarAsts( + Ast ast, IEnumerable varsInAssignments) { - new CorrectionExtent( - startLineNumber: ast.Extent.StartLineNumber, - endLineNumber: ast.Extent.EndLineNumber, - startColumnNumber: ast.Extent.StartColumnNumber, - endColumnNumber: ast.Extent.EndColumnNumber, - text: varWithUsing, - file: ast.Extent.File, - description: description - ) - }; - } + // Find all variables that are not locally assigned, and don't have $using: scope modifier + return ast.FindAll( + predicate: a => a is VariableExpressionAst varAst && + !(varAst.Parent is UsingExpressionAst) && + varsInAssignments.All( + b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && + !Helper + .Instance + .HasSpecialVars(varAst.VariablePath.UserPath), + searchNestedScriptBlocks: true) + .Select(a => a as VariableExpressionAst).ToList(); + } - /// - /// GetSessionName: Retrieves the name of the session (that Invoke-Command is run with). - /// - /// - private static string GetSessionName(CommandAst commandAst) - { - if (!(commandAst.CommandElements.First( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.Equals( - "session", StringComparison.OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) + /// + /// GetSuggestedCorrections: Retrieves a CorrectionExtent for a given variable + /// + /// + private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) { - return ""; + var varWithUsing = $"$using:{ast.VariablePath.UserPath}"; + var description = string.Format( + CultureInfo.CurrentCulture, + Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, + ast.Extent.Text, + varWithUsing); + + return new[] + { + new CorrectionExtent( + startLineNumber: ast.Extent.StartLineNumber, + endLineNumber: ast.Extent.EndLineNumber, + startColumnNumber: ast.Extent.StartColumnNumber, + endColumnNumber: ast.Extent.EndColumnNumber, + text: varWithUsing, + file: ast.Extent.File, + description: description + ) + }; } - int sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); + /// + /// GetSessionName: Retrieves the name of the session (that Invoke-Command is run with). + /// + /// + private static string GetSessionName(CommandAst commandAst) + { + if (!(commandAst.CommandElements.First( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.Equals( + "session", StringComparison.OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) + { + return ""; + } - return commandAst - .CommandElements[sessionParamAstIndex + 1] - .Extent - .Text - .Trim(); - } + int sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); - /// - /// GetAssignedVarsInSession: Retrieves all previously declared vars for a given session (as in Invoke-Command -Session $session). - /// - /// - private IEnumerable GetAssignedVarsInSession(string sessionName) - { - return _varsDeclaredPerSession[sessionName]; - } + return commandAst + .CommandElements[sessionParamAstIndex + 1] + .Extent + .Text + .Trim(); + } - /// - /// AddAssignedVarsToSession: Adds variables to the list of assigned variables for a given Invoke-Command session. - /// - /// - /// - private void AddAssignedVarsToSession(string sessionName, IEnumerable variablesToAdd) - { - if (!_varsDeclaredPerSession.ContainsKey(sessionName)) + /// + /// GetAssignedVarsInSession: Retrieves all previously declared vars for a given session (as in Invoke-Command -Session $session). + /// + /// + private IEnumerable GetAssignedVarsInSession(string sessionName) { - _varsDeclaredPerSession.Add(sessionName, new List()); + return _varsDeclaredPerSession[sessionName]; } - _varsDeclaredPerSession[sessionName].AddRange(variablesToAdd); - } + /// + /// AddAssignedVarsToSession: Adds variables to the list of assigned variables for a given Invoke-Command session. + /// + /// + /// + private void AddAssignedVarsToSession(string sessionName, IEnumerable variablesToAdd) + { + if (!_varsDeclaredPerSession.ContainsKey(sessionName)) + { + _varsDeclaredPerSession.Add(sessionName, new List()); + } - /// - /// AnalyzeScriptBlock: Generate a Diagnostic Record for each incorrectly used variable inside a given ScriptBlock. - /// - /// - private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) - { - List nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, - FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); + _varsDeclaredPerSession[sessionName].AddRange(variablesToAdd); + } - GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); - } + /// + /// AnalyzeScriptBlock: Generate a Diagnostic Record for each incorrectly used variable inside a given ScriptBlock. + /// + /// + private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + List nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); - /// - /// GenerateDiagnosticRecords: Add Diagnostic Records to the internal list for each given variable - /// - /// - private void GenerateDiagnosticRecords(IEnumerable nonAssignedNonUsingVarAsts) - { - foreach (var variableExpression in nonAssignedNonUsingVarAsts) + GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); + } + + /// + /// GenerateDiagnosticRecords: Add Diagnostic Records to the internal list for each given variable + /// + /// + private void GenerateDiagnosticRecords(IEnumerable nonAssignedNonUsingVarAsts) { - if (variableExpression == null) + foreach (var variableExpression in nonAssignedNonUsingVarAsts) { - continue; + if (variableExpression == null) + { + continue; + } + + _diagnosticAccumulator.Add(new DiagnosticRecord( + message: string.Format(CultureInfo.CurrentCulture, + Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), + extent: variableExpression.Extent, + ruleName: _rule.GetName(), + severity: Severity, + scriptPath: _analyzedFilePath, + ruleId: _rule.GetName(), + suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); } - - _diagnosticAccumulator.Add(new DiagnosticRecord( - message: string.Format(CultureInfo.CurrentCulture, - Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), - extent: variableExpression.Extent, - ruleName: _rule.GetName(), - severity: Severity, - scriptPath: _analyzedFilePath, - ruleId: _rule.GetName(), - suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); } - } - /// - /// IsInvokeCommandSessionScriptBlock: Returns true if: - /// - command is 'Invoke-Command' (or alias) - /// - parameter '-Session' is present - /// - /// - /// - private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) - { - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.Equals("session", StringComparison.OrdinalIgnoreCase)); - } + /// + /// IsInvokeCommandSessionScriptBlock: Returns true if: + /// - command is 'Invoke-Command' (or alias) + /// - parameter '-Session' is present + /// + /// + /// + private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) + { + return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.Equals("session", StringComparison.OrdinalIgnoreCase)); + } - /// - /// IsInvokeCommandComputerScriptBlock: Returns true if: - /// - command is 'Invoke-Command' (or alias) - /// - parameter '-Computer' is present - /// - /// - /// - private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) - { - // 'com' is the shortest unambiguous form for the '-Computer' parameter - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements.Any( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); - } + /// + /// IsInvokeCommandComputerScriptBlock: Returns true if: + /// - command is 'Invoke-Command' (or alias) + /// - parameter '-Computer' is present + /// + /// + /// + private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) + { + // 'com' is the shortest unambiguous form for the '-Computer' parameter + return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements.Any( + e => e is CommandParameterAst parameterAst && + parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); + } - /// - /// IsForeachScriptBlock: Returns true if: - /// - command is 'Foreach-Object' (or alias) - /// - parameter '-Parallel' is present - /// - /// - /// - private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) - { - // 'pa' is the shortest unambiguous form for the '-Parallel' parameter - return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - (scriptBlockParameterAst != null && - scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); - } + /// + /// IsForeachScriptBlock: Returns true if: + /// - command is 'Foreach-Object' (or alias) + /// - parameter '-Parallel' is present + /// + /// + /// + private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + // 'pa' is the shortest unambiguous form for the '-Parallel' parameter + return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + (scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); + } - /// - /// IsJobScriptBlock: Returns true if: - /// - command is 'Start-Job' or 'Start-ThreadJob' (or alias) - /// - parameter name for this ScriptBlock not '-InitializationScript' - /// - /// - /// - private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) - { - // 'ini' is the shortest unambiguous form for the '-InitializationScript' parameter - return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || - _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && - !(scriptBlockParameterAst != null && - scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); - } + /// + /// IsJobScriptBlock: Returns true if: + /// - command is 'Start-Job' or 'Start-ThreadJob' (or alias) + /// - parameter name for this ScriptBlock not '-InitializationScript' + /// + /// + /// + private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) + { + // 'ini' is the shortest unambiguous form for the '-InitializationScript' parameter + return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || + _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && + !(scriptBlockParameterAst != null && + scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); + } - /// - /// IsInlineScriptBlock: Returns true if: - /// - command is 'InlineScript' (or alias) - /// - /// - private bool IsInlineScriptBlock(string cmdName) - { - return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); - } + /// + /// IsInlineScriptBlock: Returns true if: + /// - command is 'InlineScript' (or alias) + /// + /// + private bool IsInlineScriptBlock(string cmdName) + { + return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); + } - /// - /// IsDSCScriptResource: Returns true if: - /// - command is 'GetScript', 'TestScript' or 'SetScript' - /// - /// - private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) - { - // Inside DSC Script resource, GetScript is of the form 'Script foo { GetScript = {} }' - // If we reach this point in the code, we are sure there are - return s_dscScriptResourceCommandNames.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && - commandAst.CommandElements[1].ToString() == "="; + /// + /// IsDSCScriptResource: Returns true if: + /// - command is 'GetScript', 'TestScript' or 'SetScript' + /// + /// + private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) + { + // Inside DSC Script resource, GetScript is of the form 'Script foo { GetScript = {} }' + // If we reach this point in the code, we are sure there are + return s_dscScriptResourceCommandNames.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + commandAst.CommandElements[1].ToString() == "="; + } } } } -} From a6acfd7bec4e3a3bce36584ecb9897443b612453 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 11:27:25 +0100 Subject: [PATCH 45/55] Remove explicit 'ToList()' for performance Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index d17075a48..155932129 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -313,7 +313,8 @@ private void AddAssignedVarsToSession(string sessionName, IEnumerable Date: Fri, 13 Mar 2020 11:57:25 +0100 Subject: [PATCH 46/55] add check for strongly typed assignments --- Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 index 0e7ea1ff8..66452baf6 100644 --- a/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 +++ b/Tests/Rules/UseUsingScopeModifierInNewRunspaces.tests.ps1 @@ -90,7 +90,7 @@ Describe "UseUsingScopeModifierInNewRunspaces" { ScriptBlock = '{ $session = new-PSSession -ComputerName "baz" $otherSession = new-PSSession -ComputerName "bar" - Invoke-Command -session $session -ScriptBlock {$foo = "foo" } + Invoke-Command -session $session -ScriptBlock {[string]$foo = "foo" } Invoke-Command -session $otherSession -ScriptBlock {Write-Output $foo} }' } @@ -165,7 +165,7 @@ Describe "UseUsingScopeModifierInNewRunspaces" { @{ Description = "Foreach-Object -Parallel with var assigned locally" ScriptBlock = '{ - 1..2 | ForEach-Object -Parallel { $var="somevalue" } + 1..2 | ForEach-Object -Parallel { [string]$var="somevalue" } }' } @{ From c4c4fded53533c68bf4f5edcb200f3734eecfce8 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 12:08:00 +0100 Subject: [PATCH 47/55] Add todo comments --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 903e2bbd2..a81d8c09b 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -203,6 +203,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA /// private static IEnumerable FindVarsInAssignmentAsts(Ast ast) { + // TODO: rewrite as visitor method, and fix the case of `[int]$x = 10`, where LHS doesn't match varExpr. // Find all variables that are assigned within this ast return ast.FindAll( predicate: a => a is VariableExpressionAst varExpr && @@ -225,6 +226,7 @@ varExpr.Parent is AssignmentStatementAst assignment && private static List FindNonAssignedNonUsingVarAsts( Ast ast, IEnumerable varsInAssignments) { + // TODO: rewrite as visitor method // Find all variables that are not locally assigned, and don't have $using: scope modifier return ast.FindAll( predicate: a => a is VariableExpressionAst varAst && From 490d8d9692bc9576de6a8128fddf49c5efa99fcf Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 13 Mar 2020 22:03:16 +0100 Subject: [PATCH 48/55] refactor FindAll predicates to static methods to avoid closure allocation --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 85 +++++++++++++------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index a81d8c09b..0fcf3968d 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -155,13 +155,14 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - string cmdName = commandAst.GetCommandName(); + string cmdName = commandAst.GetCommandName(); if (cmdName == null) { // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` return AstVisitAction.Continue; } + // We need this information, because some cmdlets can have more than one ScriptBlock parameter var scriptBlockParameterAst = commandAst.CommandElements[ commandAst.CommandElements.IndexOf(scriptBlockExpressionAst) - 1] as CommandParameterAst; @@ -179,7 +180,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA { string sessionName = GetSessionName(commandAst); - IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); if (varsInLocalAssignments != null) { AddAssignedVarsToSession(sessionName, varsInLocalAssignments); @@ -203,16 +204,29 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA /// private static IEnumerable FindVarsInAssignmentAsts(Ast ast) { - // TODO: rewrite as visitor method, and fix the case of `[int]$x = 10`, where LHS doesn't match varExpr. // Find all variables that are assigned within this ast - return ast.FindAll( - predicate: a => a is VariableExpressionAst varExpr && - varExpr.Parent is AssignmentStatementAst assignment && - assignment - .Left - .Equals(varExpr), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst); + foreach (Ast foundAst in ast.FindAll(VarsInAssignments, true)) + { + var assignment = foundAst as AssignmentStatementAst; + + if (assignment.Left is VariableExpressionAst variable) + { + yield return variable; + } + else if (assignment.Left is ConvertExpressionAst conversion) + { + yield return conversion.Child as VariableExpressionAst; + } + }; + } + + /// + /// VarsInAssignments: helper function to prevent allocation of closures for FindAll predicate. + /// + /// + private static bool VarsInAssignments(Ast ast) + { + return ast is AssignmentStatementAst; } /// @@ -223,21 +237,34 @@ varExpr.Parent is AssignmentStatementAst assignment && /// /// /// - private static List FindNonAssignedNonUsingVarAsts( + private static IEnumerable FindNonAssignedNonUsingVarAsts( Ast ast, IEnumerable varsInAssignments) { - // TODO: rewrite as visitor method // Find all variables that are not locally assigned, and don't have $using: scope modifier - return ast.FindAll( - predicate: a => a is VariableExpressionAst varAst && - !(varAst.Parent is UsingExpressionAst) && - varsInAssignments.All( - b => b.VariablePath.UserPath != varAst.VariablePath.UserPath) && - !Helper - .Instance - .HasSpecialVars(varAst.VariablePath.UserPath), - searchNestedScriptBlocks: true) - .Select(a => a as VariableExpressionAst).ToList(); + foreach (Ast varAst in ast.FindAll(NonUsingNonSpecialVariables, true)) + { + var variable = varAst as VariableExpressionAst; + + foreach (var expressionAst in varsInAssignments) + { + if (expressionAst.VariablePath.UserPath == variable.VariablePath.UserPath) + { + yield break; + } + } + yield return variable; + } + } + + /// + /// NonUsingNonSpecialVariables: helper function to prevent allocation of closures for FindAll predicate. + /// + /// + private static bool NonUsingNonSpecialVariables(Ast ast) + { + return ast is VariableExpressionAst variable && + !(variable.Parent is UsingExpressionAst) && + !Helper.Instance.HasSpecialVars(variable.VariablePath.UserPath); } /// @@ -272,14 +299,14 @@ private static IEnumerable GetSuggestedCorrections(VariableExp /// /// private static string GetSessionName(CommandAst commandAst) - { + { if (!(commandAst.CommandElements.First( e => e is CommandParameterAst parameterAst && parameterAst.ParameterName.Equals( "session", StringComparison.OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) - { + { return ""; - } + } int sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); @@ -320,8 +347,8 @@ private void AddAssignedVarsToSession(string sessionName, IEnumerable private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) { - List nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, - FindVarsInAssignmentAsts(scriptBlockExpressionAst)).ToList(); + IEnumerable nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + FindVarsInAssignmentAsts(scriptBlockExpressionAst)); GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); } From 6767a5862fb17b1f2abaa4c98c9caee82a4db7c1 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Sat, 14 Mar 2020 13:12:19 +0100 Subject: [PATCH 49/55] Refactor GetSessionName for performance. --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 57 ++++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 0fcf3968d..0951bbc43 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -178,7 +178,10 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA if (IsInvokeCommandSessionScriptBlock(cmdName, commandAst)) { - string sessionName = GetSessionName(commandAst); + if (!TryGetSessionNameFromInvokeCommand(commandAst, out var sessionName)) + { + return AstVisitAction.Continue; + } IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); if (varsInLocalAssignments != null) @@ -295,26 +298,48 @@ private static IEnumerable GetSuggestedCorrections(VariableExp } /// - /// GetSessionName: Retrieves the name of the session (that Invoke-Command is run with). + /// TryGetSessionNameFromInvokeCommand: Retrieves the name of the session (that Invoke-Command is run with). /// - /// - private static string GetSessionName(CommandAst commandAst) + /// + /// + /// + private static bool TryGetSessionNameFromInvokeCommand(CommandAst invokeCommandAst, out string sessionName) + { + // Sift through Invoke-Command parameters to find the value of the -Session parameter + // Start at 1 to skip the command name + for (int i = 1; i < invokeCommandAst.CommandElements.Count; i++) + { + // We need a parameter + if (!(invokeCommandAst.CommandElements[i] is CommandParameterAst parameterAst)) { - if (!(commandAst.CommandElements.First( - e => e is CommandParameterAst parameterAst && - parameterAst.ParameterName.Equals( - "session", StringComparison.OrdinalIgnoreCase)) is CommandParameterAst sessionParameterAst)) + continue; + } + + // The parameter must be called "Session" + if (!parameterAst.ParameterName.Equals("Session", StringComparison.OrdinalIgnoreCase)) { - return ""; + continue; + } + + // If we have a partial AST, ensure we don't crash + if (i + 1 >= invokeCommandAst.CommandElements.Count) + { + break; } - int sessionParamAstIndex = commandAst.CommandElements.IndexOf(sessionParameterAst); + // The -Session parameter expects an argument of type [System.Management.Automation.Runspaces.PSSession]. + // It will typically be provided in the form of a variable. We do not support other scenarios at this time. + if (!(invokeCommandAst.CommandElements[i + 1] is VariableExpressionAst variableAst)) + { + break; + } + + sessionName = variableAst.VariablePath.UserPath; + return true; + } - return commandAst - .CommandElements[sessionParamAstIndex + 1] - .Extent - .Text - .Trim(); + sessionName = null; + return false; } /// From c824fd790c9f60217f8f3d0c1fea6eb23918ff1d Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 19 Mar 2020 20:26:53 +0100 Subject: [PATCH 50/55] use full type for string expression variable Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 0951bbc43..b27bfd3c1 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -276,7 +276,8 @@ private static bool NonUsingNonSpecialVariables(Ast ast) /// private static IEnumerable GetSuggestedCorrections(VariableExpressionAst ast) { - var varWithUsing = $"$using:{ast.VariablePath.UserPath}"; + string varWithUsing = $"$using:{ast.VariablePath.UserPath}"; + var description = string.Format( CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesCorrectionDescription, From ab64ebf1317a529f9b193f01a0a550a4ba1e89e7 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 19 Mar 2020 20:41:47 +0100 Subject: [PATCH 51/55] use full type name for foreach variable initialization Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index b27bfd3c1..364c18735 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -385,7 +385,8 @@ private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAs /// private void GenerateDiagnosticRecords(IEnumerable nonAssignedNonUsingVarAsts) { - foreach (var variableExpression in nonAssignedNonUsingVarAsts) + foreach (VariableExpressionAst variableExpression in nonAssignedNonUsingVarAsts) + { if (variableExpression == null) { From d5b77a9940f40c92adbdbefadf7725af5ab5f772 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 19 Mar 2020 20:45:43 +0100 Subject: [PATCH 52/55] refactor diagnostic message out to local variable Co-Authored-By: Robert Holt --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index 364c18735..ef18afeb8 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -393,15 +393,21 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss continue; } - _diagnosticAccumulator.Add(new DiagnosticRecord( - message: string.Format(CultureInfo.CurrentCulture, - Strings.UseUsingScopeModifierInNewRunspacesError, variableExpression.ToString()), - extent: variableExpression.Extent, - ruleName: _rule.GetName(), - severity: Severity, - scriptPath: _analyzedFilePath, - ruleId: _rule.GetName(), - suggestedCorrections: GetSuggestedCorrections(ast: variableExpression))); + string diagnosticMessage = string.Format( + CultureInfo.CurrentCulture, + Strings.UseUsingScopeModifierInNewRunspacesError, + variableExpression.ToString()); + + _diagnosticAccumulator.Add( + new DiagnosticRecord( + diagnosticMessage, + variableExpression.Extent, + _rule.GetName(), + Severity, + _analyzedFilePath, + ruleId: _rule.GetName(), + GetSuggestedCorrections(ast: variableExpression))); + } } From 6c0e90acfaab4c946e499f230a595a3ae386c01c Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Thu, 19 Mar 2020 21:54:58 +0100 Subject: [PATCH 53/55] WIP: apply review suggestions refactoring FindVarsInAssignmentAsts to return a dictionary in progress --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 94 ++++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index ef18afeb8..86fe8d194 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; #if !CORECLR using System.ComponentModel.Composition; #endif @@ -29,7 +30,6 @@ public class UseUsingScopeModifierInNewRunspaces : IScriptRule /// A List of results from this rule public IEnumerable AnalyzeScript(Ast ast, string fileName) { - var visitor = new SyntaxCompatibilityVisitor(this, fileName); ast.Visit(visitor); return visitor.GetDiagnosticRecords(); @@ -101,19 +101,19 @@ private class SyntaxCompatibilityVisitor : AstVisitor private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; - private readonly IEnumerable _jobCmdletNamesAndAliases = + private static readonly IEnumerable s_jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); - private readonly IEnumerable _threadJobCmdletNamesAndAliases = + private static readonly IEnumerable s_threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); - private readonly IEnumerable _inlineScriptCmdletNamesAndAliases = + private static readonly IEnumerable s_inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); - private readonly IEnumerable _foreachObjectCmdletNamesAndAliases = + private static readonly IEnumerable s_foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = + private static readonly IEnumerable s_invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); private readonly Dictionary> _varsDeclaredPerSession = @@ -159,7 +159,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA if (cmdName == null) { // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` - return AstVisitAction.Continue; + return AstVisitAction.SkipChildren; } // We need this information, because some cmdlets can have more than one ScriptBlock parameter @@ -183,7 +183,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + IReadOnlyDictionary varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); if (varsInLocalAssignments != null) { AddAssignedVarsToSession(sessionName, varsInLocalAssignments); @@ -205,29 +205,52 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA /// Example: `$foo = "foo"` ==> the VariableExpressionAst for $foo is returned /// /// - private static IEnumerable FindVarsInAssignmentAsts(Ast ast) + private static IReadOnlyDictionary FindVarsInAssignmentAsts(Ast ast) { + Dictionary variableDictionary = + new Dictionary(); + // Find all variables that are assigned within this ast - foreach (Ast foundAst in ast.FindAll(VarsInAssignments, true)) + foreach (AssignmentStatementAst statementAst in ast.FindAll(IsAssignmentStatementAst, true)) { - var assignment = foundAst as AssignmentStatementAst; - - if (assignment.Left is VariableExpressionAst variable) + if (TryGetVariableFromExpression(statementAst.Left, out VariableExpressionAst variable)) { - yield return variable; - } - else if (assignment.Left is ConvertExpressionAst conversion) - { - yield return conversion.Child as VariableExpressionAst; + string variableName = string.Format(variable.VariablePath.UserPath, + StringComparer.OrdinalIgnoreCase); + variableDictionary.Add(variableName, variable); } }; + + return new ReadOnlyDictionary(variableDictionary); } /// - /// VarsInAssignments: helper function to prevent allocation of closures for FindAll predicate. + /// TryGetVariableFromExpression: extracts the variable from an expression like an assignment + /// + /// + /// + private static bool TryGetVariableFromExpression(ExpressionAst expression, out VariableExpressionAst variableExpressionAst) + { + switch (expression) + { + case VariableExpressionAst variable: + variableExpressionAst = variable; + return true; + + case AttributedExpressionAst attributedAst: + return TryGetVariableFromExpression(attributedAst.Child, out variableExpressionAst); + + default: + variableExpressionAst = null; + return false; + } + } + + /// + /// IsAssignmentStatementAst: helper function to prevent allocation of closures for FindAll predicate. /// /// - private static bool VarsInAssignments(Ast ast) + private static bool IsAssignmentStatementAst(Ast ast) { return ast is AssignmentStatementAst; } @@ -244,10 +267,8 @@ private static IEnumerable FindNonAssignedNonUsingVarAsts Ast ast, IEnumerable varsInAssignments) { // Find all variables that are not locally assigned, and don't have $using: scope modifier - foreach (Ast varAst in ast.FindAll(NonUsingNonSpecialVariables, true)) + foreach (VariableExpressionAst variable in ast.FindAll(IsNonUsingNonSpecialVariableExpressionAst, true)) { - var variable = varAst as VariableExpressionAst; - foreach (var expressionAst in varsInAssignments) { if (expressionAst.VariablePath.UserPath == variable.VariablePath.UserPath) @@ -260,10 +281,10 @@ private static IEnumerable FindNonAssignedNonUsingVarAsts } /// - /// NonUsingNonSpecialVariables: helper function to prevent allocation of closures for FindAll predicate. + /// IsNonUsingNonSpecialVariableExpressionAst: helper function to prevent allocation of closures for FindAll predicate. /// /// - private static bool NonUsingNonSpecialVariables(Ast ast) + private static bool IsNonUsingNonSpecialVariableExpressionAst(Ast ast) { return ast is VariableExpressionAst variable && !(variable.Parent is UsingExpressionAst) && @@ -373,7 +394,8 @@ private void AddAssignedVarsToSession(string sessionName, IEnumerable private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) { - IEnumerable nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + IEnumerable nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts( + scriptBlockExpressionAst, FindVarsInAssignmentAsts(scriptBlockExpressionAst)); GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); @@ -388,11 +410,6 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss foreach (VariableExpressionAst variableExpression in nonAssignedNonUsingVarAsts) { - if (variableExpression == null) - { - continue; - } - string diagnosticMessage = string.Format( CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesError, @@ -407,7 +424,6 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss _analyzedFilePath, ruleId: _rule.GetName(), GetSuggestedCorrections(ast: variableExpression))); - } } @@ -420,7 +436,7 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss /// private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) { - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && parameterAst.ParameterName.Equals("session", StringComparison.OrdinalIgnoreCase)); @@ -436,7 +452,7 @@ private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst comman private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) { // 'com' is the shortest unambiguous form for the '-Computer' parameter - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); @@ -452,7 +468,7 @@ private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst comma private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { // 'pa' is the shortest unambiguous form for the '-Parallel' parameter - return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && (scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); } @@ -467,8 +483,8 @@ private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBloc private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { // 'ini' is the shortest unambiguous form for the '-InitializationScript' parameter - return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || - _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && + return (s_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || + s_threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && !(scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); } @@ -480,7 +496,7 @@ private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockPar /// private bool IsInlineScriptBlock(string cmdName) { - return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); + return s_inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); } @@ -492,7 +508,7 @@ private bool IsInlineScriptBlock(string cmdName) private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) { // Inside DSC Script resource, GetScript is of the form 'Script foo { GetScript = {} }' - // If we reach this point in the code, we are sure there are + // If we reach this point in the code, we are sure there are at least two CommandElements, so the index of [1] will not fail. return s_dscScriptResourceCommandNames.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements[1].ToString() == "="; } From fa7c06572a928b6ff6324367b95c2975d94fc324 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 20 Mar 2020 14:31:37 +0100 Subject: [PATCH 54/55] apply review suggestions refactoredFindVarsInAssignmentAsts to return a dictionary --- Rules/UseUsingScopeModifierInNewRunspaces.cs | 125 +++++++++++-------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/Rules/UseUsingScopeModifierInNewRunspaces.cs b/Rules/UseUsingScopeModifierInNewRunspaces.cs index ef18afeb8..1db914276 100644 --- a/Rules/UseUsingScopeModifierInNewRunspaces.cs +++ b/Rules/UseUsingScopeModifierInNewRunspaces.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; #if !CORECLR using System.ComponentModel.Composition; #endif @@ -29,7 +30,6 @@ public class UseUsingScopeModifierInNewRunspaces : IScriptRule /// A List of results from this rule public IEnumerable AnalyzeScript(Ast ast, string fileName) { - var visitor = new SyntaxCompatibilityVisitor(this, fileName); ast.Visit(visitor); return visitor.GetDiagnosticRecords(); @@ -101,24 +101,23 @@ private class SyntaxCompatibilityVisitor : AstVisitor private static readonly string[] s_dscScriptResourceCommandNames = {"GetScript", "TestScript", "SetScript"}; - private readonly IEnumerable _jobCmdletNamesAndAliases = + private static readonly IEnumerable s_jobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-Job"); - private readonly IEnumerable _threadJobCmdletNamesAndAliases = + private static readonly IEnumerable s_threadJobCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Start-ThreadJob"); - private readonly IEnumerable _inlineScriptCmdletNamesAndAliases = + private static readonly IEnumerable s_inlineScriptCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("InlineScript"); - private readonly IEnumerable _foreachObjectCmdletNamesAndAliases = + private static readonly IEnumerable s_foreachObjectCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Foreach-Object"); - private readonly IEnumerable _invokeCommandCmdletNamesAndAliases = + private static readonly IEnumerable s_invokeCommandCmdletNamesAndAliases = Helper.Instance.CmdletNameAndAliases("Invoke-Command"); - private readonly Dictionary> _varsDeclaredPerSession = - new Dictionary>(); - + private readonly Dictionary> _varsDeclaredPerSession; + private readonly List _diagnosticAccumulator; private readonly UseUsingScopeModifierInNewRunspaces _rule; @@ -128,6 +127,7 @@ private class SyntaxCompatibilityVisitor : AstVisitor public SyntaxCompatibilityVisitor(UseUsingScopeModifierInNewRunspaces rule, string analyzedScriptPath) { _diagnosticAccumulator = new List(); + _varsDeclaredPerSession = new Dictionary>(); _rule = rule; _analyzedFilePath = analyzedScriptPath; } @@ -159,7 +159,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA if (cmdName == null) { // Skip for situations where command name cannot be resolved like `& $commandName -ComputerName -ScriptBlock { $foo }` - return AstVisitAction.Continue; + return AstVisitAction.SkipChildren; } // We need this information, because some cmdlets can have more than one ScriptBlock parameter @@ -183,7 +183,7 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA return AstVisitAction.Continue; } - IEnumerable varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); + IReadOnlyDictionary varsInLocalAssignments = FindVarsInAssignmentAsts(scriptBlockExpressionAst); if (varsInLocalAssignments != null) { AddAssignedVarsToSession(sessionName, varsInLocalAssignments); @@ -205,29 +205,52 @@ public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionA /// Example: `$foo = "foo"` ==> the VariableExpressionAst for $foo is returned /// /// - private static IEnumerable FindVarsInAssignmentAsts(Ast ast) + private static IReadOnlyDictionary FindVarsInAssignmentAsts(Ast ast) { + Dictionary variableDictionary = + new Dictionary(); + // Find all variables that are assigned within this ast - foreach (Ast foundAst in ast.FindAll(VarsInAssignments, true)) + foreach (AssignmentStatementAst statementAst in ast.FindAll(IsAssignmentStatementAst, true)) { - var assignment = foundAst as AssignmentStatementAst; - - if (assignment.Left is VariableExpressionAst variable) - { - yield return variable; - } - else if (assignment.Left is ConvertExpressionAst conversion) + if (TryGetVariableFromExpression(statementAst.Left, out VariableExpressionAst variable)) { - yield return conversion.Child as VariableExpressionAst; + string variableName = string.Format(variable.VariablePath.UserPath, + StringComparer.OrdinalIgnoreCase); + variableDictionary.Add(variableName, variable); } }; + + return new ReadOnlyDictionary(variableDictionary); + } + + /// + /// TryGetVariableFromExpression: extracts the variable from an expression like an assignment + /// + /// + /// + private static bool TryGetVariableFromExpression(ExpressionAst expression, out VariableExpressionAst variableExpressionAst) + { + switch (expression) + { + case VariableExpressionAst variable: + variableExpressionAst = variable; + return true; + + case AttributedExpressionAst attributedAst: + return TryGetVariableFromExpression(attributedAst.Child, out variableExpressionAst); + + default: + variableExpressionAst = null; + return false; + } } /// - /// VarsInAssignments: helper function to prevent allocation of closures for FindAll predicate. + /// IsAssignmentStatementAst: helper function to prevent allocation of closures for FindAll predicate. /// /// - private static bool VarsInAssignments(Ast ast) + private static bool IsAssignmentStatementAst(Ast ast) { return ast is AssignmentStatementAst; } @@ -241,29 +264,27 @@ private static bool VarsInAssignments(Ast ast) /// /// private static IEnumerable FindNonAssignedNonUsingVarAsts( - Ast ast, IEnumerable varsInAssignments) + Ast ast, IReadOnlyDictionary varsInAssignments) { // Find all variables that are not locally assigned, and don't have $using: scope modifier - foreach (Ast varAst in ast.FindAll(NonUsingNonSpecialVariables, true)) + foreach (VariableExpressionAst variable in ast.FindAll(IsNonUsingNonSpecialVariableExpressionAst, true)) { - var variable = varAst as VariableExpressionAst; - - foreach (var expressionAst in varsInAssignments) + var varName = string.Format(variable.VariablePath.UserPath, StringComparer.OrdinalIgnoreCase); + + if (varsInAssignments.ContainsKey(varName)) { - if (expressionAst.VariablePath.UserPath == variable.VariablePath.UserPath) - { - yield break; - } + yield break; } + yield return variable; } } /// - /// NonUsingNonSpecialVariables: helper function to prevent allocation of closures for FindAll predicate. + /// IsNonUsingNonSpecialVariableExpressionAst: helper function to prevent allocation of closures for FindAll predicate. /// /// - private static bool NonUsingNonSpecialVariables(Ast ast) + private static bool IsNonUsingNonSpecialVariableExpressionAst(Ast ast) { return ast is VariableExpressionAst variable && !(variable.Parent is UsingExpressionAst) && @@ -347,7 +368,7 @@ private static bool TryGetSessionNameFromInvokeCommand(CommandAst invokeCommandA /// GetAssignedVarsInSession: Retrieves all previously declared vars for a given session (as in Invoke-Command -Session $session). /// /// - private IEnumerable GetAssignedVarsInSession(string sessionName) + private IReadOnlyDictionary GetAssignedVarsInSession(string sessionName) { return _varsDeclaredPerSession[sessionName]; } @@ -357,14 +378,17 @@ private IEnumerable GetAssignedVarsInSession(string sessi /// /// /// - private void AddAssignedVarsToSession(string sessionName, IEnumerable variablesToAdd) + private void AddAssignedVarsToSession(string sessionName, IReadOnlyDictionary variablesToAdd) { if (!_varsDeclaredPerSession.ContainsKey(sessionName)) { - _varsDeclaredPerSession.Add(sessionName, new List()); + _varsDeclaredPerSession.Add(sessionName, new Dictionary()); } - _varsDeclaredPerSession[sessionName].AddRange(variablesToAdd); + foreach (var item in variablesToAdd) + { + _varsDeclaredPerSession[sessionName].Add(item.Key, item.Value); + } } /// @@ -373,7 +397,8 @@ private void AddAssignedVarsToSession(string sessionName, IEnumerable private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAst) { - IEnumerable nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts(scriptBlockExpressionAst, + IEnumerable nonAssignedNonUsingVarAsts = FindNonAssignedNonUsingVarAsts( + scriptBlockExpressionAst, FindVarsInAssignmentAsts(scriptBlockExpressionAst)); GenerateDiagnosticRecords(nonAssignedNonUsingVarAsts); @@ -386,13 +411,7 @@ private void AnalyzeScriptBlock(ScriptBlockExpressionAst scriptBlockExpressionAs private void GenerateDiagnosticRecords(IEnumerable nonAssignedNonUsingVarAsts) { foreach (VariableExpressionAst variableExpression in nonAssignedNonUsingVarAsts) - { - if (variableExpression == null) - { - continue; - } - string diagnosticMessage = string.Format( CultureInfo.CurrentCulture, Strings.UseUsingScopeModifierInNewRunspacesError, @@ -407,7 +426,6 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss _analyzedFilePath, ruleId: _rule.GetName(), GetSuggestedCorrections(ast: variableExpression))); - } } @@ -420,7 +438,7 @@ private void GenerateDiagnosticRecords(IEnumerable nonAss /// private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst commandAst) { - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && parameterAst.ParameterName.Equals("session", StringComparison.OrdinalIgnoreCase)); @@ -436,7 +454,7 @@ private bool IsInvokeCommandSessionScriptBlock(string cmdName, CommandAst comman private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst commandAst) { // 'com' is the shortest unambiguous form for the '-Computer' parameter - return _invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_invokeCommandCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements.Any( e => e is CommandParameterAst parameterAst && parameterAst.ParameterName.StartsWith("com", StringComparison.OrdinalIgnoreCase)); @@ -452,7 +470,7 @@ private bool IsInvokeCommandComputerScriptBlock(string cmdName, CommandAst comma private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { // 'pa' is the shortest unambiguous form for the '-Parallel' parameter - return _foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && + return s_foreachObjectCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && (scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("pa", StringComparison.OrdinalIgnoreCase)); } @@ -467,8 +485,8 @@ private bool IsForeachScriptBlock(string cmdName, CommandParameterAst scriptBloc private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockParameterAst) { // 'ini' is the shortest unambiguous form for the '-InitializationScript' parameter - return (_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || - _threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && + return (s_jobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase) || + s_threadJobCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase)) && !(scriptBlockParameterAst != null && scriptBlockParameterAst.ParameterName.StartsWith("ini", StringComparison.OrdinalIgnoreCase)); } @@ -480,10 +498,9 @@ private bool IsJobScriptBlock(string cmdName, CommandParameterAst scriptBlockPar /// private bool IsInlineScriptBlock(string cmdName) { - return _inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); + return s_inlineScriptCmdletNamesAndAliases.Contains(cmdName, StringComparer.OrdinalIgnoreCase); } - /// /// IsDSCScriptResource: Returns true if: /// - command is 'GetScript', 'TestScript' or 'SetScript' @@ -492,7 +509,7 @@ private bool IsInlineScriptBlock(string cmdName) private bool IsDSCScriptResource(string cmdName, CommandAst commandAst) { // Inside DSC Script resource, GetScript is of the form 'Script foo { GetScript = {} }' - // If we reach this point in the code, we are sure there are + // If we reach this point in the code, we are sure there are at least two CommandElements, so the index of [1] will not fail. return s_dscScriptResourceCommandNames.Contains(cmdName, StringComparer.OrdinalIgnoreCase) && commandAst.CommandElements[1].ToString() == "="; } From c1247c033eaed73dc951e2836189d4297a5bbe22 Mon Sep 17 00:00:00 2001 From: Jos Koelewijn Date: Fri, 20 Mar 2020 15:17:04 +0100 Subject: [PATCH 55/55] Fix CR/LF -> LF --- Rules/Strings.resx | 2280 ++++++++++++++++++++++---------------------- 1 file changed, 1140 insertions(+), 1140 deletions(-) diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 7cfe0186b..e64cecd6b 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1,1140 +1,1140 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability. - - - Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. - - - Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks. - - - Avoid Using Empty Catch Block - - - The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please remove Invoke-Expression from script and find other options instead. - - - Avoid Using Invoke-Expression - - - Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. - - - Avoid Using Positional Parameters - - - Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment. - - - The cmdlet '{0}' does not have a help comment. - - - Basic Comment Help - - - Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices. - - - The cmdlet '{0}' uses an unapproved verb. - - - Cmdlet Verbs - - - Ensure declared variables are used elsewhere in the script and not just during assignment. - - - The variable '{0}' is assigned but never used. - - - Extra Variables - - - Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems. - - - Found global variable '{0}'. - - - No Global Variables - - - Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. - - - $null should be on the left side of equality comparisons. - - - Null Comparison - - - Checks that cmdlets and parameters have more than one character. - - - The cmdlet name '{0}' only has one character. - - - The cmdlet '{0}' has a parameter '{1}' that only has one character. - - - A script block has a parameter '{0}' that only has one character. - - - One Char - - - For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. - - - The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. - - - The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. - - - Use PSCredential type. - - - Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors. - - - The cmdlet '{0}' uses a reserved char in its name. - - - Reserved Cmdlet Chars - - - The cmdlet '{0}' - - - Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs. - - - '{0}' defines the reserved common parameter '{1}'. - - - Reserved Parameters - - - The script - - - #,(){}[]&/\\$^;:\"'<>|?@`*%+=~ - - - Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior. - - - '{0}' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. - - - A script block has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. - - - '{0}' calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. - - - A script block calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. - - - Should Process - - - PS - - - It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed. - - - There is no call to Write-Verbose in DSC function '{0}'. If you are using Write-Verbose in a helper function, suppress this rule application. - - - Use verbose message in DSC resource - - - Some fields of the module manifest (such as ModuleVersion) are required. - - - Module Manifest Fields - - - If a script file is in a PowerShell module folder, then that folder must be loadable. - - - Cannot load the module '{0}' that file '{1}' is in. - - - Module Must Be Loadable - - - Error Message is Null. - - - Password parameters that take in plaintext will expose passwords and compromise the security of your system. - - - Parameter '{0}' should use SecureString, otherwise this will expose sensitive information. See ConvertTo-SecureString for more information. - - - Avoid Using Plain Text For Password Parameter - - - Using ConvertTo-SecureString with plain text will expose secure information. - - - File '{0}' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. - - - Avoid Using SecureString With Plain Text - - - Switch parameter should not default to true. - - - File '{0}' has a switch parameter default to true. - - - Switch Parameters Should Not Default To True - - - Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it. - - - Function '{0}' in file '{1}' uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt - - - Avoid Using ShouldContinue Without Boolean Force Parameter - - - Using Clear-Host is not recommended because the cmdlet may not work in some hosts or there may even be no hosts at all. - - - File '{0}' uses Clear-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all. - - - Avoid Using Clear-Host - - - File '{0}' uses Console.'{1}'. Using Console to write is not recommended because it may not work in all hosts or there may even be no hosts at all. Use Write-Output instead. - - - Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it. - - - File '{0}' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. - - - Avoid Using Write-Host - - - Cmdlet should use singular instead of plural nouns. - - - The cmdlet '{0}' uses a plural noun. A singular noun should be used instead. - - - Cmdlet Singular Noun - - - AvoidUsingCmdletAliases - - - AvoidDefaultValueSwitchParameter - - - AvoidGlobalVars - - - AvoidShouldContinueWithoutForce - - - AvoidUnloadableModule - - - AvoidUsingClearHost - - - AvoidUsingConvertToSecureStringWithPlainText - - - AvoidUsingEmptyCatchBlock - - - AvoidUsingInvokeExpression - - - AvoidUsingPlainTextForPassword - - - AvoidUsingPositionalParameters - - - AvoidUsingWriteHost - - - OneChar - - - PossibleIncorrectComparisonWithNull - - - ProvideCommentHelp - - - ReservedCmdletChar - - - ReservedParams - - - ShouldProcess - - - UseApprovedVerbs - - - UseDeclaredVarsMoreThanAssignments - - - UsePSCredentialType - - - UseSingularNouns - - - MissingModuleManifestField - - - UseVerboseMessageInDSCResource - - - Command Not Found - - - Commands that are undefined or do not exist should not be used. - - - Command '{0}' Is Not Found - - - CommandNotFound - - - Type Not Found - - - Undefined type should not be used - - - Type '{0}' is not found. Please check that it is defined. - - - TypeNotFound - - - Use Cmdlet Correctly - - - Cmdlet should be called with the mandatory parameters. - - - Cmdlet '{0}' may be used incorrectly. Please check that all mandatory parameters are supplied. - - - UseCmdletCorrectly - - - Use Type At Variable Assignment - - - Types should be specified at variable assignments to maintain readability and maintainability of script. - - - Specify type at the assignment of variable '{0}' - - - UseTypeAtVariableAssignment - - - Avoid Using Username and Password Parameters - - - Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString. - - - Function '{0}' has both Username and Password parameters. Either set the type of the Password parameter to SecureString or replace the Username and Password parameters with a Credential parameter of type PSCredential. If using a Credential parameter in PowerShell 4.0 or earlier, please define a credential transformation attribute after the PSCredential type attribute. - - - AvoidUsingUsernameAndPasswordParams - - - Avoid Invoking Empty Members - - - Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant. - - - '{0}' has non-constant members. Invoking non-constant members may cause bugs in the script. - - - AvoidInvokingEmptyMembers - - - Avoid Using ComputerName Hardcoded - - - The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system. - - - The ComputerName parameter of cmdlet '{0}' is hardcoded. This will expose sensitive information about the system if the script is shared. - - - AvoidUsingComputerNameHardcoded - - - Empty catch block is used. Please use Write-Error or throw statements in catch blocks. - - - '{0}' is an alias of '{1}'. Alias can introduce possible problems and make scripts hard to maintain. Please consider changing alias to its full content. - - - Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead. - - - Cmdlet '{0}' has positional parameter. Please use named parameters instead of positional parameters when calling a command. - - - {0}{1} - - - Cannot process null Ast - - - Cannot process null CommandInfo - - - PSDSC - - - Use Standard Get/Set/Test TargetResource functions in DSC Resource - - - DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions. - - - Missing '{0}' function. DSC Resource must implement Get, Set and Test-TargetResource functions. - - - StandardDSCFunctionsInResource - - - Avoid Using Internal URLs - - - Using Internal URLs in the scripts may cause security problems. - - - '{0}' could be an internal URL. Using internal URL directly in the script may cause potential information disclosure. - - - AvoidUsingInternalURLs - - - www.sharepoint.com - - - Use Identical Parameters For DSC Test and Set Functions - - - The Test and Set-TargetResource functions of DSC Resource must have the same parameters. - - - The Test and Set-TargetResource functions of DSC Resource must have the same parameters. - - - UseIdenticalParametersForDSC - - - Missing '{0}' function. DSC Class must implement Get, Set and Test functions. - - - Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions - - - The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters. - - - The '{0}' parameter '{1}' is not present in '{2}' DSC resource function(s). - - - UseIdenticalMandatoryParametersForDSC - - - Not all code path in {0} function in DSC Class {1} returns a value - - - ReturnCorrectTypesForDSCFunctions - - - Return Correct Types For DSC Functions - - - Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean. - - - {0} function in DSC Class {1} should return object of type {2} - - - {0} function in DSC Class {1} should return object of type {2} instead of type {3} - - - Set function in DSC Class {0} should not return anything - - - {0} function in DSC Resource should return object of type {1} instead of {2} - - - Set-TargetResource function in DSC Resource should not output anything to the pipeline. - - - Use ShouldProcess For State Changing Functions - - - Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'. - - - Function '{0}' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'. - - - UseShouldProcessForStateChangingFunctions - - - Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance - - - Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets. - - - File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. - - - AvoidUsingWMICmdlet - - - Use OutputType Correctly - - - The return types of a cmdlet should be declared using the OutputType attribute. - - - The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute. - - - UseOutputTypeCorrectly - - - DscTestsPresent - - - Dsc tests are present - - - Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. - - - No tests found for resource '{0}' - - - DscExamplesPresent - - - DSC examples are present - - - Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title. - - - No examples found for resource '{0}' - - - Avoid Default Value For Mandatory Parameter - - - Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored.. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block. - - - Mandatory Parameter '{0}' is initialized in the Param block. To fix a violation of this rule, please leave it uninitialized. - - - AvoidDefaultValueForMandatoryParameter - - - Avoid Using Deprecated Manifest Fields - - - "ModuleToProcess" is obsolete in the latest PowerShell version. Please update with the latest field "RootModule" in manifest files to avoid PowerShell version inconsistency. - - - AvoidUsingDeprecatedManifestFields - - - Use UTF8 Encoding For Help File - - - PowerShell help file needs to use UTF8 Encoding. - - - File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file. - - - UseUTF8EncodingForHelpFile - - - Use BOM encoding for non-ASCII files - - - For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly. - - - Missing BOM encoding for non-ASCII encoded file '{0}' - - - UseBOMForUnicodeEncodedFile - - - Script definition has a switch parameter default to true. - - - Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt - - - Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. - - - Script definition uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. - - - Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. - - - ScriptDefinition - - - Misleading Backtick - - - Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. - - - MisleadingBacktick - - - This line has a backtick at the end trailed by a whitespace character. Did you mean for this to be a line continuation? - - - Avoid using null or empty HelpMessage parameter attribute. - - - Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function. - - - HelpMessage parameter attribute should not be null or empty. To fix a violation of this rule, please set its value to a non-empty string. - - - AvoidNullOrEmptyHelpMessageAttribute - - - Use the *ToExport module manifest fields. - - - In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module. - - - Do not use wildcard or $null in this field. Explicitly specify a list for {0}. - - - UseToExportFieldsInManifest - - - Replace {0} with {1} - - - Set {0} type to SecureString - - - Add {0} = {1} to the module manifest - - - Replace {0} with {1} - - - Create hashtables with literal initializers - - - Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default - - - Create hashtables with literal initliazers - - - UseLiteralInitializerForHashtable - - - UseCompatibleCmdlets - - - Use compatible cmdlets - - - Use cmdlets compatible with the given PowerShell version and edition and operating system - - - '{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}' - - - AvoidOverwritingBuiltInCmdlets - - - Avoid overwriting built in cmdlets - - - Do not overwrite the definition of a cmdlet that is included with PowerShell - - - '{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden - - - UseCompatibleCommands - - - Use compatible commands - - - Use commands compatible with the given PowerShell version and operating system - - - The command '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - The parameter '{0}' is not available for command '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - UseCompatibleTypes - - - Use compatible types - - - Use types compatible with the given PowerShell version and operating system - - - The type '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - The method '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - The member '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' - - - UseCompatibleSyntax - - - Use compatible syntax - - - Use script syntax compatible with the given PowerShell versions - - - The {0} syntax '{1}' is not available by default in PowerShell versions {2} - - - Use the '{0}' syntax instead for compatibility with PowerShell versions {1} - - - The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' - - - Avoid global functiosn and aliases - - - Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems. - - - Avoid creating functions with a Global scope. - - - AvoidGlobalFunctions - - - Avoid global aliases. - - - Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts. - - - Avoid creating aliases with a Global scope. - - - AvoidGlobalAliases - - - AvoidTrailingWhitespace - - - Avoid trailing whitespace - - - Each line should have no trailing whitespace. - - - Line has trailing whitespace - - - AvoidLongLines - - - Avoid long lines - - - Line lengths should be less than the configured maximum - - - Line exceeds the configured maximum length of {0} characters - - - PlaceOpenBrace - - - Place open braces consistently - - - Place open braces either on the same line as the preceding expression or on a new line. - - - Open brace not on same line as preceding keyword. It should be on the same line. - - - Open brace is not on a new line. - - - There is no new line after open brace. - - - PlaceCloseBrace - - - Place close braces - - - Close brace should be on a new line by itself. - - - Close brace is not on a new line. - - - Close brace does not follow a non-empty line. - - - Close brace does not follow a new line. - - - Close brace before a branch statement is followed by a new line. - - - UseConsistentIndentation - - - Use consistent indentation - - - Each statement block should have a consistent indenation. - - - Indentation not consistent - - - UseConsistentWhitespace - - - Use whitespaces - - - Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';') - - - Use space before open brace. - - - Use space before open parenthesis. - - - Use space before and after binary and assignment operators. - - - Use space after a comma. - - - Use space after a semicolon. - - - UseSupportsShouldProcess - - - Use SupportsShouldProcess - - - Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess. - - - Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. - - - AlignAssignmentStatement - - - Align assignment statement - - - Line up assignment statements such that the assignment operator are aligned. - - - Assignment statements are not aligned - - - '=' is not an assignment operator. Did you mean the equality operator '-eq'? - - - PossibleIncorrectUsageOfAssignmentOperator - - - Use a different variable name - - - Changing automtic variables might have undesired side effects - - - This automatic variables is built into PowerShell and readonly. - - - The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name. - - - AvoidAssignmentToAutomaticVariable - - - 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. - - - '{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. - - - '=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements. - - - Did you mean to use the assignment operator '='? The equality operator in PowerShell is 'eq'. - - - '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal). - - - When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell. - - - Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal). - - - PossibleIncorrectUsageOfRedirectionOperator - - - Use $null on the left hand side for safe comparison with $null. - - - Use space after open brace. - - - Use space before closing brace. - - - Use space after pipe. - - - Use space before pipe. - - - Use exact casing of cmdlet/function/parameter name. - - - For better readability and consistency, use the exact casing of the cmdlet/function/parameter. - - - Cmdlet/Function/Parameter does not match its exact casing '{0}'. - - - UseCorrectCasing - - - Use process block for command that accepts input from pipeline. - - - If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter. - - - Command accepts pipeline input but has not defined a process block. - - - UseProcessBlockForPipelineCommand - - - The Variable '{0}' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name. - - - Use only 1 whitespace between parameter names or values. - - - ReviewUnusedParameter - - - Ensure all parameters are used within the same script, scriptblock, or function where they are declared. - - - The parameter '{0}' has been declared but not used. - - - ReviewUnusedParameter - - - Use 'Using:' scope modifier in RunSpace ScriptBlocks - - - If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock. - - - UseUsingScopeModifierInNewRunspaces - - - The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier. - - - Replace {0} with {1} - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability. + + + Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix. + + + Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks. + + + Avoid Using Empty Catch Block + + + The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command. It can be extraordinarily powerful so it is not that you want to never use it but you need to be very careful about using it. In particular, you are probably on safe ground if the data only comes from the program itself. If you include any data provided from the user - you need to protect yourself from Code Injection. To fix a violation of this rule, please remove Invoke-Expression from script and find other options instead. + + + Avoid Using Invoke-Expression + + + Readability and clarity should be the goal of any script we expect to maintain over time. When calling a command that takes parameters, where possible consider using name parameters as opposed to positional parameters. To fix a violation of this rule, please use named parameters instead of positional parameters when calling a command. + + + Avoid Using Positional Parameters + + + Checks that all cmdlets have a help comment. This rule only checks existence. It does not check the content of the comment. + + + The cmdlet '{0}' does not have a help comment. + + + Basic Comment Help + + + Checks that all defined cmdlets use approved verbs. This is in line with PowerShell's best practices. + + + The cmdlet '{0}' uses an unapproved verb. + + + Cmdlet Verbs + + + Ensure declared variables are used elsewhere in the script and not just during assignment. + + + The variable '{0}' is assigned but never used. + + + Extra Variables + + + Checks that global variables are not used. Global variables are strongly discouraged as they can cause errors across different systems. + + + Found global variable '{0}'. + + + No Global Variables + + + Checks that $null is on the left side of any equaltiy comparisons (eq, ne, ceq, cne, ieq, ine). When there is an array on the left side of a null equality comparison, PowerShell will check for a $null IN the array rather than if the array is null. If the two sides of the comaprision are switched this is fixed. Therefore, $null should always be on the left side of equality comparisons just in case. + + + $null should be on the left side of equality comparisons. + + + Null Comparison + + + Checks that cmdlets and parameters have more than one character. + + + The cmdlet name '{0}' only has one character. + + + The cmdlet '{0}' has a parameter '{1}' that only has one character. + + + A script block has a parameter '{0}' that only has one character. + + + One Char + + + For PowerShell 4.0 and earlier, a parameter named Credential with type PSCredential must have a credential transformation attribute defined after the PSCredential type attribute. + + + The Credential parameter in '{0}' must be of type PSCredential. For PowerShell 4.0 and earlier, please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. + + + The Credential parameter found in the script block must be of type PSCredential. For PowerShell 4.0 and earlier please define a credential transformation attribute, e.g. [System.Management.Automation.Credential()], after the PSCredential type attribute. + + + Use PSCredential type. + + + Checks for reserved characters in cmdlet names. These characters usually cause a parsing error. Otherwise they will generally cause runtime errors. + + + The cmdlet '{0}' uses a reserved char in its name. + + + Reserved Cmdlet Chars + + + The cmdlet '{0}' + + + Checks for reserved parameters in function definitions. If these parameters are defined by the user, an error generally occurs. + + + '{0}' defines the reserved common parameter '{1}'. + + + Reserved Parameters + + + The script + + + #,(){}[]&/\\$^;:\"'<>|?@`*%+=~ + + + Checks that if the SupportsShouldProcess is present, the function calls ShouldProcess/ShouldContinue and vice versa. Scripts with one or the other but not both will generally run into an error or unexpected behavior. + + + '{0}' has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. + + + A script block has the ShouldProcess attribute but does not call ShouldProcess/ShouldContinue. + + + '{0}' calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. + + + A script block calls ShouldProcess/ShouldContinue but does not have the ShouldProcess attribute. + + + Should Process + + + PS + + + It is a best practice to emit informative, verbose messages in DSC resource functions. This helps in debugging issues when a DSC configuration is executed. + + + There is no call to Write-Verbose in DSC function '{0}'. If you are using Write-Verbose in a helper function, suppress this rule application. + + + Use verbose message in DSC resource + + + Some fields of the module manifest (such as ModuleVersion) are required. + + + Module Manifest Fields + + + If a script file is in a PowerShell module folder, then that folder must be loadable. + + + Cannot load the module '{0}' that file '{1}' is in. + + + Module Must Be Loadable + + + Error Message is Null. + + + Password parameters that take in plaintext will expose passwords and compromise the security of your system. + + + Parameter '{0}' should use SecureString, otherwise this will expose sensitive information. See ConvertTo-SecureString for more information. + + + Avoid Using Plain Text For Password Parameter + + + Using ConvertTo-SecureString with plain text will expose secure information. + + + File '{0}' uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. + + + Avoid Using SecureString With Plain Text + + + Switch parameter should not default to true. + + + File '{0}' has a switch parameter default to true. + + + Switch Parameters Should Not Default To True + + + Functions that use ShouldContinue should have a boolean force parameter to allow user to bypass it. + + + Function '{0}' in file '{1}' uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt + + + Avoid Using ShouldContinue Without Boolean Force Parameter + + + Using Clear-Host is not recommended because the cmdlet may not work in some hosts or there may even be no hosts at all. + + + File '{0}' uses Clear-Host. This is not recommended because it may not work in some hosts or there may even be no hosts at all. + + + Avoid Using Clear-Host + + + File '{0}' uses Console.'{1}'. Using Console to write is not recommended because it may not work in all hosts or there may even be no hosts at all. Use Write-Output instead. + + + Avoid using the Write-Host cmdlet. Instead, use Write-Output, Write-Verbose, or Write-Information. Because Write-Host is host-specific, its implementation might vary unpredictably. Also, prior to PowerShell 5.0, Write-Host did not write to a stream, so users cannot suppress it, capture its value, or redirect it. + + + File '{0}' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. + + + Avoid Using Write-Host + + + Cmdlet should use singular instead of plural nouns. + + + The cmdlet '{0}' uses a plural noun. A singular noun should be used instead. + + + Cmdlet Singular Noun + + + AvoidUsingCmdletAliases + + + AvoidDefaultValueSwitchParameter + + + AvoidGlobalVars + + + AvoidShouldContinueWithoutForce + + + AvoidUnloadableModule + + + AvoidUsingClearHost + + + AvoidUsingConvertToSecureStringWithPlainText + + + AvoidUsingEmptyCatchBlock + + + AvoidUsingInvokeExpression + + + AvoidUsingPlainTextForPassword + + + AvoidUsingPositionalParameters + + + AvoidUsingWriteHost + + + OneChar + + + PossibleIncorrectComparisonWithNull + + + ProvideCommentHelp + + + ReservedCmdletChar + + + ReservedParams + + + ShouldProcess + + + UseApprovedVerbs + + + UseDeclaredVarsMoreThanAssignments + + + UsePSCredentialType + + + UseSingularNouns + + + MissingModuleManifestField + + + UseVerboseMessageInDSCResource + + + Command Not Found + + + Commands that are undefined or do not exist should not be used. + + + Command '{0}' Is Not Found + + + CommandNotFound + + + Type Not Found + + + Undefined type should not be used + + + Type '{0}' is not found. Please check that it is defined. + + + TypeNotFound + + + Use Cmdlet Correctly + + + Cmdlet should be called with the mandatory parameters. + + + Cmdlet '{0}' may be used incorrectly. Please check that all mandatory parameters are supplied. + + + UseCmdletCorrectly + + + Use Type At Variable Assignment + + + Types should be specified at variable assignments to maintain readability and maintainability of script. + + + Specify type at the assignment of variable '{0}' + + + UseTypeAtVariableAssignment + + + Avoid Using Username and Password Parameters + + + Functions should take in a Credential parameter of type PSCredential (with a Credential transformation attribute defined after it in PowerShell 4.0 or earlier) or set the Password parameter to type SecureString. + + + Function '{0}' has both Username and Password parameters. Either set the type of the Password parameter to SecureString or replace the Username and Password parameters with a Credential parameter of type PSCredential. If using a Credential parameter in PowerShell 4.0 or earlier, please define a credential transformation attribute after the PSCredential type attribute. + + + AvoidUsingUsernameAndPasswordParams + + + Avoid Invoking Empty Members + + + Invoking non-constant members would cause potential bugs. Please double check the syntax to make sure members invoked are non-constant. + + + '{0}' has non-constant members. Invoking non-constant members may cause bugs in the script. + + + AvoidInvokingEmptyMembers + + + Avoid Using ComputerName Hardcoded + + + The ComputerName parameter of a cmdlet should not be hardcoded as this will expose sensitive information about the system. + + + The ComputerName parameter of cmdlet '{0}' is hardcoded. This will expose sensitive information about the system if the script is shared. + + + AvoidUsingComputerNameHardcoded + + + Empty catch block is used. Please use Write-Error or throw statements in catch blocks. + + + '{0}' is an alias of '{1}'. Alias can introduce possible problems and make scripts hard to maintain. Please consider changing alias to its full content. + + + Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead. + + + Cmdlet '{0}' has positional parameter. Please use named parameters instead of positional parameters when calling a command. + + + {0}{1} + + + Cannot process null Ast + + + Cannot process null CommandInfo + + + PSDSC + + + Use Standard Get/Set/Test TargetResource functions in DSC Resource + + + DSC Resource must implement Get, Set and Test-TargetResource functions. DSC Class must implement Get, Set and Test functions. + + + Missing '{0}' function. DSC Resource must implement Get, Set and Test-TargetResource functions. + + + StandardDSCFunctionsInResource + + + Avoid Using Internal URLs + + + Using Internal URLs in the scripts may cause security problems. + + + '{0}' could be an internal URL. Using internal URL directly in the script may cause potential information disclosure. + + + AvoidUsingInternalURLs + + + www.sharepoint.com + + + Use Identical Parameters For DSC Test and Set Functions + + + The Test and Set-TargetResource functions of DSC Resource must have the same parameters. + + + The Test and Set-TargetResource functions of DSC Resource must have the same parameters. + + + UseIdenticalParametersForDSC + + + Missing '{0}' function. DSC Class must implement Get, Set and Test functions. + + + Use identical mandatory parameters for DSC Get/Test/Set TargetResource functions + + + The Get/Test/Set TargetResource functions of DSC resource must have the same mandatory parameters. + + + The '{0}' parameter '{1}' is not present in '{2}' DSC resource function(s). + + + UseIdenticalMandatoryParametersForDSC + + + Not all code path in {0} function in DSC Class {1} returns a value + + + ReturnCorrectTypesForDSCFunctions + + + Return Correct Types For DSC Functions + + + Set function in DSC class and Set-TargetResource in DSC resource must not return anything. Get function in DSC class must return an instance of the DSC class and Get-TargetResource function in DSC resource must return a hashtable. Test function in DSC class and Get-TargetResource function in DSC resource must return a boolean. + + + {0} function in DSC Class {1} should return object of type {2} + + + {0} function in DSC Class {1} should return object of type {2} instead of type {3} + + + Set function in DSC Class {0} should not return anything + + + {0} function in DSC Resource should return object of type {1} instead of {2} + + + Set-TargetResource function in DSC Resource should not output anything to the pipeline. + + + Use ShouldProcess For State Changing Functions + + + Functions that have verbs like New, Start, Stop, Set, Reset, Restart that change system state should support 'ShouldProcess'. + + + Function '{0}' has verb that could change system state. Therefore, the function has to support 'ShouldProcess'. + + + UseShouldProcessForStateChangingFunctions + + + Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance + + + Deprecated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets. + + + File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. + + + AvoidUsingWMICmdlet + + + Use OutputType Correctly + + + The return types of a cmdlet should be declared using the OutputType attribute. + + + The cmdlet '{0}' returns an object of type '{1}' but this type is not declared in the OutputType attribute. + + + UseOutputTypeCorrectly + + + DscTestsPresent + + + Dsc tests are present + + + Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. + + + No tests found for resource '{0}' + + + DscExamplesPresent + + + DSC examples are present + + + Every DSC resource module should contain folder "Examples" with sample configurations for every resource. Sample configurations should have resource name they are demonstrating in the title. + + + No examples found for resource '{0}' + + + Avoid Default Value For Mandatory Parameter + + + Mandatory parameter should not be initialized with a default value in the param block because this value will be ignored.. To fix a violation of this rule, please avoid initializing a value for the mandatory parameter in the param block. + + + Mandatory Parameter '{0}' is initialized in the Param block. To fix a violation of this rule, please leave it uninitialized. + + + AvoidDefaultValueForMandatoryParameter + + + Avoid Using Deprecated Manifest Fields + + + "ModuleToProcess" is obsolete in the latest PowerShell version. Please update with the latest field "RootModule" in manifest files to avoid PowerShell version inconsistency. + + + AvoidUsingDeprecatedManifestFields + + + Use UTF8 Encoding For Help File + + + PowerShell help file needs to use UTF8 Encoding. + + + File {0} has to use UTF8 instead of {1} encoding because it is a powershell help file. + + + UseUTF8EncodingForHelpFile + + + Use BOM encoding for non-ASCII files + + + For a file encoded with a format other than ASCII, ensure BOM is present to ensure that any application consuming this file can interpret it correctly. + + + Missing BOM encoding for non-ASCII encoded file '{0}' + + + UseBOMForUnicodeEncodedFile + + + Script definition has a switch parameter default to true. + + + Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt + + + Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. + + + Script definition uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. + + + Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. + + + ScriptDefinition + + + Misleading Backtick + + + Ending a line with an escaped whitepsace character is misleading. A trailing backtick is usually used for line continuation. Users typically don't intend to end a line with escaped whitespace. + + + MisleadingBacktick + + + This line has a backtick at the end trailed by a whitespace character. Did you mean for this to be a line continuation? + + + Avoid using null or empty HelpMessage parameter attribute. + + + Setting the HelpMessage attribute to an empty string or null value causes PowerShell interpreter to throw an error while executing the corresponding function. + + + HelpMessage parameter attribute should not be null or empty. To fix a violation of this rule, please set its value to a non-empty string. + + + AvoidNullOrEmptyHelpMessageAttribute + + + Use the *ToExport module manifest fields. + + + In a module manifest, AliasesToExport, CmdletsToExport, FunctionsToExport and VariablesToExport fields should not use wildcards or $null in their entries. During module auto-discovery, if any of these entries are missing or $null or wildcard, PowerShell does some potentially expensive work to analyze the rest of the module. + + + Do not use wildcard or $null in this field. Explicitly specify a list for {0}. + + + UseToExportFieldsInManifest + + + Replace {0} with {1} + + + Set {0} type to SecureString + + + Add {0} = {1} to the module manifest + + + Replace {0} with {1} + + + Create hashtables with literal initializers + + + Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default + + + Create hashtables with literal initliazers + + + UseLiteralInitializerForHashtable + + + UseCompatibleCmdlets + + + Use compatible cmdlets + + + Use cmdlets compatible with the given PowerShell version and edition and operating system + + + '{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}' + + + AvoidOverwritingBuiltInCmdlets + + + Avoid overwriting built in cmdlets + + + Do not overwrite the definition of a cmdlet that is included with PowerShell + + + '{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden + + + UseCompatibleCommands + + + Use compatible commands + + + Use commands compatible with the given PowerShell version and operating system + + + The command '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + The parameter '{0}' is not available for command '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + UseCompatibleTypes + + + Use compatible types + + + Use types compatible with the given PowerShell version and operating system + + + The type '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + The method '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + The member '{0}' is not available on type '{1}' by default in PowerShell version '{2}' on platform '{3}' + + + UseCompatibleSyntax + + + Use compatible syntax + + + Use script syntax compatible with the given PowerShell versions + + + The {0} syntax '{1}' is not available by default in PowerShell versions {2} + + + Use the '{0}' syntax instead for compatibility with PowerShell versions {1} + + + The type accelerator '{0}' is not available by default in PowerShell version '{1}' on platform '{2}' + + + Avoid global functiosn and aliases + + + Checks that global functions and aliases are not used. Global functions are strongly discouraged as they can cause errors across different systems. + + + Avoid creating functions with a Global scope. + + + AvoidGlobalFunctions + + + Avoid global aliases. + + + Checks that global aliases are not used. Global aliases are strongly discouraged as they overwrite desired aliases with name conflicts. + + + Avoid creating aliases with a Global scope. + + + AvoidGlobalAliases + + + AvoidTrailingWhitespace + + + Avoid trailing whitespace + + + Each line should have no trailing whitespace. + + + Line has trailing whitespace + + + AvoidLongLines + + + Avoid long lines + + + Line lengths should be less than the configured maximum + + + Line exceeds the configured maximum length of {0} characters + + + PlaceOpenBrace + + + Place open braces consistently + + + Place open braces either on the same line as the preceding expression or on a new line. + + + Open brace not on same line as preceding keyword. It should be on the same line. + + + Open brace is not on a new line. + + + There is no new line after open brace. + + + PlaceCloseBrace + + + Place close braces + + + Close brace should be on a new line by itself. + + + Close brace is not on a new line. + + + Close brace does not follow a non-empty line. + + + Close brace does not follow a new line. + + + Close brace before a branch statement is followed by a new line. + + + UseConsistentIndentation + + + Use consistent indentation + + + Each statement block should have a consistent indenation. + + + Indentation not consistent + + + UseConsistentWhitespace + + + Use whitespaces + + + Check for whitespace between keyword and open paren/curly, around assigment operator ('='), around arithmetic operators and after separators (',' and ';') + + + Use space before open brace. + + + Use space before open parenthesis. + + + Use space before and after binary and assignment operators. + + + Use space after a comma. + + + Use space after a semicolon. + + + UseSupportsShouldProcess + + + Use SupportsShouldProcess + + + Commands typically provide Confirm and Whatif parameters to give more control on its execution in an interactive environment. In PowerShell, a command can use a SupportsShouldProcess attribute to provide this capability. Hence, manual addition of these parameters to a command is discouraged. If a commands need Confirm and Whatif parameters, then it should support ShouldProcess. + + + Whatif and/or Confirm manually defined in function {0}. Instead, please use SupportsShouldProcess attribute. + + + AlignAssignmentStatement + + + Align assignment statement + + + Line up assignment statements such that the assignment operator are aligned. + + + Assignment statements are not aligned + + + '=' is not an assignment operator. Did you mean the equality operator '-eq'? + + + PossibleIncorrectUsageOfAssignmentOperator + + + Use a different variable name + + + Changing automtic variables might have undesired side effects + + + This automatic variables is built into PowerShell and readonly. + + + The Variable '{0}' cannot be assigned since it is a readonly automatic variable that is built into PowerShell, please use a different name. + + + AvoidAssignmentToAutomaticVariable + + + 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. + + + '{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. + + + '=' or '==' are not comparison operators in the PowerShell language and rarely needed inside conditional statements. + + + Did you mean to use the assignment operator '='? The equality operator in PowerShell is 'eq'. + + + '>' is not a comparison operator. Use '-gt' (greater than) or '-ge' (greater or equal). + + + When switching between different languages it is easy to forget that '>' does not mean 'great than' in PowerShell. + + + Did you mean to use the redirection operator '>'? The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal). + + + PossibleIncorrectUsageOfRedirectionOperator + + + Use $null on the left hand side for safe comparison with $null. + + + Use space after open brace. + + + Use space before closing brace. + + + Use space after pipe. + + + Use space before pipe. + + + Use exact casing of cmdlet/function/parameter name. + + + For better readability and consistency, use the exact casing of the cmdlet/function/parameter. + + + Cmdlet/Function/Parameter does not match its exact casing '{0}'. + + + UseCorrectCasing + + + Use process block for command that accepts input from pipeline. + + + If a command parameter takes its value from the pipeline, the command must use a process block to bind the input objects from the pipeline to that parameter. + + + Command accepts pipeline input but has not defined a process block. + + + UseProcessBlockForPipelineCommand + + + The Variable '{0}' is an automatic variable that is built into PowerShell, assigning to it might have undesired side effects. If assignment is not by design, please use a different name. + + + Use only 1 whitespace between parameter names or values. + + + ReviewUnusedParameter + + + Ensure all parameters are used within the same script, scriptblock, or function where they are declared. + + + The parameter '{0}' has been declared but not used. + + + ReviewUnusedParameter + + + Use 'Using:' scope modifier in RunSpace ScriptBlocks + + + If a ScriptBlock is intended to be run as a new RunSpace, variables inside it should use 'Using:' scope modifier, or be initialized within the ScriptBlock. + + + UseUsingScopeModifierInNewRunspaces + + + The variable '{0}' is not declared within this ScriptBlock, and is missing the 'Using:' scope modifier. + + + Replace {0} with {1} + +