Modules/businessdev.ALbuild.Apps/Private/ConvertTo-BcCoverageExclusionRule.ps1

function ConvertTo-BcCoverageExclusionRule {
    <#
    .SYNOPSIS
        Turns albuild.json 'coverage.exclusions' entries into matchable rules.
 
    .DESCRIPTION
        An entry is { object, procedure?, lines?, reason } (a PSCustomObject from JSON or a hashtable).
        'object' is "<Type> <Id>" or "<Type> <Name>" (the name may be quoted). 'lines' is a range text
        ("10-12, 20") or a number array. An entry without 'object' or without 'reason' is ignored with a
        warning: an exclusion nobody can explain is exactly what the report must not hide.
 
    .OUTPUTS
        PSCustomObject per valid entry: Type, Key, Procedure, Lines (HashSet[int] or $null), Reason, Label, Matched.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param([AllowNull()] [object] $Entry)

    $read = {
        param($e, [string] $name)
        if ($e -is [System.Collections.IDictionary]) {
            foreach ($k in $e.Keys) { if ("$k" -ieq $name) { return $e[$k] } }
            return $null
        }
        $prop = $e.PSObject.Properties[$name]
        if ($prop) { return $prop.Value }
        return $null
    }

    foreach ($e in @($Entry)) {
        if ($null -eq $e) { continue }
        $object = "$(& $read $e 'object')".Trim()
        $reason = "$(& $read $e 'reason')".Trim()
        $procedure = "$(& $read $e 'procedure')".Trim().Trim('"')
        $label = if ($procedure) { "$object / $procedure" } else { $object }
        if (-not $object -or $object -notmatch '^(?<type>[A-Za-z]+)\s+(?<key>.+)$') {
            Write-ALbuildLog -Level Warning "Coverage exclusion ignored: 'object' must be '<Type> <Id>' or '<Type> <Name>', got '$object'."
            continue
        }
        $type = $Matches['type']
        $key = $Matches['key'].Trim().Trim('"')
        if (-not $reason) {
            Write-ALbuildLog -Level Warning "Coverage exclusion '$label' ignored: it has no 'reason'. Every excluded line needs one."
            continue
        }

        $lineSet = $null
        $linesValue = & $read $e 'lines'
        if ($null -ne $linesValue -and "$linesValue".Trim()) {
            $lineSet = [System.Collections.Generic.HashSet[int]]::new()
            $parts = if ($linesValue -is [string]) { $linesValue -split ',' } else { @($linesValue) }
            foreach ($part in $parts) {
                $t = "$part".Trim()
                if ($t -match '^(\d+)\s*-\s*(\d+)$') { for ($n = [int]$Matches[1]; $n -le [int]$Matches[2]; $n++) { [void]$lineSet.Add($n) } }
                elseif ($t -match '^\d+$') { [void]$lineSet.Add([int]$t) }
                elseif ($t) { Write-ALbuildLog -Level Warning "Coverage exclusion '$label': '$t' is not a line or a range and is ignored." }
            }
            $label = "$label (lines $linesValue)"
        }

        [PSCustomObject]@{
            Type      = $type
            Key       = $key
            Procedure = $procedure
            Lines     = $lineSet
            Reason    = $reason
            Label     = $label
            Matched   = $false
        }
    }
}

function Test-BcCoverageExclusionRuleObject {
    <#
    .SYNOPSIS
        Whether an exclusion rule names the given object, by id or by name (case-insensitive).
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)] [object] $Rule,
        [Parameter(Mandatory)] [string] $Type,
        [Parameter(Mandatory)] [int] $Id,
        [AllowEmptyString()] [string] $Name
    )
    if ($Rule.Type -ine $Type) { return $false }
    return ($Rule.Key -eq "$Id") -or ($Name -and $Rule.Key -ieq $Name)
}