PowerShell AST or "How I Stopped Looking for that Curly Brace"

PowerShell's Abstract Syntax Tree (AST) is a powerful yet underutilized feature that opens up sophisticated possibilities for code analysis, transformation, and tooling development. If you've ever wondered how PowerShell development tools work under the hood or wanted to build your own code analysis capabilities, understanding the AST is essential.

What is PowerShell AST?

The Abstract Syntax Tree represents PowerShell code as a hierarchical tree structure of objects, where each node corresponds to a different element of the code—commands, parameters, variables, expressions, and more. Rather than treating code as simple text, the AST provides a structured representation that makes it possible to programmatically inspect, analyze, and modify PowerShell scripts with precision.

Think of it as PowerShell's way of understanding your code the same way you do, but in a format that programs can easily work with.

Key Components

When working with PowerShell AST, you'll primarily interact with these essential classes:

System.Management.Automation.Language.Parser serves as your entry point for parsing code into AST format. This is where the magic begins—transforming raw PowerShell text into structured objects.

System.Management.Automation.Language.Ast acts as the base class for all AST nodes, providing common functionality and properties that all nodes share.

Specific AST node types include CommandAst for command invocations, ParameterAst for parameters, VariableExpressionAst for variable references, ScriptBlockAst for script blocks, and dozens of other specialized types that represent different language constructs.

Real-World Use Cases

The AST shines in several practical scenarios that every PowerShell developer encounters:

Code Analysis becomes surgical when you can traverse the AST to find security vulnerabilities, code quality issues, or gather detailed metrics about your scripts. Instead of using regex patterns that might miss edge cases, you can definitively identify all variables, commands, or parameters used throughout your codebase.

Static Analysis Tools leverage the AST to build sophisticated linters and code analyzers that examine PowerShell scripts without executing them. This enables safe analysis of potentially dangerous code and integration into CI/CD pipelines.

Code Transformation scenarios include automatically refactoring scripts, updating deprecated cmdlets across large codebases, renaming variables consistently, or injecting logging and monitoring code into existing scripts.

Security Scanning becomes more reliable when you can identify potentially dangerous operations, hunt for hardcoded credentials, or detect suspicious patterns with the precision that only comes from understanding code structure rather than just text patterns.

Documentation Generation can extract function definitions, parameters, help text, and usage examples to automatically generate comprehensive documentation for your PowerShell modules and scripts.

Basic Example: Getting Started

Here's how you can begin exploring PowerShell AST:

# Parse a simple script
$code = 'Get-Process | Where-Object {$_.CPU -gt 100}'
$ast = [System.Management.Automation.Language.Parser]::ParseInput($code, [ref]$null, [ref]$null)

# Find all command elements
$commands = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)

# Display the commands found
$commands | ForEach-Object {
    Write-Host "Found command: $($_.GetCommandName())"
}

This simple example demonstrates the fundamental pattern: parse code into an AST, then use the FindAll method with a filter predicate to locate specific types of nodes.

Advanced Example: Finding Matching Braces

One particularly useful application of AST analysis is finding matching pairs of braces, which is essential for code editors, formatters, and refactoring tools. Here's a comprehensive implementation that uses token analysis for precise brace matching:

function Find-MatchingBrace {
    param(
        [string]$Code,
        [int]$Position
    )

    # First check if the position actually contains a brace
    if ($Position -ge $Code.Length -or ($Code[$Position] -ne '{' -and $Code[$Position] -ne '}')) {
        Write-Host "Position $Position does not contain a brace character" -ForegroundColor Red
        return $null
    }

    # Parse the code into AST
    $tokens = $null
    $errors = $null
    $ast = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$tokens, [ref]$errors)

    # Find all tokens that are braces
    $braceTokens = $tokens | Where-Object { 
        $_.Kind -eq [System.Management.Automation.Language.TokenKind]::LCurly -or 
        $_.Kind -eq [System.Management.Automation.Language.TokenKind]::RCurly 
    }

    Write-Host "Debug: Found $($braceTokens.Count) brace tokens" -ForegroundColor Cyan
    foreach ($token in $braceTokens) {
        $braceChar = if ($token.Kind -eq [System.Management.Automation.Language.TokenKind]::LCurly) { "{" } else { "}" }
        $color = if ($token.Kind -eq [System.Management.Automation.Language.TokenKind]::LCurly) { "Green" } else { "Magenta" }
        Write-Host "  Token at position $($token.Extent.StartOffset): '$braceChar' (Kind: $($token.Kind))" -ForegroundColor $color
    }

    # Create a stack to match braces
    $braceStack = @()
    $braceMap = @{}

    foreach ($token in $braceTokens | Sort-Object { $_.Extent.StartOffset }) {
        $tokenPos = $token.Extent.StartOffset
        
        if ($token.Kind -eq [System.Management.Automation.Language.TokenKind]::LCurly) {
            # Opening brace - push to stack
            $braceStack += $tokenPos
        }
        elseif ($token.Kind -eq [System.Management.Automation.Language.TokenKind]::RCurly) {
            # Closing brace - pop from stack and create mapping
            if ($braceStack.Count -gt 0) {
                $matchingOpen = $braceStack[-1]
                $braceStack = $braceStack[0..($braceStack.Count-2)]
                
                # Create bidirectional mapping
                $braceMap[$matchingOpen] = $tokenPos
                $braceMap[$tokenPos] = $matchingOpen
            }
        }
    }

    Write-Host "Debug: Brace mapping:" -ForegroundColor Cyan
    foreach ($key in $braceMap.Keys | Sort-Object) {
        $keyChar = $Code[$key]
        $valueChar = $Code[$braceMap[$key]]
        
        if ($keyChar -eq '{') {
            Write-Host "  Position " -NoNewline
            Write-Host "$key" -ForegroundColor Green -NoNewline
            Write-Host " ($keyChar) -> Position " -NoNewline
            Write-Host "$($braceMap[$key])" -ForegroundColor Magenta -NoNewline
            Write-Host " ($valueChar)"
        }
    }

    # Check if our position has a match
    if ($braceMap.ContainsKey($Position)) {
        $matchingPos = $braceMap[$Position]
        $isOpening = $Code[$Position] -eq '{'
        
        # Find the AST node that contains this brace for additional context
        $containingNode = $ast.FindAll({
            $node = $args[0]
            $node.Extent.StartOffset -le $Position -and $node.Extent.EndOffset -gt $Position
        }, $true) | Where-Object {
            $_ -is [System.Management.Automation.Language.ScriptBlockAst] -or
            $_ -is [System.Management.Automation.Language.ScriptBlockExpressionAst] -or
            $_ -is [System.Management.Automation.Language.HashtableAst] -or
            $_ -is [System.Management.Automation.Language.ArrayLiteralAst]
        } | Select-Object -First 1

        return @{
            Type = if ($isOpening) { 'Opening' } else { 'Closing' }
            Position = $Position
            MatchingPosition = $matchingPos
            BlockType = if ($containingNode) { $containingNode.GetType().Name } else { 'Unknown' }
            Character = $Code[$Position]
            MatchingCharacter = $Code[$matchingPos]
        }
    }

    return $null
}

