moonbit-pathfinding/scripts/doc_api_guard.ps1

312 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env pwsh
# ──────────────────────────────────────────────────────────────────────
# doc_api_guard.ps1 — 任务 29.4 · 文档完整性硬门禁Doc_Api_Guard, R21.3/21.4
# ──────────────────────────────────────────────────────────────────────
# 用途(对应 design.md §文档行数门禁 与 requirements.md Requirement 21.3/21.4
# 在既有 audit_doc.ps1 的基础上扩展为「R21.3 文档行数门禁」:
# 1. 扫描全部 src/**/*.mbt排除 *_test.mbt / *_wbtest.mbt识别每个
# Pub_Api以 `pub` 修饰的函数 / 类型 / 方法 / trait / impl 等)。
# 2. 统计该 API 紧邻上方的 `///` Doc_Comment 中的「非空注释行」数量。
# 非空注释行定义R21.3):去除首尾空白后长度 > 0 的 `///` 注释行;
# `///|` 是 MoonBit 的块分隔符,不属于文档内容,亦标记文档块的上界。
# 3. IF 某 Pub_Api 的非空注释行 < MinLines默认 5以「API 标识 +
# 实际非空注释行数」报告并以非零退出状态使门禁失败R21.4)。
#
# 与 audit_doc.ps1 的关系:
# audit_doc.ps1 只校验「是否存在 Doc_Comment」≥1 行即通过)。本脚本进一步
# 要求「非空注释行 ≥ 5」是其严格超集audit_doc 保留用于快速巡检。
#
# 退出码约定:
# 0 —— 全部 Pub_Api 的非空注释行 ≥ 阈值。
# 1 —— 至少一个 Pub_Api 的非空注释行 < 阈值R21.4),或报告写出失败。
# 2 —— 前置条件不满足(如 src 目录不存在)。
#
# 兼容性:面向 pwshPowerShell 7+)编写,同时兼容 Windows PowerShell 5.1
# (不使用 null 合并 / 三元等 7+ 专属语法),跨平台运行。
# ──────────────────────────────────────────────────────────────────────
[CmdletBinding()]
param(
# 每个 Pub_Api 要求的最小非空注释行数。<0 时回退到 DOC_MIN_LINES 环境
# 变量,仍未提供则取 R21.3 规定的 5。
[int]$MinLines = -1,
# 被扫描的源码根目录(相对脚本父目录或绝对路径)。
[string]$SrcDir = "src",
# 报告产物输出目录。
[string]$OutDir = "docs/verification",
# 控制台最多打印多少条不达标 API完整清单始终写入产物
[int]$MaxListed = 200,
# 排除模式PowerShell -like 通配,匹配规范化后的相对路径与文件名)。
[string[]]$ExcludeGlobs = @('*_test.mbt', '*_wbtest.mbt')
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
# 文本产物统一使用「无 BOM」的 UTF-8确保 JSON 可被严格解析器直接读取。
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
# ───────────────────────── 阈值解析(兼容 5.1,不用 null 合并) ─────────────────────────
if ($MinLines -lt 0) {
if ($env:DOC_MIN_LINES) {
$MinLines = [int]$env:DOC_MIN_LINES
} else {
$MinLines = 5
}
}
Write-Host "=== Doc API Guard (task 29.4 · R21.3/21.4) ===" -ForegroundColor Cyan
Write-Host ("Requirement : 每个 Pub_Api 的非空 Doc_Comment 行 >= {0}" -f $MinLines)
Write-Host ("Exclude : {0}" -f ($ExcludeGlobs -join ', '))
# ───────────────────────── 路径解析与前置检查 ─────────────────────────
$root = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$srcRoot = if ([IO.Path]::IsPathRooted($SrcDir)) { $SrcDir } else { Join-Path $root $SrcDir }
$outRoot = if ([IO.Path]::IsPathRooted($OutDir)) { $OutDir } else { Join-Path $root $OutDir }
if (-not (Test-Path -LiteralPath $srcRoot)) {
Write-Host "::error::源码目录不存在:$srcRoot" -ForegroundColor Red
exit 2
}
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$generatedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# ───────────────────── 辅助:相对路径规范化(统一用 / ─────────────────────
function Get-RelPath {
param([string]$FullPath)
$rel = $FullPath
if ($FullPath.StartsWith($root)) {
$rel = $FullPath.Substring($root.Length).TrimStart('\', '/')
}
return ($rel -replace '\\', '/')
}
function Test-Excluded {
param([string]$RelPath, [string]$FileName, [string[]]$Globs)
foreach ($g in $Globs) {
if ($RelPath -like $g) { return $true }
if ($FileName -like $g) { return $true }
}
return $false
}
# ───────────────────── 辅助:从 pub 声明行提取 API 标识R21.4 报告用) ─────────────────────
# 尽力解析出可读的 API 标识,如 `fn DSU::new`、`struct DSU`、
# `impl Weight for Int::zero`、`trait Weight`。无法解析时回退到裁剪后的原行。
function Get-ApiIdentifier {
param([string]$Line)
$t = $Line.Trim()
$head = [regex]::Match($t, '^pub(?:\((?:all|open|readonly)\))?\s+(fn|let|const|struct|enum|trait|impl|type|typealias)\b')
if (-not $head.Success) {
# 退化:截断原始行作为标识。
if ($t.Length -gt 80) { return $t.Substring(0, 80) + ' …' }
return $t
}
$kind = $head.Groups[1].Value
$rest = $t.Substring($head.Length)
switch ($kind) {
'fn' {
# 可选泛型 [..],随后是 名称 或 Type::method。
$m = [regex]::Match($rest, '^\s*(?:\[[^\]]*\]\s*)?([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)?)')
if ($m.Success) { return "fn " + $m.Groups[1].Value }
}
'impl' {
# 形如 `Trait for Type with fn method(...)`,尽量带上方法名。
$mm = [regex]::Match($rest, '^\s*(.+?)\s+with\s+fn\s+([A-Za-z_][A-Za-z0-9_]*)')
if ($mm.Success) { return "impl " + $mm.Groups[1].Value.Trim() + "::" + $mm.Groups[2].Value }
$m2 = [regex]::Match($rest, '^\s*(.+?)\s*(\{|$)')
if ($m2.Success) { return "impl " + $m2.Groups[1].Value.Trim() }
}
default {
# struct / enum / trait / type / typealias / let / const取首个标识符。
$m = [regex]::Match($rest, '^\s*([A-Za-z_][A-Za-z0-9_]*)')
if ($m.Success) { return "$kind " + $m.Groups[1].Value }
}
}
if ($t.Length -gt 80) { return $t.Substring(0, 80) + ' …' }
return $t
}
# ───────────────────── 辅助:统计某 pub 行上方 Doc_Comment 的非空注释行数R21.3 ─────────────────────
# 从 pub 行上一行起向上扫描契约:
# - 空白行 → 文档块结束(停止)
# - `///|` 分隔符行 → 文档块上界(停止,不计入)
# - `///` 文档行 → 去掉 `///` 前缀并 Trim长度>0 计为非空注释行
# - `#...` 属性行 → 跳过继续向上doc 可能在属性之上)
# - 其他非注释代码行 → 停止
function Measure-DocLines {
param([string[]]$Lines, [int]$PubIndex)
$nonEmpty = 0
$totalDoc = 0
for ($j = $PubIndex - 1; $j -ge 0; $j--) {
$prev = $Lines[$j]
if ($prev -match '^\s*$') { break }
if ($prev -match '^\s*///\|') { break }
if ($prev -match '^\s*///') {
$totalDoc++
$content = ($prev -replace '^\s*///', '').Trim()
if ($content.Length -gt 0) { $nonEmpty++ }
continue
}
if ($prev -match '^\s*#') { continue }
break
}
return [pscustomobject]@{ NonEmpty = $nonEmpty; TotalDoc = $totalDoc }
}
# ───────────────────── 1. 扫描全部源文件,识别 Pub_Api 并统计文档行 ─────────────────────
# Pub_Api 声明匹配:行首(允许缩进)以 pub 修饰,后接受支持的声明关键字。
# `(fn|...)` 后无强制空格,以兼容 `pub fn[N : Eq] Type::method(...)` 写法。
$pubRegex = '^\s*pub(\((all|open|readonly)\))?\s+(fn|let|const|struct|enum|trait|impl|type|typealias)\b'
$apis = New-Object System.Collections.Generic.List[object]
$violations = New-Object System.Collections.Generic.List[object]
$scannedFiles = 0
$totalApis = 0
$mbtFiles = Get-ChildItem -Path $srcRoot -Recurse -Filter '*.mbt' -File | Sort-Object FullName
foreach ($f in $mbtFiles) {
$rel = Get-RelPath -FullPath $f.FullName
if (Test-Excluded -RelPath $rel -FileName $f.Name -Globs $ExcludeGlobs) { continue }
$scannedFiles++
$lines = Get-Content -LiteralPath $f.FullName
if ($null -eq $lines) { continue }
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -match $pubRegex) {
$totalApis++
$apiId = Get-ApiIdentifier -Line $line
$doc = Measure-DocLines -Lines $lines -PubIndex $i
$record = [pscustomobject]@{
file = $rel
line = $i + 1
api = $apiId
doc_lines = $doc.NonEmpty
total_doc = $doc.TotalDoc
location = "{0}:{1}" -f $rel, ($i + 1)
}
$apis.Add($record)
if ($doc.NonEmpty -lt $MinLines) {
$violations.Add($record)
}
}
}
}
# ───────────────────── 2. 组装产物MD + JSON ─────────────────────
$passed = $violations.Count -eq 0
$status = if ($passed) { "passed" } else { "failed" }
# 不达标项按「非空注释行升序、其次位置」排序,便于优先修补文档最缺的 API。
$sortedViolations = $violations | Sort-Object doc_lines, location
$artifact = [ordered]@{
schema = "moonbit-pathfinding.doc-api-guard.v1"
generated_at = $generatedAt
generated_by = "scripts/doc_api_guard.ps1"
min_lines = $MinLines
src_dir = (Get-RelPath -FullPath $srcRoot)
exclude_globs = $ExcludeGlobs
scanned_files = $scannedFiles
total_apis = $totalApis
violation_count = $violations.Count
status = $status
violations = @($sortedViolations | ForEach-Object {
[ordered]@{
api = $_.api
location = $_.location
doc_lines = $_.doc_lines
}
})
}
function Write-DocGuardArtifacts {
param([object]$Artifact, [string[]]$MarkdownLines)
try {
New-Item -ItemType Directory -Force -Path $outRoot | Out-Null
$json = $Artifact | ConvertTo-Json -Depth 12
$jsonPath = Join-Path $outRoot ("doc-api-guard-$timestamp.json")
$latestJson = Join-Path $outRoot "latest-doc-api-guard.json"
$mdPath = Join-Path $outRoot "latest-doc-api-guard.md"
[IO.File]::WriteAllText($jsonPath, $json + "`n", $Utf8NoBom)
[IO.File]::WriteAllText($latestJson, $json + "`n", $Utf8NoBom)
[IO.File]::WriteAllText($mdPath, ($MarkdownLines -join "`n") + "`n", $Utf8NoBom)
Write-Host "=== DOC API GUARD ARTIFACTS ===" -ForegroundColor Cyan
Write-Host $jsonPath
Write-Host $latestJson
Write-Host $mdPath
return $true
} catch {
Write-Host "::error::文档门禁报告写出失败: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
$md = New-Object System.Collections.Generic.List[string]
$md.Add("# Doc API Guard Report")
$md.Add("")
$md.Add("- Generated at: $generatedAt")
$md.Add("- Script: scripts/doc_api_guard.ps1")
$md.Add("- Min non-empty doc lines per Pub_Api: $MinLines")
$md.Add("- Scanned source dir: " + (Get-RelPath -FullPath $srcRoot))
$md.Add("- Exclude globs: " + ($ExcludeGlobs -join ', '))
$md.Add("- Scanned files: $scannedFiles")
$md.Add("- Total Pub_Api: $totalApis")
$md.Add("- Violations (< $MinLines lines): $($violations.Count)")
$md.Add("- Status: $($status.ToUpper())")
$md.Add("")
$md.Add("## 文档不达标的 Pub_ApiAPI 标识 + 实际非空注释行数 · R21.4")
$md.Add("")
if ($violations.Count -eq 0) {
$md.Add("(全部 Pub_Api 文档行数达标)")
} else {
$md.Add("| API 标识 | 位置 | 非空注释行 | 缺口 |")
$md.Add("| --- | --- | ---: | ---: |")
foreach ($v in $sortedViolations) {
$md.Add(("| {0} | {1} | {2} | {3} |" -f $v.api, $v.location, $v.doc_lines, ($MinLines - $v.doc_lines)))
}
}
$md.Add("")
$wrote = Write-DocGuardArtifacts -Artifact $artifact -MarkdownLines $md.ToArray()
# ───────────────────── 3. 控制台摘要与门禁退出语义 ─────────────────────
Write-Host ""
Write-Host "--- Summary ---" -ForegroundColor Cyan
Write-Host ("Scanned files : {0}" -f $scannedFiles)
Write-Host ("Total Pub_Api : {0}" -f $totalApis)
Write-Host ("Violations : {0} (< {1} non-empty doc lines)" -f $violations.Count, $MinLines)
if (-not $wrote) {
# 报告写出失败按门禁失败处理。
exit 1
}
if (-not $passed) {
# R21.4以「API 标识 + 实际非空注释行数」逐条报告并非零退出。
Write-Host ("::error::Doc API gate FAILED: {0} 个 Pub_Api 的非空注释行 < {1}" -f $violations.Count, $MinLines) -ForegroundColor Red
$shown = [Math]::Min($MaxListed, $sortedViolations.Count)
for ($i = 0; $i -lt $shown; $i++) {
$v = $sortedViolations[$i]
Write-Host (" {0} [{1}] doc_lines={2} (需 >= {3})" -f $v.api, $v.location, $v.doc_lines, $MinLines)
}
if ($sortedViolations.Count -gt $shown) {
Write-Host (" … 其余 {0} 项见报告产物 latest-doc-api-guard.md" -f ($sortedViolations.Count - $shown)) -ForegroundColor Yellow
}
exit 1
}
Write-Host ("::notice::Doc API gate PASSED: 全部 {0} 个 Pub_Api 的非空注释行 >= {1}" -f $totalApis, $MinLines) -ForegroundColor Green
exit 0