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

function Resolve-BcAlAppContext {
    <#
    .SYNOPSIS
        Resolves the AL app an .al file belongs to: its manifest, whether it is a test app, and the
        preprocessor symbols it was compiled with.
 
    .DESCRIPTION
        Coverage needs two facts about every source file, and both come from the same place - the
        app.json of the app the file belongs to:
 
          * Is this a TEST app? Its objects must not be measured. Test code is covered by construction
            (it runs when the test runs), so counting it rewards test volume instead of test depth. In
            one real repository 42% of the denominator was test code.
          * Which preprocessor symbols were used? Lines inside an inactive #if branch are not compiled
            and can never be covered, so counting them only inflates the denominator.
 
        The symbols are read from the manifest rather than passed down from the pipeline: the compile
        step writes them there (Update-BcAppManifest sets 'preprocessorSymbols' alongside application,
        runtime and platform) before the tests run. That makes the coverage view agree with what alc.exe
        actually compiled - including repository-specific symbols such as ONPREM or CLEAN - without any
        pipeline wiring, and it is per-app-correct in a repository holding several apps.
 
        Results are cached per app folder: Test-BcIsTestApp may scan AL source when the manifest is
        inconclusive, which is far too expensive to repeat for every file of an app.
 
    .PARAMETER Path
        The .al file (or a folder) to resolve the owning app for.
 
    .OUTPUTS
        PSCustomObject: AppFolder, Manifest, IsTestApp, PreprocessorSymbols.
        A file outside any app (no app.json above it) yields IsTestApp = $false and no symbols, so
        callers behave exactly as they did before this existed.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path
    )

    # Test-Path over the variable provider, not `-not $script:...`: the module runs under
    # Set-StrictMode -Version Latest, where reading a never-assigned variable throws.
    if (-not (Test-Path -LiteralPath 'variable:script:BcAlAppContextCache')) { $script:BcAlAppContextCache = @{} }

    $none = [PSCustomObject]@{ AppFolder = ''; Manifest = $null; IsTestApp = $false; PreprocessorSymbols = @() }

    # Start at the file's folder (or the folder itself) and walk up to the first app.json.
    $dir = if (Test-Path -LiteralPath $Path -PathType Container) { $Path } else { Split-Path -Parent $Path }
    if (-not $dir) { return $none }

    $appFolder = $null
    $current = $dir
    while ($current) {
        if (Test-Path -LiteralPath (Join-Path $current 'app.json')) { $appFolder = $current; break }
        $parent = Split-Path -Parent $current
        if ($parent -eq $current) { break }   # reached the drive/volume root
        $current = $parent
    }
    if (-not $appFolder) { return $none }

    $key = $appFolder.ToLowerInvariant()
    if ($script:BcAlAppContextCache.ContainsKey($key)) { return $script:BcAlAppContextCache[$key] }

    $manifest = $null
    try { $manifest = Get-Content -LiteralPath (Join-Path $appFolder 'app.json') -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop }
    catch {
        Write-ALbuildLog -Level Verbose "Could not read '$appFolder\app.json' ($($_.Exception.Message)); treating it as a non-test app without symbols."
        $script:BcAlAppContextCache[$key] = $none
        return $none
    }

    $isTest = $false
    try { $isTest = [bool] (Test-BcIsTestApp -Manifest $manifest -AppFolder $appFolder) }
    catch { Write-ALbuildLog -Level Verbose "Could not classify '$appFolder' as app/test app ($($_.Exception.Message)); treating it as an app." }

    $symbols = @()
    if ($manifest.PSObject.Properties.Name -contains 'preprocessorSymbols') {
        $symbols = @($manifest.preprocessorSymbols | Where-Object { -not [string]::IsNullOrWhiteSpace("$_") } | ForEach-Object { "$_".Trim() })
    }

    $context = [PSCustomObject]@{
        AppFolder           = $appFolder
        Manifest            = $manifest
        IsTestApp           = $isTest
        PreprocessorSymbols = $symbols
    }
    $script:BcAlAppContextCache[$key] = $context
    return $context
}