# Helper function to find all brace positions
function Find-BracePositions {
    param([string]$Code)
    
    Write-Host "Code with position markers:" -ForegroundColor Yellow
    Write-Host "==========================" -ForegroundColor Yellow
    
    # Show the code with position numbers
    $lines = $Code -split "`n"
    $position = 0
    
    for ($lineNum = 0; $lineNum -lt $lines.Count; $lineNum++) {
        $line = $lines[$lineNum]
        Write-Host ("Line {0,2}: " -f ($lineNum + 1)) -ForegroundColor Gray -NoNewline
        Write-Host $line -ForegroundColor White
        
        # Show positions of braces in this line
        for ($charPos = 0; $charPos -lt $line.Length; $charPos++) {
            if ($line[$charPos] -eq '{' -or $line[$charPos] -eq '}') {
                $absolutePos = $position + $charPos
                $braceColor = if ($line[$charPos] -eq '{') { "Green" } else { "Magenta" }
                Write-Host "         Brace " -ForegroundColor Gray -NoNewline
                Write-Host "'$($line[$charPos])'" -ForegroundColor $braceColor -NoNewline
                Write-Host " at position " -ForegroundColor Gray -NoNewline
                Write-Host "$absolutePos" -ForegroundColor $braceColor
            }
        }
        
        $position += $line.Length + 1  # +1 for newline character
    }
    Write-Host ""
}

# Example usage
$sampleCode = @'
Get-Process | Where-Object {
    $_.CPU -gt 100 -and
    $_.WorkingSet -gt 50MB
} | ForEach-Object {
    Write-Host "High CPU process: $($_.Name)"
}
'@

Write-Host "Sample code:" -ForegroundColor Yellow
Write-Host $sampleCode -ForegroundColor White
Write-Host ""

# First, let's see where the braces actually are
Find-BracePositions -Code $sampleCode

Write-Host "Testing brace matching:" -ForegroundColor Yellow
Write-Host "=====================" -ForegroundColor Yellow

# Find all brace positions and test them
for ($i = 0; $i -lt $sampleCode.Length; $i++) {
    if ($sampleCode[$i] -eq '{' -or $sampleCode[$i] -eq '}') {
        $braceColor = if ($sampleCode[$i] -eq '{') { "Green" } else { "Magenta" }
        
        Write-Host "Testing position " -ForegroundColor Gray -NoNewline
        Write-Host "$i" -ForegroundColor $braceColor -NoNewline
        Write-Host " (character: " -ForegroundColor Gray -NoNewline
        Write-Host "'$($sampleCode[$i])'" -ForegroundColor $braceColor -NoNewline
        Write-Host ")" -ForegroundColor Gray
        
        $result = Find-MatchingBrace -Code $sampleCode -Position $i
        
        if ($result) {
            $matchColor = if ($result.MatchingCharacter -eq '{') { "Green" } else { "Magenta" }
            Write-Host "  âś“ Found " -ForegroundColor Green -NoNewline
            Write-Host "$($result.Type)" -ForegroundColor $braceColor -NoNewline
            Write-Host " brace at position " -ForegroundColor Green -NoNewline
            Write-Host "$($result.Position)" -ForegroundColor $braceColor
            
            Write-Host "  âś“ Matching brace is at position " -ForegroundColor Green -NoNewline
            Write-Host "$($result.MatchingPosition)" -ForegroundColor $matchColor
            
            Write-Host "  âś“ Block type: " -ForegroundColor Green -NoNewline
            Write-Host "$($result.BlockType)" -ForegroundColor Cyan
        } else {
            Write-Host "  âś— No match found" -ForegroundColor Red
        }
        Write-Host ""
    }
}

This example demonstrates the power of using both AST analysis and token parsing together. The token approach provides precise character positions, while the AST gives us the semantic context of what type of block contains each brace.

Why AST Matters

The AST is particularly valuable for building PowerShell development tools, automated code review systems, and advanced scripting scenarios where you need to work with code as structured data rather than just executing it. Whether you're building the next great PowerShell IDE extension, creating organization-wide code standards enforcement, or developing sophisticated deployment automation, the AST provides the foundation for reliable, precise code manipulation.

The key insight is that the AST transforms code from text that humans read into data structures that programs can efficiently process, opening up possibilities that would be difficult or impossible with traditional text-based approaches.

Large Script Analysis: Two Essential Examples

When dealing with scripts that exceed 1500 lines, manual analysis becomes impractical. Here are two powerful examples that can save hours of work on large codebases:

Example 1: Finding Unused Variables and Functions

Large scripts often accumulate unused variables and functions over time. This analyzer identifies dead code automatically:

