Modules/businessdev.ALbuild.Apps/Public/Get-BcCallGraphReport.ps1
|
function Get-BcCallGraphReport { <# .SYNOPSIS Analyses a call graph and writes its metrics, findings, impact and interactive HTML report. .DESCRIPTION Reads a call graph built by New-BcCallGraph and writes next to it (or to OutputFolder): analysis.json metrics, findings, impact and per-member source hashes (the next build's baseline) summary.md a Markdown summary for the build summary page report.html a self-contained interactive report: a layered map of the parts, drilled down into objects and members with callers and callees; the pages and what they reach; flows and the start tour; impact, findings and AL checks The parts come from callgraph.architecture.json in the workspace root (see New-BcCallGraphArchitecture), otherwise from the folder structure, so the map is readable without one. Metrics: objects, methods, triggers, calls (by kind), calls into dependencies, event publishers and subscribers, interface implementations, test methods, product members not reached by any [Test] method, calls of obsolete code from product code, and dead methods (non-public, called neither in the graph nor anywhere in the source text, nor as event subscriber, interface implementation or by name). With a baseline - the folder of an earlier call graph - each metric gets its change, and the impact lists the added, modified and removed members, their transitive callers and the tests that reach the change. Reporting never throws for a finding. The optional gate (MaxNotReachedPercent, MaxDeadMethods, FailOnObsoleteCalls) is evaluated and returned in Gate; acting on it is up to the caller. .PARAMETER GraphPath The call graph folder (or its graph.jsonl). .PARAMETER BaselinePath The folder of the call graph to compare with. .PARAMETER BaselineLabel How the report names the baseline, e.g. 'build 20261031.4 (refs/heads/dev)'. Default: its folder. .PARAMETER OutputFolder Where the report files are written. Default: the graph folder. .PARAMETER WorkspaceRoot Root that source paths are shown relative to. Default: recorded by New-BcCallGraph. .PARAMETER ArchitecturePath callgraph.architecture.json to apply. Default: the file in the workspace root when there is one; without it the parts are derived from the folders. .PARAMETER MaxNotReachedPercent Gate: fail when more than this percentage of product methods and triggers is not reached by tests. .PARAMETER MaxDeadMethods Gate: fail when more dead methods than this are found. .PARAMETER FailOnObsoleteCalls Gate: fail when product code calls obsolete code. .PARAMETER PipelineTab Mention the build's Call graph tab in the summary (pipeline runs). .EXAMPLE Get-BcCallGraphReport -GraphPath .albuild/callgraph -BaselinePath ./baseline/callgraph -MaxDeadMethods 0 .OUTPUTS PSCustomObject: Metrics, BaselineMetrics, Impact, Findings, Gate, AnalysisPath, SummaryPath, ReportPath. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string] $GraphPath, [string] $BaselinePath, [string] $BaselineLabel, [string] $OutputFolder, [string] $WorkspaceRoot, [string] $ArchitecturePath, [Nullable[double]] $MaxNotReachedPercent, [Nullable[int]] $MaxDeadMethods, [switch] $FailOnObsoleteCalls, [switch] $PipelineTab ) $graph = Resolve-BcCallGraphPath -Path $GraphPath -Require if (-not $OutputFolder) { $OutputFolder = $graph.Folder } New-Item -ItemType Directory -Force -Path $OutputFolder | Out-Null $analysis = Invoke-BcCallGraphAnalysis -GraphPath $graph.GraphPath -BaselinePath $BaselinePath -WorkspaceRoot $WorkspaceRoot -ArchitecturePath $ArchitecturePath if ($BaselineLabel -and $analysis['baseline']) { $analysis['baseline'] = $BaselineLabel } $m = $analysis['metrics'] $f = $analysis['findings'] # The gate: only what was asked for. $gate = $null if ($null -ne $MaxNotReachedPercent -or $null -ne $MaxDeadMethods -or $FailOnObsoleteCalls) { $failures = New-Object System.Collections.Generic.List[string] if ($null -ne $MaxNotReachedPercent) { if ($null -eq $m['notReachedPercent']) { Write-ALbuildLog -Level Warning 'Gate: no [Test] methods, so the test-reach limit cannot be checked.' } elseif ([double]$m['notReachedPercent'] -gt $MaxNotReachedPercent) { $failures.Add("$($m['notReachedPercent'])% of product methods are not reached by tests (limit $MaxNotReachedPercent%)") } } if ($null -ne $MaxDeadMethods -and [int]$m['deadMethods'] -gt $MaxDeadMethods) { $failures.Add("$($m['deadMethods']) dead methods (limit $MaxDeadMethods)") } if ($FailOnObsoleteCalls -and [int]$m['obsoleteCalls'] -gt 0) { $failures.Add("$($m['obsoleteCalls']) calls of obsolete code") } $gate = [PSCustomObject]@{ Passed = ($failures.Count -eq 0); Failures = $failures.ToArray() } } $analysisPath = Join-Path $OutputFolder 'analysis.json' $json = [ALbuild.CallGraph.Json]::Serialize($analysis) [System.IO.File]::WriteAllText($analysisPath, $json, (New-Object System.Text.UTF8Encoding($false))) $summaryPath = Join-Path $OutputFolder 'summary.md' $summary = Format-BcCallGraphSummary -Analysis $analysis -Gate $gate -PipelineTab:$PipelineTab [System.IO.File]::WriteAllText($summaryPath, $summary, (New-Object System.Text.UTF8Encoding($false))) # The report embeds its styles, the layout library and the data, so it works offline, as a download and inside the tab. $resourceRoot = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath 'Resources' $resources = Join-Path -Path $resourceRoot -ChildPath 'CallGraph' $template = [System.IO.File]::ReadAllText((Join-Path $resources 'report.html')) $baseCss = [System.IO.File]::ReadAllText((Join-Path $resourceRoot 'Shared/albuild-report.css')) $elk = [System.IO.File]::ReadAllText((Join-Path $resources 'vendor/elk.bundled.js')) # The per-member hashes are for the next build's comparison, not for the page. $view = New-Object 'System.Collections.Generic.Dictionary[string,object]' foreach ($key in $analysis.Keys) { if ($key -ne 'methodHashes') { $view[$key] = $analysis[$key] } } $html = $template.Replace('/*__BASE_CSS__*/', $baseCss.Replace('</style', '<\/style')).Replace('/*__ELK__*/', $elk.Replace('</script', '<\/script')).Replace('/*__DATA__*/', [ALbuild.CallGraph.Json]::Serialize($view)) $reportPath = Join-Path $OutputFolder 'report.html' [System.IO.File]::WriteAllText($reportPath, $html, (New-Object System.Text.UTF8Encoding($false))) $impact = $analysis['impact'] Write-ALbuildLog ("Call graph: {0} objects, {1} methods, {2} calls; not reached by tests: {3}; obsolete calls: {4}; dead methods: {5}{6}." -f $m['objects'], $m['methods'], $m['edges'], $(if ($null -eq $m['notReachedPercent']) { 'n/a' } else { "$($m['notReachedByTests']) ($($m['notReachedPercent'])%)" }), $m['obsoleteCalls'], $m['deadMethods'], $(if ($impact) { "; impact: $($impact['addedTotal']) added, $($impact['modifiedTotal']) modified, $($impact['removedTotal']) removed, $($impact['affectedTotal']) affected, $($impact['testsTotal']) tests" } else { '' })) if ($gate -and -not $gate.Passed) { Write-ALbuildLog -Level Warning "Call graph gate failed: $($gate.Failures -join '; ')." } [PSCustomObject]@{ Metrics = ConvertTo-BcCallGraphObject $m BaselineMetrics = ConvertTo-BcCallGraphObject $analysis['baselineMetrics'] Impact = if ($impact) { [PSCustomObject]@{ Added = $impact['addedTotal']; Modified = $impact['modifiedTotal']; Removed = $impact['removedTotal'] Affected = $impact['affectedTotal']; AffectedObjects = $impact['affectedObjects']; Tests = $impact['testsTotal'] Baseline = $analysis['baseline'] } } else { $null } Findings = [PSCustomObject]@{ NotReachedByTests = $f['untestedTotal']; ObsoleteCalls = $f['obsoleteCallsTotal']; DeadMethods = $f['deadTotal'] } Gate = $gate AnalysisPath = $analysisPath SummaryPath = $summaryPath ReportPath = $reportPath } } |