src/Measure-PSComplexity.ps1
|
<# .SYNOPSIS Public API for PSComplexity: Measure-PSComplexity (data) and Test-PSComplexity (gate). #> function Get-PSCxSourceFile { # Resolve a path (file or directory) to the PowerShell source files to measure. [OutputType([string[]])] [CmdletBinding()] param([Parameter(Mandatory)] [string]$Path, [switch]$Recurse) if (Test-Path -LiteralPath $Path -PathType Leaf) { return [string[]]@((Resolve-Path -LiteralPath $Path).Path) } $gci = @{ Path = $Path; Include = @('*.ps1', '*.psm1'); File = $true; Recurse = [bool]$Recurse } return [string[]]@(Get-ChildItem @gci | ForEach-Object { $_.FullName }) } function Measure-PSComplexity { <# .SYNOPSIS Measure cyclomatic and cognitive complexity of PowerShell code, per unit (each function/filter, plus one <script-body> per file for top-level code). .DESCRIPTION Parses each .ps1/.psm1 file with the PowerShell AST and reports both metrics. Cognitive complexity is a faithful port of the SonarSource metric (nesting-aware); cyclomatic is the classic decision-point count. Files that fail to parse are skipped with a warning. .PARAMETER Path One or more files or directories to measure. .PARAMETER Recurse Recurse into subdirectories when a directory is given. .OUTPUTS [pscustomobject] with File, Unit, Line, Cyclomatic, Cognitive -- one per unit. .EXAMPLE Measure-PSComplexity ./src -Recurse | Sort-Object Cognitive -Descending .EXAMPLE # Fail a build if anything is too complex: if (-not (Test-PSComplexity ./src -Recurse)) { exit 1 } #> [OutputType([pscustomobject[]])] [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)] [ValidateNotNullOrEmpty()] [string[]]$Path, [switch]$Recurse ) process { foreach ($p in $Path) { foreach ($file in (Get-PSCxSourceFile -Path $p -Recurse:$Recurse)) { $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$null, [ref]$errors) if ($errors) { Write-Warning "Skipped '$file' -- parse error: $($errors[0].Message)"; continue } $cyc = Get-PSCxCyclomaticMap -Ast $ast $cog = Get-PSCxCognitiveMap -Ast $ast $lines = Get-PSCxUnitTable -Ast $ast foreach ($k in $lines.Keys) { [pscustomobject]@{ File = $file Unit = ($k -replace '@\d+$', '') Line = $lines[$k] Cyclomatic = $cyc[$k] Cognitive = $cog[$k] } } } } } } function Test-PSComplexity { <# .SYNOPSIS Return $true if every unit is at or under the cyclomatic and cognitive ceilings; otherwise $false, writing a warning per offending unit. Intended as a CI gate. .PARAMETER Path One or more files or directories to check. .PARAMETER MaxCyclomatic Cyclomatic ceiling (default 15). .PARAMETER MaxCognitive Cognitive ceiling (default 15). .PARAMETER Recurse Recurse into subdirectories when a directory is given. .OUTPUTS [bool] .EXAMPLE if (-not (Test-PSComplexity ./src -Recurse)) { throw 'Complexity gate failed' } #> [OutputType([bool])] [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string[]]$Path, [int]$MaxCyclomatic = 15, [int]$MaxCognitive = 15, [switch]$Recurse ) $violations = @(Measure-PSComplexity -Path $Path -Recurse:$Recurse | Where-Object { $_.Cyclomatic -gt $MaxCyclomatic -or $_.Cognitive -gt $MaxCognitive }) foreach ($v in $violations) { Write-Warning ("{0}:{1} {2} -- cyclomatic {3} (max {4}), cognitive {5} (max {6})" -f ` $v.File, $v.Line, $v.Unit, $v.Cyclomatic, $MaxCyclomatic, $v.Cognitive, $MaxCognitive) } return $violations.Count -eq 0 } |