function Find-UnusedCode {
    param(
        [Parameter(ParameterSetName = 'File')]
        [string]$ScriptPath,
        
        [Parameter(ParameterSetName = 'String')]
        [string]$ScriptContent,
        
        [switch]$AnalyzeSelf
    )
    
    try {
        # Determine what to analyze
        if ($AnalyzeSelf) {
            $content = $MyInvocation.ScriptName | Get-Content -Raw -ErrorAction Stop
            $sourceName = "Current Script"
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path $ScriptPath)) {
                Write-Host "Error: File '$ScriptPath' does not exist." -ForegroundColor Red
                return $null
            }
            $content = Get-Content -Path $ScriptPath -Raw -ErrorAction Stop
            $sourceName = Split-Path $ScriptPath -Leaf
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'String') {
            $content = $ScriptContent
            $sourceName = "Provided Script Content"
        }
        else {
            Write-Host "Error: Must specify either -ScriptPath, -ScriptContent, or -AnalyzeSelf" -ForegroundColor Red
            return $null
        }
        
        if ([string]::IsNullOrWhiteSpace($content)) {
            Write-Host "Error: Script content is empty or null." -ForegroundColor Red
            return $null
        }
        
        Write-Host "Analyzing: $sourceName" -ForegroundColor Cyan
        Write-Host "Content length: $($content.Length) characters" -ForegroundColor Gray
        
        # Parse the content
        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$errors)
        
        if ($errors.Count -gt 0) {
            Write-Host "Warning: Found $($errors.Count) parse errors:" -ForegroundColor Yellow
            foreach ($error in $errors) {
                Write-Host "  - Line $($error.Extent.StartLineNumber): $($error.Message)" -ForegroundColor Yellow
            }
        }
        
        # Find all variable assignments
        $variableAssignments = $ast.FindAll({
            $args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]
        }, $true)
        
        Write-Host "Found $($variableAssignments.Count) variable assignments" -ForegroundColor Gray
        
        # Find all variable references
        $variableReferences = $ast.FindAll({
            $args[0] -is [System.Management.Automation.Language.VariableExpressionAst]
        }, $true)
        
        Write-Host "Found $($variableReferences.Count) variable references" -ForegroundColor Gray
        
        # Find all function definitions
        $functionDefinitions = $ast.FindAll({
            $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]
        }, $true)
        
        Write-Host "Found $($functionDefinitions.Count) function definitions" -ForegroundColor Gray
        
        # Find all function calls (commands)
        $functionCalls = $ast.FindAll({
            $args[0] -is [System.Management.Automation.Language.CommandAst]
        }, $true)
        
        Write-Host "Found $($functionCalls.Count) command calls" -ForegroundColor Gray
        
        $results = @{
            UnusedVariables = @()
            UnusedFunctions = @()
            Statistics = @{}
            SourceName = $sourceName
            ParseErrors = $errors
        }
        
        # Analyze variables
        $assignedVariables = @()
        foreach ($assignment in $variableAssignments) {
            if ($assignment.Left -is [System.Management.Automation.Language.VariableExpressionAst]) {
                $varName = $assignment.Left.VariablePath.UserPath
                $assignedVariables += @{
                    Name = $varName
                    Line = $assignment.Extent.StartLineNumber
                    Position = $assignment.Extent.StartOffset
                }
            }
        }
        
        Write-Host "Debug - Assigned variables:" -ForegroundColor Magenta
        foreach ($var in $assignedVariables) {
            Write-Host "  $($var.Name) (Line $($var.Line))" -ForegroundColor Magenta
        }
        
        $referencedVariables = @()
        foreach ($reference in $variableReferences) {
            $varName = $reference.VariablePath.UserPath
            $referencedVariables += $varName
        }
        
        $uniqueReferencedVars = $referencedVariables | Select-Object -Unique
        Write-Host "Debug - Referenced variables:" -ForegroundColor Magenta
        foreach ($var in $uniqueReferencedVars) {
            Write-Host "  $var" -ForegroundColor Magenta
        }
        
        # Find unused variables
        $unusedVars = @()
        foreach ($assignedVar in $assignedVariables) {
            $varName = $assignedVar.Name
            # Exclude automatic variables, parameters, and commonly used variables
            $excludedVars = @('_', 'args', 'input', 'PSItem', 'this', 'matches', 'lastexitcode', 
                             'error', 'warning', 'information', 'verbose', 'debug', 'progress',
                             'whatifpreference', 'confirmpreference', 'erroractionpreference')
            
            if ($varName -notin $excludedVars -and $varName -notin $referencedVariables) {
                $unusedVars += $assignedVar
            }
        }
        
        $results.UnusedVariables = $unusedVars
        
        # Analyze functions
        $definedFunctions = @()
        foreach ($funcDef in $functionDefinitions) {
            $definedFunctions += @{
                Name = $funcDef.Name
                Line = $funcDef.Extent.StartLineNumber
                Position = $funcDef.Extent.StartOffset
            }
        }
        
        $calledFunctions = @()
        foreach ($call in $functionCalls) {
            $commandName = $call.GetCommandName()
            if ($commandName) {
                $calledFunctions += $commandName
            }
        }
        
        # Find unused functions
        $unusedFuncs = @()
        foreach ($definedFunc in $definedFunctions) {
            if ($definedFunc.Name -notin $calledFunctions) {
                $unusedFuncs += $definedFunc
            }
        }
        
        $results.UnusedFunctions = $unusedFuncs
        
        # Generate statistics
        $results.Statistics = @{
            TotalLines = ($content -split "`n").Count
            TotalVariables = $assignedVariables.Count
            TotalFunctions = $definedFunctions.Count
            UnusedVariableCount = $unusedVars.Count
            UnusedFunctionCount = $unusedFuncs.Count
            ReferencedVariables = ($referencedVariables | Select-Object -Unique).Count
            CalledFunctions = ($calledFunctions | Select-Object -Unique).Count
            ParseErrors = $errors.Count
        }
        
        return $results
    }
    catch {
        Write-Host "Error analyzing script: $($_.Exception.Message)" -ForegroundColor Red
        return $null
    }
}

