PSScriptAnalyzer is a static analysis tool, it examines source code and flags issues based on configured rules. PSScriptAnalyzer comes allows a developer to define and inject their own rules using the CustomRulePath parameter. Custom rules might be used to apply an organisation-specific style to code, beyond the best-practice rules.

The rules used as examples in this article are available on GitHub/indented-automation

About AST

Before defining rules a little background is required. PSScriptAnalyzer performs its analysis using the Abstract Syntax Tree, or AST. The AST describes the elements of a piece of code in a hierarchy (a tree).

Access the syntax tree in PowerShell is simple, each script block has an “AST” property.

Climbing the tree

It is possible to work down through the properties of this AST object. For example, the parts which make up the Write-Host command are accessible:

In the example above the elements are placed under the EndBlock property. This occurs because the End block is the default block without the declaration of an explicit block, such as begin or process.

Find and FindAll

It is possible to search the syntax tree using either the Find or FindAll methods. to search the syntax tree, a predicate must be defined. The predicate can be defined as a script block. A single argument is passed in to the script block, each of the branches and leaves of the syntax tree.

The example below describes a predicate which might be used to find a command within the syntax tree:

A param block may be declared within the predicate script block to give the argument a more meaningful name.

The predicate is the first argument of both the Find and FindAll methods. The second argument, a boolean value, dictates whether or not the search should descend into nested script blocks.

The example below uses FindAll to search for all instances of Write-Host within a script block:

The second term in the predicate uses the GetCommandName method of CommandAst, limiting the results to the Write-Host command instead of any command.

The Find method returns the first match instead of all.

The next example introduces a (called) nested script block. Using the FindAll method with the second argument set to false will limit the results to the first command, the second is within a nested script block and therefore cannot be discovered.

Script analyzer rules

A script analyzer rule has the following characteristics:

  • Defined as a function in a module.
  • The first parameter name ends with “ast” or “token”.
  • The first parameter should be an AST type. For example, ScriptBlockAst.

Each rule should expect to return an object of type Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord. This type is declared within the PSScriptAnalyzer module assembly, it will be available if the PSScriptAnalyzer module is imported. The DiagnosticRecord may be created based on a hashtable. The hashtable must include the following keys:

  • Message – A description of the problem.
  • Extent – Normally taken from the AST object, describes the region (start and end) of code which triggered the rule.
  • RuleName – The name of the rule function.
  • Severity – Information, Warning, or Error.

Rule template

The basic structure of a rule may be as defined as shown below:

The OutputType attribute is used as an indication, but is not used or consumed by PSScriptAnalyzer.

Neither CmdletBinding or Parameter are necessary when defining a rule. The parameter does not need a mandatory flag, it does not need to accept pipeline input, and so on.

Naming rules

Rule names should be short and to the point. The rules included with ScriptAnalyzer are prefixed with “PS” by default, or “PSDSC” if the rule applies to a DSC resource. Maintaining this convention, or using a similarly short prefix seems sensible.

For example, a custom rule which seeks to avoid the use of empty begin, process, or end blocks might be called “PSAvoidEmptyNamedBlocks”.

The community rules within the PSScriptAnalyzer module has adopted a convention of prefixing rule names with verb “Measure”. On one hand this extends the verb-noun pairing convention to rules, on the other hand is not descriptive and lends nothing of value to the rule. Script analyzer rules are not subject to by-convention discovery techniques available when interacting with commands in PowerShell.

The AST type

PSScriptAnalyzer uses the type name of the parameter to determine which tokens should be passed to the rule for analysis. The AST type should therefore be as specific as possible.

A rule which tests the content of a single command should use the CommandAst type, as shown below:

A rule working with the characteristics of a function should use FunctionDefinitionAst, Named blocks a NamedBlockAst, and so on.

The possible AST types are listed under the System.Management.Automation.Language namespace and documented on MSDN:

https://msdn.microsoft.com/en-us/library/system.management.automation.language(v=vs.85).aspx

Alternatively, a short script will reveal the available AST types:

Example rules

The following rules are used as working examples:

  • PSAvoidEmptyNamedBlocks
  • PSAvoidNestedFunctions
  • PSAvoidProcessWithoutPipeline
  • PSAvoidWriteErrorStop

Each rule represents a personal convention, not necessarily a best practice.

PSAvoidEmptyNamedBlocks

The PSAvoidEmptyNamedBlocks rule applies to any NamedBlockAst element, therefore the parameter type is defined as NamedBlockAst.

If the AST type is not known, it might be discovered by exploring AST.

The Statements property is used to evaluate whether or not the block is empty. This is shown by viewing the AST object, and with confirmation using Get-Member.

The ReadOnlyCollection type exposes a Count property which can be used to assess whether or not the block contains statements.

The rule can be tested by passing an AST of the expected type. The first command will trigger the rule, the second will not.

White space (empty lines) will not be counted as a statement, this rule will be effective no matter how many empty lines appear in the process block.

PSAvoidNestedFunctions

Functions can be nested within another function. To find nested functions the AST type FunctionDefinitionAst is used as the starting point.

As with the named block, this type may be discovered by working with the AST tree.

The following script block shows the scenario the rule must look for.

The rule must perform a search on every function definition, in each case searching for another function within the body (of the current function). The FindAll method is used to execute the search.

The rule can be tested by passing an AST of the expected type. The first command will trigger the rule, the second will not.

PSAvoidProcessWithoutPipeline

A rule may combine information from more than one AST search. The starting point for this rule is a script block with a process block.

The second part of the search finds parameters which are configured to accept pipeline input.

If the process block is present, but none of the parameters accepts pipeline input a record is created.

This scenario is demonstrated by the following script. The use of the process block implies an intent to support an input pipeline, but support for the pipeline has not been defined.

The rule, therefore, accepts a ScriptBlockAst and tests different nodes beneath.

PSAvoidWriteErrorStop

This final example explores examining the arguments passed to a command. In this case the intent is to avoid using Write-Error to generate a terminating error. The rule does not test the ErrorActionPreference variable which may limit the effectiveness of the test.

As this rule focuses on a command, it expects to receive a CommandAst as the argument. The rule then filters to a specific command name before exploring the commands parameters.

The index (location) of the ErrorAction parameter is extracted and used to locate the argument for the parameter.

The SafeGetValue method will fail if the value is a variable, or sub-expression. This error is shown when running the example below:

If the PS variable were in fact a constant, such as the varible “true”, SafeGetValue would return the value.

Evaluating the value of the argument for the ErrorAction parameter is left to the Enum.Parse static method. This allows conversion of the different ways “Stop” might be expressed as shown below.

The rule below incorporates each of these steps.

Rule suppression and Extent

# TODO – Add custom extent code

Leave a Reply

Your email address will not be published. Required fields are marked *