PSComplexity.psd1

@{
    RootModule           = 'PSComplexity.psm1'
    ModuleVersion        = '0.6.0'
    GUID                 = '961aa886-4f8e-40c0-9d25-68fd4c52e69f'
    Author               = 'Fortigi'
    CompanyName          = 'Fortigi'
    Copyright            = '(c) Fortigi. MIT licensed.'
    Description          = 'Cyclomatic and cognitive complexity for PowerShell. Cognitive complexity implements the SonarSource metric in full (nesting-aware -- the better signal for "hard to understand"), scoring every reference example exactly as published, and extends it for PowerShell constructs the specification does not cover: ForEach-Object and Where-Object, the .ForEach() and .Where() methods, the && and || pipeline chains, and ?? and ??=. Measures per unit (function/filter, class method/constructor, initialised class property, + script body) via the PowerShell AST; ships a Test-PSComplexity gate for CI.'
    PowerShellVersion    = '7.0'
    CompatiblePSEditions = @('Core')

    FunctionsToExport    = @('Measure-PSComplexity', 'Test-PSComplexity')
    CmdletsToExport      = @()
    VariablesToExport    = @()
    AliasesToExport      = @()

    PrivateData = @{
        PSData = @{
            Tags         = @('complexity', 'cyclomatic', 'cognitive', 'code-quality', 'ast', 'metrics', 'maintainability', 'lint', 'ci')
            LicenseUri   = 'https://github.com/Fortigi/PSComplexity/blob/main/LICENSE'
            ProjectUri   = 'https://github.com/Fortigi/PSComplexity'
            ReleaseNotes = '0.6.0: **The `.ForEach()` and `.Where()` methods now score as `ForEach-Object` and `Where-Object` do.** The methods take a script block exactly as the cmdlets do, and branch exactly as much, but scored nothing: `$x | Where-Object { ... }` scored one more than the identical `$x.Where({ ... })`, so the method form was a cheaper spelling of the same logic. Each is now one decision for cyclomatic and one increment plus nesting for cognitive, the same as the cmdlet. Only instance calls count -- the intrinsics are never static, so `[Type]::Where()` is somebody else''s method. **Scores rise for any code that uses them, and the metric version is now 2.** A baseline recorded against version 1 is refused rather than compared; regenerate it with `-UpdateBaseline` and review the diff. Code that uses neither method, nor a recursion form below, scores exactly as before, and every SonarSource reference case is unchanged. The gallery description names the methods too; it had gone on listing the extensions without them, and a test now holds it to the README''s list of scored constructs. **A large baseline no longer slows the gate down.** Each baseline entry and each acceptance was checked by searching the whole list of measured units, so the check took entries x units: a 2,000-entry baseline over 2,200 units took a gate from 9 seconds to 31. The units are now indexed once per run and each entry is a single lookup -- measured on that size, 12.3 seconds of checking becomes 0.3. Matching is unchanged: file and unit, ignoring case. This one moves no score. **A path on a UNC share is measured, and named, like a local one.** PowerShell writes a share path with a `Microsoft.PowerShell.Core\FileSystem::` prefix, and two places kept it. A FILE on a share -- `Measure-PSComplexity \\server\share\src\A.ps1`, or `./A.ps1` with the current location on a share -- was skipped as "could not be read", and the gate refused the run. Worse, quietly: with the current location on a share, a measured DIRECTORY named every file by its full path rather than relative to that location, so baseline and acceptance entries no longer matched, and a `-ChangedFile` list matched nothing -- which the gate reads as "no PowerShell changed" and passes. Paths are now resolved to the plain file-system form, so a share, or any path written provider-qualified, behaves exactly as a local path does. This one moves no score either. **Two files whose names differ only in case are both measured where the file system tells them apart.** The scan compared paths ignoring case on every platform, so next to `src/A.ps1` the file `src/a.ps1` was skipped as already seen -- without a word -- and the gate passed over code it never read. That happened on Linux, in a WSL project measured from Windows PowerShell, and in a case-sensitive Windows directory. File names are now compared the way the working directory''s file system compares them, read once per run (the platform decides only if the directory cannot say). `-ChangedFile`, `-Accept` and the baseline follow the same rule, so on a case-sensitive file system an entry must name a file in its exact case; a unit name still ignores case everywhere. A case-sensitive folder inside a case-insensitive project is reported rather than guessed at: the second of two files differing only in case is skipped, naming the first, and the gate refuses the run. **`-UpdateBaseline` no longer adds a unit the baseline has never recorded without being told to.** The "never record anything worse" check only looked at units the file already named, so a unit renamed *and* made worse -- `A` at 3 becoming `A2` at 10 -- and a brand-new breaching unit were both written in silently: the gate refused the run, and re-running with `-UpdateBaseline` absorbed the regression it had just caught. Updating an existing baseline now refuses such units, naming each with its scores; the new **`-AllowNew`** switch records them deliberately, and each one is still named in a warning. Seeding a baseline, or regenerating one across a metric version change, is unchanged and needs no switch. **Measuring is markedly faster.** Nearly all of a file''s measurement time was PowerShell''s price for calling a .NET method once per syntax node, and a second walk of the whole tree to find the units. The index pass now reads through indexers -- 51.8us a node down to 12.7us -- and the unit table reads the index instead of walking again: 209ms to 7ms on a 28k-node file. A 28k-node file went from about 1.85s to 0.8s, and the module''s own source from about 660ms to 360ms. Every score, unit and `-Detailed` contribution is unchanged. **The README now records how a construct in a condition is scored.** `if ($x ? 1 : 2)` charges the ternary the `if`''s nesting, as one in its body would be. The specification does not decide it and SonarSource''s own implementations disagree -- SonarC# charges it, SonarJava does not for an `if` -- so the choice and both implementations are now written down rather than left to be reported as a bug. Nothing about the scores changes. **`-Detailed` gives each clause of an `if` chain its own row, and names method recursion.** An `if`/`elseif`/`else` chain was one row at the `if` line carrying every clause''s point -- `if +4` -- so the breakdown could not say which branch cost what. Each `elseif` and the `else` is now its own `+1` row at its own line, construct `elseif` or `else`. A class method calling itself was reported as construct `unknown`; it is `recursion`, as a function calling itself already was. Every total is unchanged; only the rows that make it up are split and named. **`-Detailed` rows on one line are in the order they are written.** Rows were sorted by line and then by construct NAME, so a chain on one line read `else, elseif, if`, and an `if` came after the boolean run in its own condition. They are now ordered left to right within a line, as the README already promised. No number changes, but rows that share a line may come out in a different order than before. **Recursion is counted through a module qualifier or a scope modifier, and a scope modifier is no longer part of a unit''s name.** Recursion was an exact match on the command text, so `MyMod\Get-Fact` or `script:Get-Fact` inside `Get-Fact`, and `Get-Fact` inside `function global:Get-Fact`, scored nothing. Each now counts; a path such as `.\Get-Fact` runs a script, not the function, and still does not. The unit was named `global:Get-Fact`, so adding or removing the modifier renamed it and orphaned its baseline entry and acceptance. It is named `Get-Fact` now. **An acceptance naming the old form fails** as naming no measured unit -- write it without the modifier. A baseline needs no separate step: this release''s metric version change already has it regenerated. A `global:` and a plain definition of one name in one file are now duplicates, numbered `#1`, `#2`. **A `-ChangedFile` list naming PowerShell files that are not there is refused.** The list is resolved against the current directory and git prints paths relative to the repository root, so a gate run from a subdirectory matched nothing, measured no units and **passed**, with only the subset notice to show for it. Both commands now refuse a listed `.ps1` or `.psm1` that does not exist, naming it and the likely cause. A deleted file is refused too: list changes with `git diff --name-only --diff-filter=d`, as every example now does. Other entries, such as a deleted README, are not checked. **Hidden files and dot-directories are measured on every OS, and a host that cannot parse a file says so.** On Linux, `-Recurse` skipped dot-directories and dot-files silently, so `.github/scripts/x.ps1` was measured on Windows and not on Linux: one tree, two answers. Both now measure it. **On Linux this can add units to a run**, and a unit over the ceiling there now fails the gate. A `workflow`, which PowerShell 7 cannot parse, and a DSC `Configuration` on a host without the DSC schema store (Linux by default) were skipped as "parse error", and the DSC one carried a whole .NET stack trace. Each is now named for what it is, in one line. The gate still refuses such a run. **Recursion is counted through `${function:Name}` and `$MyInvocation`, and a script calling itself counts too.** Recursion was only ever a function calling its own name, so `& ${function:Get-Fact}` and `& $MyInvocation.MyCommand` inside `Get-Fact` scored nothing, and a script recursing into itself -- through `& $PSCommandPath`, `$MyInvocation.MyCommand.Path`, or its own file name as `.\Self.ps1` or `$PSScriptRoot\Self.ps1` -- could not score recursion at all. Each now counts +1, the `<script-body>` unit''s for a script. **Scores rise** where these forms are used, under the same metric version 2, so one regenerated baseline covers both changes. The README''s new "Recursion" section lists every form recognised and those left out on purpose. `& $function:Get-Fact` stays uncounted: it is `$function:Get` followed by the text `-Fact`, and does not run `Get-Fact`. Full changelog: https://github.com/Fortigi/PSComplexity/blob/main/CHANGELOG.md'
        }
    }
}