function Show-UnusedCodeReport {
    param([object]$Analysis)
    
    if (-not $Analysis) {
        Write-Host "No analysis data provided." -ForegroundColor Red
        return
    }
    
    $separator = "=" * 60
    Write-Host "`n$separator" -ForegroundColor Cyan
    Write-Host "Script Analysis Results: $($Analysis.SourceName)" -ForegroundColor Green
    Write-Host "$separator" -ForegroundColor Cyan
    
    # Statistics
    Write-Host "`nStatistics:" -ForegroundColor Yellow
    Write-Host "  Total Lines: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.TotalLines)" -ForegroundColor White
    
    Write-Host "  Variables: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.UnusedVariableCount)" -ForegroundColor Red -NoNewline
    Write-Host " unused / " -ForegroundColor Gray -NoNewline
    Write-Host "$($Analysis.Statistics.TotalVariables)" -ForegroundColor Green -NoNewline
    Write-Host " total" -ForegroundColor Gray
    
    Write-Host "  Functions: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.UnusedFunctionCount)" -ForegroundColor Red -NoNewline
    Write-Host " unused / " -ForegroundColor Gray -NoNewline
    Write-Host "$($Analysis.Statistics.TotalFunctions)" -ForegroundColor Green -NoNewline
    Write-Host " total" -ForegroundColor Gray
    
    if ($Analysis.Statistics.ParseErrors -gt 0) {
        Write-Host "  Parse Errors: " -NoNewline -ForegroundColor Gray
        Write-Host "$($Analysis.Statistics.ParseErrors)" -ForegroundColor Red
    }
    
    # Unused Variables
    if ($Analysis.UnusedVariables.Count -gt 0) {
        Write-Host "`nUnused Variables:" -ForegroundColor Yellow
        foreach ($var in $Analysis.UnusedVariables) {
            Write-Host "  - " -NoNewline -ForegroundColor Gray
            Write-Host "`$$($var.Name)" -ForegroundColor Red -NoNewline
            Write-Host " (Line $($var.Line))" -ForegroundColor Gray
        }
    } else {
        Write-Host "`nUnused Variables: " -NoNewline -ForegroundColor Yellow
        Write-Host "None found âś“" -ForegroundColor Green
    }
    
    # Unused Functions
    if ($Analysis.UnusedFunctions.Count -gt 0) {
        Write-Host "`nUnused Functions:" -ForegroundColor Yellow
        foreach ($func in $Analysis.UnusedFunctions) {
            Write-Host "  - " -NoNewline -ForegroundColor Gray
            Write-Host "$($func.Name)" -ForegroundColor Red -NoNewline
            Write-Host " (Line $($func.Line))" -ForegroundColor Gray
        }
    } else {
        Write-Host "`nUnused Functions: " -NoNewline -ForegroundColor Yellow
        Write-Host "None found âś“" -ForegroundColor Green
    }
    
    Write-Host "`n$separator" -ForegroundColor Cyan
}

# Example usage with sample code
$sampleScript = @'
# Sample PowerShell script for testing
function Get-UserInfo {
    param($Username)
    return Get-ADUser $Username
}

function Send-Email {
    param($To, $Subject, $Body)
    Send-MailMessage -To $To -Subject $Subject -Body $Body
}

function Process-Data {
    # This function is never called
    param($Data)
    return $Data | Sort-Object
}

# Variables
$usedVariable = "Hello World"
$unusedVariable = "This is never used"
$anotherUnused = 42

# Usage
Write-Host $usedVariable
$userInfo = Get-UserInfo -Username "john.doe"
Send-Email -To "test@example.com" -Subject "Test" -Body $usedVariable
'@

Write-Host "Testing with sample script content:" -ForegroundColor Cyan
$analysis = Find-UnusedCode -ScriptContent $sampleScript
Show-UnusedCodeReport -Analysis $analysis

$dashSeparator = "-" * 60
Write-Host "`n$dashSeparator" -ForegroundColor Gray
Write-Host "To analyze a specific file, use:" -ForegroundColor Yellow
Write-Host '  $analysis = Find-UnusedCode -ScriptPath "C:\Path\To\Your\Script.ps1"' -ForegroundColor White
Write-Host '  Show-UnusedCodeReport -Analysis $analysis' -ForegroundColor White

Write-Host "`nTo analyze the current script file, use:" -ForegroundColor Yellow
Write-Host '  $analysis = Find-UnusedCode -AnalyzeSelf' -ForegroundColor White
Write-Host '  Show-UnusedCodeReport -Analysis $analysis' -ForegroundColor White

Example 2: Dependency Mapping and Impact Analysis

Understanding dependencies in large scripts is crucial for refactoring and maintenance. This example maps all function dependencies and identifies critical functions:

function Show-DependencyMap {
    param(
        [Parameter(ParameterSetName = 'File')]
        [string]$ScriptPath,

        [Parameter(ParameterSetName = 'String')]
        [string]$ScriptContent,

        [switch]$AnalyzeSelf,
        [string]$ExportPath = $null
    )

    try {
        # Determine what to analyze
        if ($AnalyzeSelf) {
            $content = $MyInvocation.ScriptName | Get-Content -Raw -ErrorAction Stop
            $sourceName = "Current Script"
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path $ScriptPath)) {
                Write-Host "Error: File '$ScriptPath' does not exist." -ForegroundColor Red
                return $null
            }
            $content = Get-Content -Path $ScriptPath -Raw -ErrorAction Stop
            $sourceName = Split-Path $ScriptPath -Leaf
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'String') {
            $content = $ScriptContent
            $sourceName = "Provided Script Content"
        }
        else {
            Write-Host "Error: Must specify either -ScriptPath, -ScriptContent, or -AnalyzeSelf" -ForegroundColor Red
            return $null
        }

        if ([string]::IsNullOrWhiteSpace($content)) {
            Write-Host "Error: Script content is empty or null." -ForegroundColor Red
            return $null
        }

        Write-Host "Analyzing dependencies in: $sourceName" -ForegroundColor Cyan
        Write-Host "Content length: $($content.Length) characters" -ForegroundColor Gray

        # Parse the content
        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$errors)

        if ($errors.Count -gt 0) {
            Write-Host "Warning: Found $($errors.Count) parse errors:" -ForegroundColor Yellow
            foreach ($error in $errors) {
                Write-Host "  - Line $($error.Extent.StartLineNumber): $($error.Message)" -ForegroundColor Yellow
            }
        }

        # Find all function definitions with their content
        $functions = $ast.FindAll({
            $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]
        }, $true)

        Write-Host "Found $($functions.Count) function definitions" -ForegroundColor Gray

        $dependencyMap = @{}
        $reverseDependencyMap = @{}

        foreach ($function in $functions) {
            $functionName = $function.Name
            $functionBody = $function.Body.ToString()

            Write-Host "Analyzing function: $functionName" -ForegroundColor Gray

            # Find all commands called within this function
            $commandsInFunction = $function.FindAll({
                $args[0] -is [System.Management.Automation.Language.CommandAst]
            }, $true)

            $dependencies = @()
            foreach ($command in $commandsInFunction) {
                $commandName = $command.GetCommandName()
                # Check if this command is a function defined in our script
                if ($commandName -in $functions.Name) {
                    $dependencies += $commandName
                    Write-Host "  - Calls: $commandName" -ForegroundColor DarkGray
                }
            }

            $dependencyMap[$functionName] = $dependencies | Select-Object -Unique

            # Build reverse dependency map
            foreach ($dependency in ($dependencies | Select-Object -Unique)) {
                if (-not $reverseDependencyMap.ContainsKey($dependency)) {
                    $reverseDependencyMap[$dependency] = @()
                }
                $reverseDependencyMap[$dependency] += $functionName
            }
        }

        # Calculate dependency metrics
        $analysis = @{
            DependencyMap = $dependencyMap
            ReverseDependencyMap = $reverseDependencyMap
            CriticalFunctions = @()
            IsolatedFunctions = @()
            Statistics = @{}
            SourceName = $sourceName
            ParseErrors = $errors
        }

        # Identify critical functions (used by many others)
        $analysis.CriticalFunctions = $reverseDependencyMap.Keys | Where-Object {
            $reverseDependencyMap[$_].Count -ge 2  # Lowered threshold for demo
        } | Sort-Object { $reverseDependencyMap[$_].Count } -Descending

        # Identify isolated functions (no dependencies and not used by others)
        $analysis.IsolatedFunctions = $dependencyMap.Keys | Where-Object {
            $dependencyMap[$_].Count -eq 0 -and
            (-not $reverseDependencyMap.ContainsKey($_) -or $reverseDependencyMap[$_].Count -eq 0)
        }

        $avgDependencies = if ($dependencyMap.Count -gt 0) {
            ($dependencyMap.Values | ForEach-Object { $_.Count } | Measure-Object -Average).Average
        } else { 0 }

        $analysis.Statistics = @{
            TotalFunctions = $functions.Count
            CriticalFunctionCount = $analysis.CriticalFunctions.Count
            IsolatedFunctionCount = $analysis.IsolatedFunctions.Count
            AverageDependencies = $avgDependencies
        }

        return $analysis
    }
    catch {
        Write-Host "Error analyzing dependencies: $($_.Exception.Message)" -ForegroundColor Red
        return $null
    }
}

