src/Cyclomatic.ps1

<#
.SYNOPSIS
    Cyclomatic complexity per unit. Depends on Ast.ps1.

.DESCRIPTION
    Cyclomatic complexity = 1 + decision points, where a decision point is each
    if/elseif clause, each switch clause, each for/foreach/while/do loop, each
    catch/trap, each ternary, and each -and/-or in a boolean expression. This is the
    classic control-flow count (independent-path proxy); pairs with cognitive
    complexity, which weights nesting instead.
#>


function Get-PSCxCycClauseRow {
    # if / switch: one decision per CLAUSE. `else` is not a clause -- it is the absence of a
    # decision -- so Clauses.Count is the count, not Clauses.Count + 1.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.IfStatementAst] }, $true)) {
        [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = $n.Clauses.Count }
    }
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.SwitchStatementAst] }, $true)) {
        [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = $n.Clauses.Count }
    }
}

function Get-PSCxCycBlockRow {
    # Loops, catch and trap: one decision each.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    foreach ($tn in 'ForEachStatementAst', 'ForStatementAst', 'WhileStatementAst', 'DoWhileStatementAst', 'DoUntilStatementAst', 'CatchClauseAst', 'TrapStatementAst') {
        # The closure is required: without it $tn resolves at CALL time, when the loop has
        # already finished, and every type matches the last name in the list.
        foreach ($n in $Ast.FindAll({ param($x) $x.GetType().Name -eq $tn }.GetNewClosure(), $true)) {
            [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 }
        }
    }
}

function Get-PSCxCycFlowCommandRow {
    # ForEach-Object / Where-Object and their aliases: one decision each, exactly as the
    # keyword loop and conditional they stand in for.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    foreach ($n in $Ast.FindAll({ param($x) Test-PSCxFlowCommand -Node $x }, $true)) {
        [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 }
    }
}

function Get-PSCxCycOperatorRow {
    # Decisions expressed as operators rather than statements.
    #
    # && and || are control flow between pipelines, not boolean operators: `a && b` runs b
    # only if a succeeded, which is a decision exactly as `if ($?)` would be. ?? and ??= each
    # choose between two values on a null test.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.PipelineChainAst] }, $true)) {
        [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 }
    }
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.TernaryExpressionAst] }, $true)) {
        [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 }
    }
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.BinaryExpressionAst] }, $true)) {
        if ($n.Operator -in 'And', 'Or', 'QuestionQuestion') { [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 } }
    }
    foreach ($n in $Ast.FindAll({ param($x) $x -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true)) {
        if ($n.Operator -eq 'QuestionQuestionEquals') { [pscustomobject]@{ Key = Get-PSCxUnitKey -Node $n; Amount = 1 } }
    }
}

function Get-PSCxCyclomaticRow {
    # Every decision-point row, attributed per unit. Composed from one collector per KIND of
    # decision, mirroring Cognitive.ps1 -- adding a construct then means a new collector or a
    # new entry in one, never another loop in a function that already has eight.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    @(Get-PSCxCycClauseRow -Ast $Ast) + @(Get-PSCxCycBlockRow -Ast $Ast) +
    @(Get-PSCxCycFlowCommandRow -Ast $Ast) + @(Get-PSCxCycOperatorRow -Ast $Ast)
}

function Get-PSCxCyclomaticMap {
    # unit key -> cyclomatic complexity (1 + summed decision points).
    [OutputType([hashtable])]
    [CmdletBinding()]
    param([Parameter(Mandatory)] $Ast)
    $map = @{}
    foreach ($row in Get-PSCxCyclomaticRow -Ast $Ast) {
        $map[$row.Key] = [int]$map[$row.Key] + $row.Amount
    }
    $out = @{}
    foreach ($k in (Get-PSCxUnitTable -Ast $Ast).Keys) { $out[$k] = 1 + [int]$map[$k] }
    return $out
}