Modules/businessdev.ALbuild.Apps/Private/Resolve-BcAlAssertionReach.ps1

function Resolve-BcAlAssertionReach {
    <#
    .SYNOPSIS
        Answers whether an assertion is reachable from a set of statements through calls into the workspace.
    .DESCRIPTION
        A test that delegates its checks - `MatrixLibrary.RunCase('AA-DEB-P1-B1-A1-X11')` - asserts nothing
        in its own body, and a per-body scan therefore has to either report it or trust the callee's name.
        This walks the calls instead: every identifier followed by '(' is looked up in the procedure index,
        and the callee's body is examined for an assertion, then its callees, up to MaxDepth.
 
        Only procedures that exist in the scanned source are followed, so the walk terminates on AL built-ins
        and on calls into dependency apps. Each procedure is visited once, which also breaks recursion.
    .PARAMETER Statements
        The statement lines to start from (the test body).
    .PARAMETER Index
        Procedure index: lowercase procedure name -> list of statement arrays. Several procedures may share
        a name (one per object), so all candidates are examined; the walk asks whether an assertion is
        reachable, not which overload the compiler would pick.
    .PARAMETER AssertionRegex
        The assertion pattern applied to the callee bodies.
    .PARAMETER MaxDepth
        How many call levels to follow. 1 = only the procedures the test calls itself.
    .OUTPUTS
        PSCustomObject (via, depth) when an assertion is reachable, otherwise $null.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $Statements,
        [Parameter(Mandatory)] [System.Collections.IDictionary] $Index,
        [Parameter(Mandatory)] [regex] $AssertionRegex,
        [int] $MaxDepth = 3
    )

    if ($MaxDepth -lt 1) { return $null }

    $callRx = [regex]::new('(?i)([A-Za-z0-9_]+)\s*\(')
    $visited = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    $frontier = @($Statements)

    for ($depth = 1; $depth -le $MaxDepth; $depth++) {
        $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
        foreach ($statement in $frontier) {
            foreach ($call in $callRx.Matches($statement)) { [void]$names.Add($call.Groups[1].Value) }
        }

        $next = [System.Collections.Generic.List[string]]::new()
        foreach ($name in $names) {
            if (-not $visited.Add($name)) { continue }
            if (-not $Index.Contains($name)) { continue }
            foreach ($body in @($Index[$name])) {
                foreach ($statement in @($body)) {
                    if ($AssertionRegex.IsMatch($statement)) {
                        return [PSCustomObject]@{ via = $name; depth = $depth }
                    }
                    [void]$next.Add($statement)
                }
            }
        }

        if ($next.Count -eq 0) { break }
        $frontier = @($next)
    }

    return $null
}