function Show-DependencyReport {
    param($Analysis, [string]$ExportPath = $null)

    if (-not $Analysis) {
        Write-Host "No analysis data provided." -ForegroundColor Red
        return
    }

    $separator = "=" * 50
    Write-Host "`n$separator" -ForegroundColor Cyan
    Write-Host "Function Dependency Analysis: $($Analysis.SourceName)" -ForegroundColor Green
    Write-Host "$separator" -ForegroundColor Cyan

    Write-Host "`nStatistics:" -ForegroundColor Yellow
    Write-Host "  Total Functions: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.TotalFunctions)" -ForegroundColor White

    Write-Host "  Critical Functions: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.CriticalFunctionCount)" -ForegroundColor Yellow

    Write-Host "  Isolated Functions: " -NoNewline -ForegroundColor Gray
    Write-Host "$($Analysis.Statistics.IsolatedFunctionCount)" -ForegroundColor Cyan

    Write-Host "  Average Dependencies: " -NoNewline -ForegroundColor Gray
    Write-Host "$([Math]::Round($Analysis.Statistics.AverageDependencies, 2))" -ForegroundColor White

    # Show dependency map
    if ($Analysis.DependencyMap.Count -gt 0) {
        Write-Host "`nFunction Dependencies:" -ForegroundColor Yellow
        foreach ($func in $Analysis.DependencyMap.GetEnumerator() | Sort-Object Key) {
            if ($func.Value.Count -gt 0) {
                Write-Host "  $($func.Key) depends on:" -ForegroundColor White
                foreach ($dep in $func.Value) {
                    Write-Host "    - $dep" -ForegroundColor Gray
                }
            } else {
                Write-Host "  $($func.Key)" -ForegroundColor White -NoNewline
                Write-Host " (no internal dependencies)" -ForegroundColor DarkGray
            }
        }
    }

    if ($Analysis.CriticalFunctions.Count -gt 0) {
        Write-Host "`nCritical Functions (High Impact if Changed):" -ForegroundColor Red
        foreach ($func in $Analysis.CriticalFunctions) {
            $usageCount = $Analysis.ReverseDependencyMap[$func].Count
            Write-Host "  $func " -ForegroundColor Yellow -NoNewline
            Write-Host "(used by $usageCount functions)" -ForegroundColor Gray
            foreach ($user in $Analysis.ReverseDependencyMap[$func]) {
                Write-Host "    - $user" -ForegroundColor Gray
            }
        }
    } else {
        Write-Host "`nCritical Functions: " -NoNewline -ForegroundColor Red
        Write-Host "None found" -ForegroundColor Green
    }

    if ($Analysis.IsolatedFunctions.Count -gt 0) {
        Write-Host "`nIsolated Functions (Safe to Remove/Modify):" -ForegroundColor Green
        foreach ($func in $Analysis.IsolatedFunctions) {
            Write-Host "  - $func" -ForegroundColor Cyan
        }
    } else {
        Write-Host "`nIsolated Functions: " -NoNewline -ForegroundColor Green
        Write-Host "None found" -ForegroundColor Yellow
    }

    # Show functions with most dependencies (potential complexity issues)
    $complexFunctions = $Analysis.DependencyMap.GetEnumerator() |
        Where-Object { $_.Value.Count -gt 3 } |  # Lowered threshold for demo
        Sort-Object { $_.Value.Count } -Descending

    if ($complexFunctions.Count -gt 0) {
        Write-Host "`nComplex Functions (Many Dependencies):" -ForegroundColor Magenta
        foreach ($func in $complexFunctions) {
            Write-Host "  $($func.Key) " -ForegroundColor Yellow -NoNewline
            Write-Host "depends on $($func.Value.Count) functions" -ForegroundColor Gray
        }
    } else {
        Write-Host "`nComplex Functions: " -NoNewline -ForegroundColor Magenta
        Write-Host "None found" -ForegroundColor Green
    }

    # Export to CSV if requested
    if ($ExportPath) {
        try {
            $exportData = $Analysis.DependencyMap.GetEnumerator() | ForEach-Object {
                [PSCustomObject]@{
                    Function = $_.Key
                    DependsOn = ($_.Value -join '; ')
                    DependencyCount = $_.Value.Count
                    UsedBy = if ($Analysis.ReverseDependencyMap[$_.Key]) {
                        ($Analysis.ReverseDependencyMap[$_.Key] -join '; ')
                    } else { 'None' }
                    UsageCount = if ($Analysis.ReverseDependencyMap[$_.Key]) {
                        $Analysis.ReverseDependencyMap[$_.Key].Count
                    } else { 0 }
                }
            }

            $exportData | Export-Csv -Path $ExportPath -NoTypeInformation
            Write-Host "`nDetailed report exported to: " -ForegroundColor Green -NoNewline
            Write-Host "$ExportPath" -ForegroundColor Cyan
        }
        catch {
            Write-Host "`nError exporting to CSV: $($_.Exception.Message)" -ForegroundColor Red
        }
    }

    Write-Host "`n$separator" -ForegroundColor Cyan
}

