Modules/businessdev.ALbuild.Apps/Private/Get-BcAlExecutableLines.ps1
|
function Get-BcAlExecutableLines { <# .SYNOPSIS Identifies the executable statement lines of an AL source file -- the honest denominator for code coverage. .DESCRIPTION Business Central's coverage export only ever lists Covered/PartiallyCovered lines (verified against the platform: "AL Code Coverage Mgt.".SaveCoverageResults filters to those statuses), so the raw CSV alone always reads ~100%. To compute an honest line-coverage percentage ALbuild derives the *total* executable lines from the AL source instead. BC counts a source line as a "Code" line only when it carries an executable statement. This parser mirrors that: it walks each procedure/trigger body (the `begin`..matching `end` after a `procedure`/`trigger` header) and returns the 1-based source line numbers of statement lines, excluding object/property declarations, procedure & trigger signatures, `var` sections and their variable declarations, lone `begin`/`end`/`else`/`do`/`then` block keywords, comments and blank lines. Calibrated against a controlled BC28.2 container (a codeunit whose only statements sit on specific lines reported exactly by BC). One AL object per file is assumed (BC/AL convention), matching Get-BcAlObjectMap. .PARAMETER Path The .al file to analyse. .OUTPUTS int[] -- sorted, distinct 1-based source line numbers that carry an executable statement. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Returns a set of executable lines; the plural noun is intentional and clearer.')] [CmdletBinding()] [OutputType([int[]])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path ) $lines = @(Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue) if ($lines.Count -eq 0) { return @() } $exec = [System.Collections.Generic.List[int]]::new() $inBody = $false # inside a procedure/trigger body (after its opening begin) $depth = 0 # begin/end nesting within the current body (0 = not in a body) $pendingHeader = $false # saw a procedure/trigger header, waiting for its opening begin $inBlockComment = $false for ($i = 0; $i -lt $lines.Count; $i++) { $raw = $lines[$i] $line = $raw.Trim() # Strip block comments /* ... */ (whole-line handling is enough for AL statement detection). if ($inBlockComment) { if ($line -match '\*/') { $inBlockComment = $false; $line = ($line -replace '^.*?\*/', '').Trim() } else { continue } } if ($line -match '/\*' -and $line -notmatch '/\*.*\*/') { $inBlockComment = $true; $line = ($line -replace '/\*.*$', '').Trim() } if ($line -eq '') { continue } if ($line.StartsWith('//')) { continue } # Strip trailing line comment for keyword tests. $code = ($line -replace '//.*$', '').Trim() if ($code -eq '') { continue } $lower = $code.ToLowerInvariant() # A procedure/trigger header opens a (possibly var-prefixed) body. if ($lower -match '^(local\s+|internal\s+|protected\s+)*(procedure|trigger)\b') { $pendingHeader = $true continue } # The opening `begin` of a procedure/trigger body. if (-not $inBody -and $pendingHeader -and $lower -match '^begin\b') { $inBody = $true; $depth = 1; $pendingHeader = $false continue } if (-not $inBody) { # Outside any body: var sections, declarations, object/property lines -- never executable. continue } # Inside a procedure/trigger body. Track begin/end nesting and skip pure block keywords. # Count begins/ends on the line to maintain depth (a line may open and close blocks). $opens = ([regex]::Matches($lower, '(?<![A-Za-z0-9_])begin(?![A-Za-z0-9_])')).Count $closes = ([regex]::Matches($lower, '(?<![A-Za-z0-9_])end(?![A-Za-z0-9_])')).Count $isPureKeyword = $lower -match '^(begin|end|end;|else|do|then|var)$' -or $lower -match '^(end\s+else\b.*)$' # A statement line: inside the body, not a lone block keyword. `end else begin`-style lines are control flow. if (-not $isPureKeyword) { # Lines that are only opening/closing braces or block punctuation aren't statements. if ($lower -notmatch '^[{}();]+$') { $exec.Add($i + 1) | Out-Null } } $depth += $opens - $closes if ($depth -le 0) { $inBody = $false; $depth = 0 } } return @($exec | Sort-Object -Unique) } |