Modules/businessdev.ALbuild.Apps/Private/Format-BcCallGraphSummary.ps1
|
function Format-BcCallGraphSummary { <# .SYNOPSIS Renders the Markdown build summary of a call graph analysis. .PARAMETER Analysis The result of Invoke-BcCallGraphAnalysis. .PARAMETER Gate The gate result (Passed, Failures) or $null when no gate is configured. .PARAMETER MaxItems Rows per list in the summary; the HTML report has them all. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] $Analysis, $Gate, [int] $MaxItems = 10, # Point at the build's Call graph tab (pipeline runs only). [switch] $PipelineTab ) $m = $Analysis['metrics'] $b = $Analysis['baselineMetrics'] $f = $Analysis['findings'] $imp = $Analysis['impact'] $sb = New-Object System.Text.StringBuilder $own = @($Analysis['apps'] | Where-Object { $_['role'] -eq 'own' } | ForEach-Object { $_['name'] }) [void]$sb.AppendLine("## Call graph: $($own -join ', ')") [void]$sb.AppendLine() if ($Analysis['baseline']) { [void]$sb.AppendLine("Compared with $($Analysis['baseline'])."); [void]$sb.AppendLine() } $noSource = @($Analysis['apps'] | Where-Object { $_['role'] -eq 'dependency' -and -not $_['hasSource'] } | ForEach-Object { $_['name'] }) if ($noSource.Count -gt 0) { [void]$sb.AppendLine("> **Note:** no source for $($noSource -join ', '). Calls into these apps, and calls passing their types, are missing; dead code and test reach are less precise.") [void]$sb.AppendLine() } if ($Gate) { if ($Gate.Passed) { [void]$sb.AppendLine('**Gate:** passed.') } else { [void]$sb.AppendLine("**Gate failed:** $($Gate.Failures -join '; ')") } [void]$sb.AppendLine() } $rows = @( @('Objects', 'objects'), @('Methods', 'methods'), @('Triggers', 'triggers'), @('Calls', 'edges'), @('Test methods', 'testMethods'), @('Not reached by tests', 'notReachedByTests'), @('Not reached by tests (%)', 'notReachedPercent'), @('Obsolete calls', 'obsoleteCalls'), @('Dead methods', 'deadMethods'), @('Calls into dependencies', 'dependencyCalls'), @('Event publishers', 'eventPublishers'), @('Event subscribers', 'eventSubscribers'), @('Interface implementations', 'interfaceImplementations') ) if ($b) { [void]$sb.AppendLine('| Metric | This build | Baseline | Change |'); [void]$sb.AppendLine('|---|---:|---:|---:|') } else { [void]$sb.AppendLine('| Metric | This build |'); [void]$sb.AppendLine('|---|---:|') } foreach ($r in $rows) { $v = $m[$r[1]] $shown = if ($null -eq $v) { '-' } else { "$v" } if ($b) { $old = $b[$r[1]] $delta = '' if ($null -ne $v -and $null -ne $old) { $d = [math]::Round(([double]$v - [double]$old), 1) $delta = if ($d -gt 0) { "+$d" } elseif ($d -lt 0) { "$d" } else { '' } } [void]$sb.AppendLine("| $($r[0]) | $shown | $(if ($null -eq $old) { '-' } else { "$old" }) | $delta |") } else { [void]$sb.AppendLine("| $($r[0]) | $shown |") } } [void]$sb.AppendLine() $memberLine = { param($x) "- ``$($x['object']).$(if ($x['member']) { $x['member'] } else { $x['kind'] })``$(if ($x['path']) { " ($($x['path']):$($x['line']))" })" } $list = { param($title, $items, $total) if ($total -le 0) { return } [void]$sb.AppendLine("### $title ($total)") foreach ($x in @($items | Select-Object -First $MaxItems)) { [void]$sb.AppendLine((& $memberLine $x)) } if ($total -gt $MaxItems) { [void]$sb.AppendLine("- ... and $($total - $MaxItems) more in the HTML report") } [void]$sb.AppendLine() } if ($imp) { [void]$sb.AppendLine("### Impact") [void]$sb.AppendLine("$($imp['addedTotal']) added, $($imp['modifiedTotal']) modified, $($imp['removedTotal']) removed; $($imp['affectedTotal']) affected callers in $($imp['affectedObjects']) objects; $($imp['testsTotal']) tests reach the change.") [void]$sb.AppendLine() & $list 'Modified' $imp['modified'] $imp['modifiedTotal'] & $list 'Added' $imp['added'] $imp['addedTotal'] & $list 'Removed' $imp['removed'] $imp['removedTotal'] & $list 'Affected callers' $imp['affected'] $imp['affectedTotal'] & $list 'Tests reaching the change' $imp['tests'] $imp['testsTotal'] } if ($f['obsoleteCallsTotal'] -gt 0) { [void]$sb.AppendLine("### Obsolete calls ($($f['obsoleteCallsTotal']))") foreach ($x in @($f['obsoleteCalls'] | Select-Object -First $MaxItems)) { [void]$sb.AppendLine("- ``$($x['caller'])`` calls ``$($x['callee'])`` ($($x['path']):$($x['line']))") } if ($f['obsoleteCallsTotal'] -gt $MaxItems) { [void]$sb.AppendLine("- ... and $($f['obsoleteCallsTotal'] - $MaxItems) more in the HTML report") } [void]$sb.AppendLine() } & $list 'Dead methods' $f['dead'] $f['deadTotal'] & $list 'Not reached by tests' $f['untested'] $f['untestedTotal'] $arch = $Analysis['architecture'] if ($arch) { [void]$sb.AppendLine('### Architecture') $from = if ($arch['source'] -eq 'file') { 'callgraph.architecture.json' } else { 'the folders (no callgraph.architecture.json yet - `albuild graph describe` writes one)' } [void]$sb.AppendLine("$($arch['parts'].Count) parts from $from; $($arch['objects'] - $arch['unassigned']) of $($arch['objects']) objects assigned, $($arch['described']) described, $($arch['flows'].Count) flows.") foreach ($w in @($arch['warnings'] | Select-Object -First $MaxItems)) { [void]$sb.AppendLine("- $w") } [void]$sb.AppendLine() } $checks = $Analysis['checks'] if ($checks) { $elevated = @($checks['elevatedOwn']).Count $elevatedDep = @($checks['elevatedDependency']).Count if ($elevated + $elevatedDep + $checks['nonDebuggableTotal'] -gt 0) { [void]$sb.AppendLine('### AL checks (information)') if ($elevated) { [void]$sb.AppendLine("- $elevated own object(s) run with inherent permissions or entitlements: " + ((@($checks['elevatedOwn']) | Select-Object -First 5 | ForEach-Object { "``$($_['object'])``" }) -join ', ')) } if ($elevatedDep) { [void]$sb.AppendLine("- $elevatedDep Microsoft object(s) with inherent permissions or entitlements are called: " + ((@($checks['elevatedDependency']) | Select-Object -First 5 | ForEach-Object { "``$($_['object'])``" }) -join ', ')) } if ($checks['nonDebuggableTotal']) { [void]$sb.AppendLine("- $($checks['nonDebuggableTotal']) product method(s) are [NonDebuggable].") } [void]$sb.AppendLine() } } if ($PipelineTab) { [void]$sb.AppendLine('_The interactive graph is in the **Call graph** tab of this build._') } return $sb.ToString() } |