# Sample script for testing
$sampleScript = @'
# Sample PowerShell script with function dependencies

function Get-DatabaseConnection {
    param($ConnectionString)
    # Base utility function
    return "Connection to $ConnectionString"
}

function Get-UserData {
    param($UserId)
    $connection = Get-DatabaseConnection -ConnectionString "UserDB"
    return "User data for $UserId from $connection"
}

function Get-OrderData {
    param($OrderId)
    $connection = Get-DatabaseConnection -ConnectionString "OrderDB"
    return "Order data for $OrderId from $connection"
}

function Generate-UserReport {
    param($UserId)
    $userData = Get-UserData -UserId $UserId
    $orderData = Get-OrderData -OrderId "123"
    return "Report: $userData, $orderData"
}

function Send-EmailNotification {
    param($Message)
    # Isolated function - no dependencies
    return "Email sent: $Message"
}

function Process-ComplexWorkflow {
    param($UserId, $OrderId)
    $userData = Get-UserData -UserId $UserId
    $orderData = Get-OrderData -OrderId $OrderId
    $report = Generate-UserReport -UserId $UserId
    return "Workflow complete: $userData, $orderData, $report"
}

function Unused-HelperFunction {
    # This function is not called by anyone
    return "I'm lonely"
}
'@

Write-Host "Testing with sample script content:" -ForegroundColor Cyan
$dependencyAnalysis = Show-DependencyMap -ScriptContent $sampleScript
Show-DependencyReport -Analysis $dependencyAnalysis

$dashSeparator = "-" * 60
Write-Host "`n$dashSeparator" -ForegroundColor Gray
Write-Host "Usage Examples:" -ForegroundColor Yellow

Write-Host "`nTo analyze a specific file:" -ForegroundColor Yellow
Write-Host '  $analysis = Show-DependencyMap -ScriptPath "C:\Path\To\Your\Script.ps1"' -ForegroundColor White
Write-Host '  Show-DependencyReport -Analysis $analysis' -ForegroundColor White

Write-Host "`nTo export results to CSV:" -ForegroundColor Yellow
Write-Host '  Show-DependencyReport -Analysis $analysis -ExportPath "C:\Reports\Dependencies.csv"' -ForegroundColor White

Write-Host "`nTo analyze the current script:" -ForegroundColor Yellow
Write-Host '  $analysis = Show-DependencyMap -AnalyzeSelf' -ForegroundColor White
Write-Host '  Show-DependencyReport -Analysis $analysis' -ForegroundColor White

These examples demonstrate the power of AST analysis for large-scale PowerShell development. The unused code finder can identify dead code that's been accumulating over months or years, while the dependency mapper reveals the hidden relationships that make refactoring risky without proper analysis.

Supercharging AST Analysis with PSScriptAnalyzer

While the raw AST functionality is powerful on its own, Microsoft's PSScriptAnalyzer module takes PowerShell code analysis to the next level. This static code checker leverages the same AST concepts we've discussed but provides over 50 built-in rules based on PowerShell best practices.

Installing PSScriptAnalyzer

Install-Module PSScriptAnalyzer -Scope CurrentUser

Immediate Value for Large Scripts

For the large script scenarios we discussed earlier, PSScriptAnalyzer can instantly identify many issues without writing custom AST code:

# Comprehensive analysis of a large script
Invoke-ScriptAnalyzer -Path "C:\Scripts\LargeScript.ps1" -Recurse

# Focus on specific rule categories
Invoke-ScriptAnalyzer -Path "C:\Scripts\LargeScript.ps1" -Settings PSGallery

# Automatically fix formatting and style issues
Invoke-ScriptAnalyzer -Path "C:\Scripts\LargeScript.ps1" -Fix

Simplified Unused Code Detection

Instead of the complex unused variable finder we built earlier, PSScriptAnalyzer provides built-in rules:

# Find unused variables with a single command
Invoke-ScriptAnalyzer -Path "C:\Scripts\LargeScript.ps1" -IncludeRule PSUseDeclaredVarsMoreThanAssignments

# Check for other code quality issues
Invoke-ScriptAnalyzer -Path "C:\Scripts\LargeScript.ps1" -IncludeRule PSAvoidUsingWriteHost,PSAvoidGlobalVars

Custom Rules for Specialized Analysis

PSScriptAnalyzer also supports custom rules, allowing you to extend its capabilities for organization-specific requirements:

