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

function Resolve-BcAnalyzer {
    <#
    .SYNOPSIS
        Resolves analyzer references (tokens, short names, or DLL paths) to compiler analyzer DLLs.
    .DESCRIPTION
        Internal helper. The AL toolchain names its built-in analyzers with tokens
        ('${CodeCop}', '${UICop}', '${AppSourceCop}', '${PerTenantExtensionCop}') that the VS Code AL
        extension resolves to DLLs; a CLI compile must resolve them itself. This maps each token/short
        name (with or without the '${...}' wrapper, case-insensitive) to its
        'Microsoft.Dynamics.Nav.<Cop>.dll' file and locates it under -SearchRoot (searched recursively
        - the AL Tool ships them in an 'Analyzers' folder; the container's AL Language extension in
        'bin/Analyzers'). An entry that is already a path to an existing .dll is returned as-is; a
        custom '<name>.dll' is located by that file name. Unresolved entries are dropped and reported
        via -UnresolvedVariable so the caller can warn. Returns the resolved DLL paths.
    .PARAMETER Analyzer
        The analyzer references to resolve.
    .PARAMETER SearchRoot
        The folder tree to search for the analyzer DLLs (the compiler's install directory).
    .PARAMETER UnresolvedVariable
        Name of a variable in the caller's scope to receive the references that could not be resolved.
    .OUTPUTS
        System.String[]: resolved analyzer DLL paths.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [string[]] $Analyzer = @(),
        [string] $SearchRoot,
        [string] $UnresolvedVariable
    )

    $map = @{
        'codecop'               = 'Microsoft.Dynamics.Nav.CodeCop.dll'
        'uicop'                 = 'Microsoft.Dynamics.Nav.UICop.dll'
        'appsourcecop'          = 'Microsoft.Dynamics.Nav.AppSourceCop.dll'
        'pertenantextensioncop' = 'Microsoft.Dynamics.Nav.PerTenantExtensionCop.dll'
    }

    $resolved = [System.Collections.Generic.List[string]]::new()
    $unresolved = [System.Collections.Generic.List[string]]::new()
    $dllCache = $null   # lazily filled once, only if a token needs a search

    foreach ($a in $Analyzer) {
        if ([string]::IsNullOrWhiteSpace($a)) { continue }
        $token = $a.Trim()

        # Already a concrete, existing .dll path.
        if ($token -match '\.dll$' -and (Test-Path -LiteralPath $token)) {
            $resolved.Add((Resolve-Path -LiteralPath $token).Path)
            continue
        }

        # '${CodeCop}' / 'CodeCop' / 'codecop' -> the mapped DLL file name; a bare '<name>.dll' -> that name.
        $key = $token.Trim('$', '{', '}').ToLowerInvariant()
        $dllName = $map[$key]
        if (-not $dllName -and $token -match '\.dll$') { $dllName = Split-Path -Path $token -Leaf }
        if (-not $dllName) { $unresolved.Add($token); continue }

        if (-not $SearchRoot -or -not (Test-Path -LiteralPath $SearchRoot)) { $unresolved.Add($token); continue }
        if ($null -eq $dllCache) {
            $dllCache = @(Get-ChildItem -LiteralPath $SearchRoot -Recurse -Filter '*.dll' -File -ErrorAction SilentlyContinue)
        }
        $hit = $dllCache | Where-Object { $_.Name -ieq $dllName } | Select-Object -First 1
        if ($hit) { $resolved.Add($hit.FullName) } else { $unresolved.Add($token) }
    }

    if ($UnresolvedVariable) { Set-Variable -Name $UnresolvedVariable -Scope 1 -Value @($unresolved) }
    return @($resolved | Select-Object -Unique)
}