Modules/businessdev.ALbuild.Apps/Public/Resolve-BcAnalyzerConfig.ps1

function Resolve-BcAnalyzerConfig {
    <#
    .SYNOPSIS
        Determines the code analyzers and ruleset for a compile, layering the sources by precedence.
 
    .DESCRIPTION
        Precedence (highest first): an explicit value (the task input) > the project's albuild.json
        (Analyzers / RuleSet) > the project's .vscode/settings.json ('al.codeAnalyzers' /
        'al.ruleSetPath'). Only the highest source that supplies a value is used (the sources do not
        merge), matching how an explicit pipeline input overrides committed config. Analyzer entries
        are returned as given (tokens / short names / DLL paths - Resolve-BcAnalyzer maps them at
        compile time); a relative ruleset path is resolved against the project folder.
 
    .PARAMETER ProjectFolder
        The AL project folder (holds app.json, optionally .vscode/settings.json and albuild.json).
 
    .PARAMETER WorkspaceRoot
        Workspace root for albuild.json resolution (merged root + app). Default: the project folder.
 
    .PARAMETER InputAnalyzers
        Explicit analyzers from the task input (comma/semicolon separated). Wins over config.
 
    .PARAMETER InputRuleSet
        Explicit ruleset path from the task input. Wins over config.
 
    .OUTPUTS
        PSCustomObject: Analyzers (string[]), RuleSet (string), AnalyzersSource, RuleSetSource.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [string] $ProjectFolder,
        [string] $WorkspaceRoot,
        [string] $InputAnalyzers,
        [string] $InputRuleSet
    )

    if (-not $WorkspaceRoot) { $WorkspaceRoot = $ProjectFolder }

    # albuild.json (Analyzers / RuleSet)
    $cfgAnalyzers = @(); $cfgRuleSet = ''
    $albuildJsonPath = Join-Path $ProjectFolder 'albuild.json'
    # Checked up front, not in the catch: Get-ALbuildProjectConfig swallows a malformed file and simply
    # returns nothing, which is the same silent "no configuration" that hid Banking's missing analyzers.
    if (Test-Path -LiteralPath $albuildJsonPath) {
        try { $null = ConvertFrom-BcJsonc -Text (Get-Content -LiteralPath $albuildJsonPath -Raw) }
        catch {
            throw ("'$albuildJsonPath' could not be parsed as JSON: $($_.Exception.Message)$([Environment]::NewLine)" +
                'Analyzer configuration could not be read; refusing to compile without analyzers. Fix the ' +
                'file, or pass the analyzers explicitly on the task input.')
        }
    }
    try {
        $cfg = Get-ALbuildProjectConfig -AppFolder $ProjectFolder -WorkspaceRoot $WorkspaceRoot
        if ($cfg) {
            if ($cfg.Analyzers) { $cfgAnalyzers = @($cfg.Analyzers) }
            if ($cfg.RuleSet) { $cfgRuleSet = [string] $cfg.RuleSet }
        }
    }
    catch {
        # The file parses (checked above); Get-ALbuildProjectConfig throwing here means something else -
        # a missing file, a schema it dislikes - and that is not this function's business.
        Write-Verbose "No albuild.json analyzer config at '$ProjectFolder': $($_.Exception.Message)"
    }

    # .vscode/settings.json (al.codeAnalyzers / al.ruleSetPath)
    $setAnalyzers = @(); $setRuleSet = ''
    $settingsPath = Join-Path $ProjectFolder '.vscode/settings.json'
    if (Test-Path -LiteralPath $settingsPath) {
        $raw = Get-Content -LiteralPath $settingsPath -Raw
        $settings = $null
        try { $settings = ConvertFrom-BcJsonc -Text $raw }
        catch {
            # The file belongs to VS Code and its authors keep writing JSONC in it, so the reader is
            # tolerant of comments and trailing commas. Whatever is STILL unparsable stops the build
            # rather than silently yielding no analyzers.
            throw ("'$settingsPath' could not be parsed as JSON: $($_.Exception.Message)$([Environment]::NewLine)" +
                'Analyzer configuration could not be read; refusing to compile without analyzers. Fix the ' +
                'file, or pass the analyzers explicitly on the task input.')
        }
        # Asked for, not assumed: under Set-StrictMode reading a property that is not there throws, and
        # a settings.json with analyzers but no ruleset is perfectly normal. This used to sit inside the
        # try/catch that swallowed everything - which is precisely how the real parse failure hid.
        $props = $settings.PSObject.Properties
        if ($props['al.codeAnalyzers'] -and $props['al.codeAnalyzers'].Value) {
            $setAnalyzers = @($props['al.codeAnalyzers'].Value)
        }
        if ($props['al.ruleSetPath'] -and $props['al.ruleSetPath'].Value) {
            $setRuleSet = [string] $props['al.ruleSetPath'].Value
        }
    }

    # Precedence: task input > albuild.json > settings.json.
    $analyzers = @(); $aSource = 'none'
    if (-not [string]::IsNullOrWhiteSpace($InputAnalyzers)) {
        $analyzers = @($InputAnalyzers -split '[;,]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        $aSource = 'task input'
    }
    elseif ($cfgAnalyzers.Count -gt 0) { $analyzers = $cfgAnalyzers; $aSource = 'albuild.json' }
    elseif ($setAnalyzers.Count -gt 0) { $analyzers = $setAnalyzers; $aSource = '.vscode/settings.json' }

    $rawRule = ''; $rSource = 'none'
    if (-not [string]::IsNullOrWhiteSpace($InputRuleSet)) { $rawRule = $InputRuleSet; $rSource = 'task input' }
    elseif ($cfgRuleSet) { $rawRule = $cfgRuleSet; $rSource = 'albuild.json' }
    elseif ($setRuleSet) { $rawRule = $setRuleSet; $rSource = '.vscode/settings.json' }

    $ruleset = ''
    if ($rawRule) {
        $ruleset = if ([System.IO.Path]::IsPathRooted($rawRule)) { $rawRule } else { Join-Path $ProjectFolder $rawRule }
    }

    # Test apps are never AppSource-published, so their analyzer findings (AppSourceCop/CodeCop/UICop) are
    # noise - and the V1 pipeline never ran analyzers at all, so migrated products would newly fail on them
    # (e.g. AS0015 'TranslationFile'). Disable ALL analyzers for a test app, detected by the AL test
    # framework (a Microsoft *assert*/*test* dependency, a manifest 'test' reference, or a 'Subtype = Test'
    # object). This wins over every config source. The ruleset is left as-is (it has no effect without
    # analyzers, but still governs the compiler's own diagnostics).
    if ($analyzers.Count -gt 0) {
        $manifest = $null
        $appJsonPath = Join-Path $ProjectFolder 'app.json'
        if (Test-Path -LiteralPath $appJsonPath) {
            try { $manifest = Get-Content -LiteralPath $appJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json }
            catch { $manifest = $null }
        }
        if ($manifest -and (Test-BcIsTestApp -Manifest $manifest -AppFolder $ProjectFolder)) {
            Write-ALbuildLog "Test app at '$ProjectFolder': disabling all code analyzers (was $($analyzers -join ', ') from $aSource)."
            $analyzers = @()
            $aSource = 'test app (analyzers disabled)'
        }
    }

    return [PSCustomObject]@{
        Analyzers       = @($analyzers)
        RuleSet         = $ruleset
        AnalyzersSource = $aSource
        RuleSetSource   = $rSource
    }
}