# Custom PSScriptAnalyzer Rule for Company Standards
function Measure-CompanyStandards {
    <#
    .SYNOPSIS
        Validates PowerShell code against company-specific standards.
    
    .DESCRIPTION
        This custom PSScriptAnalyzer rule checks for:
        - Function naming conventions (must start with approved verbs + "Company")
        - Variable naming standards (PascalCase for parameters, camelCase for local vars)
        - Mandatory comment-based help for public functions
    
    .PARAMETER ScriptBlockAst
        The AST (Abstract Syntax Tree) of the script to analyze.
    
    .OUTPUTS
        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
        Returns diagnostic records for any violations found.
    
    .EXAMPLE
        # This rule will be automatically called by PSScriptAnalyzer when the module is imported
        Invoke-ScriptAnalyzer -Path "MyScript.ps1" -CustomRulePath "CompanyRules.psm1"
    #>
    
    [CmdletBinding()]
    [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst
    )
    
    process {
        $results = @()
        
        try {
            Write-Verbose "Analyzing script with company standards rule"
            
            # Rule 1: Function Naming Convention
            $functions = $ScriptBlockAst.FindAll({
                $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]
            }, $true)
            
            Write-Verbose "Found $($functions.Count) functions to analyze"
            
            foreach ($function in $functions) {
                # Check naming convention: Must start with approved verb + "Company"
                $approvedVerbs = @('Get', 'Set', 'New', 'Remove', 'Test', 'Start', 'Stop', 'Restart', 'Add', 'Clear', 'Copy', 'Move', 'Update', 'Import', 'Export')
                $verbPattern = ($approvedVerbs -join '|')
                $expectedPattern = "^($verbPattern)-Company"
                
                if ($function.Name -notmatch $expectedPattern) {
                    $results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                        "Function '$($function.Name)' doesn't follow company naming convention. Expected format: ApprovedVerb-Company*",
                        $function.Extent,
                        'CompanyNamingConvention',
                        [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Warning,
                        $null
                    )
                }
                
                # Rule 2: Check for comment-based help in public functions
                if ($function.Name -match '^(Get|Set|New|Remove)-Company') {
                    # Look for comment-based help before the function
                    $helpFound = $false
                    
                    # Simple check: look for .SYNOPSIS in the function body or preceding comments
                    $functionText = $function.Extent.Text
                    if ($functionText -match '\.SYNOPSIS' -or $functionText -match '<#[\s\S]*\.SYNOPSIS[\s\S]*#>') {
                        $helpFound = $true
                    }
                    
                    if (-not $helpFound) {
                        $results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                            "Public function '$($function.Name)' is missing comment-based help with .SYNOPSIS",
                            $function.Extent,
                            'CompanyPublicFunctionHelp',
                            [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Information,
                            $null
                        )
                    }
                }
            }
            
            # Rule 3: Parameter Naming Convention (PascalCase)
            $parameters = $ScriptBlockAst.FindAll({
                $args[0] -is [System.Management.Automation.Language.ParameterAst]
            }, $true)
            
            Write-Verbose "Found $($parameters.Count) parameters to analyze"
            
            foreach ($parameter in $parameters) {
                $paramName = $parameter.Name.VariablePath.UserPath
                
                # Skip common PowerShell automatic parameters
                $skipParams = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable')
                
                if ($paramName -notin $skipParams) {
                    # Check if parameter follows PascalCase convention
                    if ($paramName -cnotmatch '^[A-Z][a-zA-Z0-9]*$') {
                        $results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                            "Parameter '$paramName' should use PascalCase naming convention",
                            $parameter.Extent,
                            'CompanyParameterNaming',
                            [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Information,
                            $null
                        )
                    }
                }
            }
            
            # Rule 4: Variable Assignment Conventions (camelCase for local variables)
            $assignments = $ScriptBlockAst.FindAll({
                $args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]
            }, $true)
            
            Write-Verbose "Found $($assignments.Count) variable assignments to analyze"
            
            foreach ($assignment in $assignments) {
                if ($assignment.Left -is [System.Management.Automation.Language.VariableExpressionAst]) {
                    $varName = $assignment.Left.VariablePath.UserPath
                    
                    # Skip special PowerShell variables and short variables
                    $skipVars = @('_', 'PSItem', 'args', 'input', 'matches', 'error', 'lastexitcode')
                    
                    if ($varName -notin $skipVars -and $varName.Length -gt 2) {
                        # Check if local variable follows camelCase (starts with lowercase)
                        if ($varName -cnotmatch '^[a-z][a-zA-Z0-9]*$') {
                            $results += [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
                                "Local variable '$varName' should use camelCase naming convention",
                                $assignment.Left.Extent,
                                'CompanyVariableNaming',
                                [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Information,
                                $null
                            )
                        }
                    }
                }
            }
            
            Write-Verbose "Company standards analysis complete. Found $($results.Count) issues."
            
        }
        catch {
            Write-Error "Error in Measure-CompanyStandards: $($_.Exception.Message)"
        }
        
        return $results
    }
}

# Export the rule function (required for PSScriptAnalyzer to discover it)
Export-ModuleMember -Function Measure-CompanyStandards

# Debug-CompanyRules.ps1 - Test and debug the custom rules

function Test-CompanyRuleDebug {
    Write-Host "=== DEBUGGING COMPANY RULES ===" -ForegroundColor Magenta

    # First, let's test if the module loads correctly
    try {
        Import-Module ".\CompanyRules.psm1" -Force
        Write-Host "âś… Module loaded successfully" -ForegroundColor Green
    }
    catch {
        Write-Host "❌ Failed to load module: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    # Test if the function exists
    if (Get-Command Measure-CompanyStandards -ErrorAction SilentlyContinue) {
        Write-Host "âś… Measure-CompanyStandards function found" -ForegroundColor Green
    } else {
        Write-Host "❌ Measure-CompanyStandards function not found" -ForegroundColor Red
        return
    }

    Write-Host "`n=== TESTING WITH SAMPLE CODE ===" -ForegroundColor Cyan

    # Sample code with clear violations
    $testCode = @'
function BadFunction {
    param($badParam)
    $BadVariable = "test"
    Write-Host "No help"
}

function Get-CompanyData {
    param($Data)
    $result = "missing help"
    return $result
}
'@

    Write-Host "Test code:" -ForegroundColor Yellow
    Write-Host $testCode -ForegroundColor White

    # Parse the code
    Write-Host "`n=== PARSING CODE ===" -ForegroundColor Cyan
    $ast = [System.Management.Automation.Language.Parser]::ParseInput($testCode, [ref]$null, [ref]$null)
    Write-Host "âś… Code parsed successfully" -ForegroundColor Green

    # Check what functions were found
    $functions = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true)
    Write-Host "Found $($functions.Count) functions:" -ForegroundColor Yellow
    foreach ($func in $functions) {
        Write-Host "  - $($func.Name)" -ForegroundColor White
    }

    # Run the custom rule
    Write-Host "`n=== RUNNING CUSTOM RULE ===" -ForegroundColor Cyan
    try {
        $violations = Measure-CompanyStandards -ScriptBlockAst $ast
        Write-Host "âś… Rule executed successfully" -ForegroundColor Green
        Write-Host "Found $($violations.Count) violations:" -ForegroundColor Yellow

        if ($violations.Count -eq 0) {
            Write-Host "❌ No violations found - this might indicate an issue with the rule logic" -ForegroundColor Red
        } else {
            foreach ($violation in $violations) {
                Write-Host "`n[$($violation.Severity)] $($violation.RuleName)" -ForegroundColor Cyan
                Write-Host "Message: $($violation.Message)" -ForegroundColor White
                Write-Host "Line: $($violation.Extent.StartLineNumber)" -ForegroundColor Gray
            }
        }
    }
    catch {
        Write-Host "❌ Error running rule: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Stack trace: $($_.ScriptStackTrace)" -ForegroundColor Red
    }

    Write-Host "`n=== TESTING WITH PSSCRIPTANALYZER ===" -ForegroundColor Cyan

    # Test with actual PSScriptAnalyzer
    if (Get-Module PSScriptAnalyzer -ListAvailable) {
        Write-Host "âś… PSScriptAnalyzer module available" -ForegroundColor Green

        # Create a temporary test file
        $tempFile = "TempTestFile.ps1"
        $testCode | Out-File -FilePath $tempFile -Encoding UTF8

        try {
            Write-Host "Running PSScriptAnalyzer with custom rule..." -ForegroundColor Yellow
            $results = Invoke-ScriptAnalyzer -Path $tempFile -CustomRulePath ".\CompanyRules.psm1"

            if ($results.Count -eq 0) {
                Write-Host "❌ PSScriptAnalyzer found no violations" -ForegroundColor Red
                Write-Host "This could mean:" -ForegroundColor Yellow
                Write-Host "  1. The rule isn't being loaded properly" -ForegroundColor Yellow
                Write-Host "  2. The rule logic isn't matching the violations" -ForegroundColor Yellow
                Write-Host "  3. The test code doesn't actually violate the rules" -ForegroundColor Yellow
            } else {
                Write-Host "âś… PSScriptAnalyzer found $($results.Count) violations:" -ForegroundColor Green
                foreach ($result in $results) {
                    Write-Host "  - $($result.RuleName): $($result.Message)" -ForegroundColor White
                }
            }
        }
        catch {
            Write-Host "❌ Error running PSScriptAnalyzer: $($_.Exception.Message)" -ForegroundColor Red
        }
        finally {
            # Clean up temp file
            if (Test-Path $tempFile) {
                Remove-Item $tempFile -Force
            }
        }
    } else {
        Write-Host "❌ PSScriptAnalyzer module not available" -ForegroundColor Red
        Write-Host "Install it with: Install-Module PSScriptAnalyzer" -ForegroundColor Yellow
    }
}

# Run the debug test
Test-CompanyRuleDebug

# Instructions for using this as a PSScriptAnalyzer rule
$usageInstructions = @"

đź“‹ HOW TO USE THIS CUSTOM RULE:

1. Save this file as 'CompanyRules.psm1'

2. Use with PSScriptAnalyzer:
   Invoke-ScriptAnalyzer -Path "YourScript.ps1" -CustomRulePath "CompanyRules.psm1"

3. Or include in your PSScriptAnalyzer settings file:
   @{
       CustomRulePath = 'CompanyRules.psm1'
       Rules = @{
           PSUseApprovedVerbs = @{Enable = $true}
           CompanyNamingConvention = @{Enable = $true}
           CompanyParameterNaming = @{Enable = $true}
           CompanyVariableNaming = @{Enable = $true}
           CompanyPublicFunctionHelp = @{Enable = $true}
       }
   }

4. Test the rule:
   Test-CompanyRule

"@

Write-Host $usageInstructions -ForegroundColor Yellow

# Run the test automatically
Write-Host "`nRunning test automatically..." -ForegroundColor Cyan
Test-CompanyRule

As always, as I constantly tinker and update, the most up-to-date version of scripts used here can be found on the companion GitHub.

Integration with Development Workflow

PSScriptAnalyzer integrates seamlessly with popular editors like VS Code, providing real-time feedback as you write code. This transforms AST analysis from a separate step into an ongoing part of the development process.

When to Use Raw AST vs PSScriptAnalyzer

Use PSScriptAnalyzer when:

  • Performing standard code quality checks
  • Looking for common PowerShell anti-patterns
  • Integrating analysis into CI/CD pipelines
  • Need automatic code fixes
  • Working with team standards and style guides

Use raw AST when:

  • Building specialized analysis tools
  • Creating custom development utilities
  • Need precise control over parsing and analysis
  • Developing your own static analysis rules
  • Working with AST manipulation and transformation

The beauty is that these approaches complement each other perfectly. PSScriptAnalyzer handles the common cases efficiently, while raw AST provides the foundation for specialized tooling.

Getting Started

Begin with PSScriptAnalyzer to get immediate value from AST-based analysis, then gradually explore raw AST manipulation for more specialized needs. The PowerShell AST documentation provides comprehensive details about all available node types and their properties, making it an excellent reference as you develop your AST skills.

Remember that working with AST is about precision and reliability—you're leveraging PowerShell's own understanding of its language to build tools that work correctly across all the edge cases and complexities that make PowerShell powerful.

As you explore PowerShell AST and build your own analysis tools, I'll leave with these thought-provoking questions that could spark valuable discussions:

For Script Analysis:

  • What's the largest PowerShell script you've had to maintain, and what AST analysis would have saved you the most time?
  • Have you discovered any surprising patterns or anti-patterns in your organization's PowerShell codebases using AST analysis?
  • Which PSScriptAnalyzer rules do you find most valuable in your daily work, and are there custom rules you wish existed?

For Tool Development:

  • What PowerShell development pain points could be solved with better AST-based tooling?
  • If you could build the perfect PowerShell code analysis dashboard, what metrics would it display?
  • How do you balance the depth of analysis with performance when working with large scripts or modules?

For Team Collaboration:

  • How has static code analysis changed your team's PowerShell development workflow?
  • What coding standards or conventions has AST analysis helped you enforce across your organization?
  • Have you used AST analysis to help onboard new team members or audit legacy scripts?

For Advanced Scenarios:

  • What creative applications of PowerShell AST have you discovered beyond traditional code analysis?
  • How do you handle AST analysis across different PowerShell versions (5.1 vs 7+) in mixed environments?
  • What would you want to see in the next generation of PowerShell development tooling?

For Learning and Growth:

  • Which AST concepts were most challenging to understand initially, and how did you overcome those hurdles?
  • What resources or examples helped you transition from basic PowerShell scripting to AST manipulation?
  • How has understanding AST changed the way you write PowerShell code?