chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
320
AirPlan/docs/spec/AirPlan-ParaV2/plugins/airsdb/scripts/airsdb_cppcheck.py
Executable file
320
AirPlan/docs/spec/AirPlan-ParaV2/plugins/airsdb/scripts/airsdb_cppcheck.py
Executable file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run local cppcheck scans and maintain AirSDB reports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from airsdb_mode import ensure_files, read_env, setup_cppcheck, stamp
|
||||
|
||||
DEFAULT_ENABLE = "warning,style,performance,portability,information"
|
||||
DEFAULT_CHECK_LEVEL = "exhaustive"
|
||||
DEFAULT_EXCLUDES = [
|
||||
".git",
|
||||
".airsdb",
|
||||
"node_modules",
|
||||
"build",
|
||||
"cmake-build-debug",
|
||||
"cmake-build-release",
|
||||
"dist",
|
||||
"out",
|
||||
"vendor",
|
||||
"third_party",
|
||||
"external",
|
||||
]
|
||||
|
||||
|
||||
def iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def rel(project: Path, path: str) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
try:
|
||||
return str(Path(path).resolve().relative_to(project))
|
||||
except Exception:
|
||||
return path
|
||||
|
||||
|
||||
def find_project_file(project: Path, configured: str = "") -> Optional[Path]:
|
||||
candidates = []
|
||||
if configured:
|
||||
candidates.append(Path(configured))
|
||||
candidates.extend(
|
||||
[
|
||||
project / "compile_commands.json",
|
||||
project / "build" / "compile_commands.json",
|
||||
project / "cmake-build-debug" / "compile_commands.json",
|
||||
project / "cmake-build-release" / "compile_commands.json",
|
||||
]
|
||||
)
|
||||
for candidate in candidates:
|
||||
p = candidate if candidate.is_absolute() else project / candidate
|
||||
if p.exists():
|
||||
return p.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def build_args(
|
||||
project: Path,
|
||||
tool: str,
|
||||
project_file: str = "",
|
||||
target: str = "",
|
||||
enable: str = DEFAULT_ENABLE,
|
||||
check_level: str = DEFAULT_CHECK_LEVEL,
|
||||
jobs: int = 1,
|
||||
std: str = "",
|
||||
extra: Optional[List[str]] = None,
|
||||
) -> List[str]:
|
||||
build_dir = project / "AirPlan" / "state" / "airsdb" / "cppcheck-build"
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
args = [
|
||||
tool,
|
||||
f"--enable={enable}",
|
||||
f"--check-level={check_level}",
|
||||
"--inconclusive",
|
||||
"--inline-suppr",
|
||||
"--quiet",
|
||||
"--xml",
|
||||
"--xml-version=2",
|
||||
f"--cppcheck-build-dir={build_dir}",
|
||||
]
|
||||
if jobs > 1:
|
||||
args.append(f"-j{jobs}")
|
||||
if std:
|
||||
args.append(f"--std={std}")
|
||||
for item in extra or []:
|
||||
if item:
|
||||
args.extend(shlex.split(item, posix=(os.name != "nt")))
|
||||
|
||||
pf = find_project_file(project, project_file)
|
||||
if pf:
|
||||
args.append(f"--project={pf}")
|
||||
else:
|
||||
for ignored in DEFAULT_EXCLUDES:
|
||||
args.append(f"-i{ignored}")
|
||||
args.append(target or ".")
|
||||
return args
|
||||
|
||||
|
||||
def command_text(args: List[str]) -> str:
|
||||
if os.name == "nt":
|
||||
return subprocess.list2cmdline(args)
|
||||
return shlex.join(args)
|
||||
|
||||
|
||||
def extract_xml(text: str) -> str:
|
||||
start = text.find("<?xml")
|
||||
if start >= 0:
|
||||
return text[start:]
|
||||
start = text.find("<results")
|
||||
if start >= 0:
|
||||
return text[start:]
|
||||
return text
|
||||
|
||||
|
||||
def parse_cppcheck_xml(xml_text: str, project: Path) -> Dict[str, Any]:
|
||||
xml_text = extract_xml(xml_text)
|
||||
findings: List[Dict[str, Any]] = []
|
||||
counts: Counter[str] = Counter()
|
||||
if not xml_text.strip():
|
||||
return {"counts": {}, "findings": [], "parseError": "empty_xml"}
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except ET.ParseError as exc:
|
||||
return {"counts": {}, "findings": [], "parseError": str(exc)}
|
||||
for error in root.findall(".//error"):
|
||||
severity = error.attrib.get("severity", "unknown")
|
||||
counts[severity] += 1
|
||||
locations = []
|
||||
for loc in error.findall("location"):
|
||||
locations.append(
|
||||
{
|
||||
"file": rel(project, loc.attrib.get("file", "")),
|
||||
"line": loc.attrib.get("line", ""),
|
||||
"info": loc.attrib.get("info", ""),
|
||||
}
|
||||
)
|
||||
findings.append(
|
||||
{
|
||||
"severity": severity,
|
||||
"id": error.attrib.get("id", ""),
|
||||
"msg": error.attrib.get("msg", ""),
|
||||
"cwe": error.attrib.get("cwe", ""),
|
||||
"locations": locations,
|
||||
}
|
||||
)
|
||||
return {"counts": dict(counts), "findings": findings, "parseError": ""}
|
||||
|
||||
|
||||
def top_findings(findings: List[Dict[str, Any]], limit: int = 12) -> List[str]:
|
||||
rank = {"error": 0, "warning": 1, "performance": 2, "portability": 3, "style": 4, "information": 5}
|
||||
|
||||
def key(item: Dict[str, Any]) -> tuple[int, str]:
|
||||
return (rank.get(item.get("severity", ""), 9), item.get("id", ""))
|
||||
|
||||
lines = []
|
||||
for item in sorted(findings, key=key)[:limit]:
|
||||
loc = item.get("locations") or [{}]
|
||||
first = loc[0]
|
||||
where = first.get("file", "")
|
||||
if first.get("line"):
|
||||
where += f":{first['line']}"
|
||||
cwe = f" CWE-{item['cwe']}" if item.get("cwe") else ""
|
||||
lines.append(f"[{item.get('severity')}:{item.get('id')}{cwe}] {where} {item.get('msg')}".strip())
|
||||
return lines
|
||||
|
||||
|
||||
def append_staticanalysis(project: Path, data: Dict[str, Any]) -> None:
|
||||
ensure_files(project)
|
||||
path = project / "AirPlan" / "docs" / "staticanalysis.md"
|
||||
counts = data.get("counts") or {}
|
||||
counts_text = ", ".join(f"{k}={v}" for k, v in sorted(counts.items())) or "none"
|
||||
tops = data.get("topFindings") or []
|
||||
top_text = "\n".join(f" - {line}" for line in tops) if tops else " - none"
|
||||
result = "failed" if data.get("returnCode", 0) not in [0, None] else ("findings" if data.get("findingCount", 0) else "ok")
|
||||
entry = (
|
||||
f"\n### {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: AirSDB cppcheck\n\n"
|
||||
f"- Target: {data.get('target', 'local')}\n"
|
||||
f"- Tool: `{data.get('tool', '')}` {data.get('version', '')}\n"
|
||||
f"- Command: `{data.get('command', '')}`\n"
|
||||
f"- Result: {result}; returnCode={data.get('returnCode', '')}\n"
|
||||
f"- Counts: {counts_text}\n"
|
||||
f"- Reports: `{data.get('xmlReport', '')}`, `{data.get('jsonReport', '')}`\n"
|
||||
f"- Top findings:\n{top_text}\n"
|
||||
f"- AirDbg/AirDo handoff: {data.get('handoff', 'Review top findings before code-level repair or todo validation.')}\n"
|
||||
f"- Residual risk: cppcheck is static analysis and may miss runtime, integration, configuration, or dependency issues.\n"
|
||||
)
|
||||
path.open("a", encoding="utf-8", newline="\n").write(entry)
|
||||
|
||||
|
||||
def write_reports(project: Path, data: Dict[str, Any], xml_text: str, prefix: str = "cppcheck") -> Dict[str, Any]:
|
||||
reports = project / "AirPlan" / "state" / "airsdb" / "reports"
|
||||
reports.mkdir(parents=True, exist_ok=True)
|
||||
ts = stamp()
|
||||
xml_path = reports / f"{ts}-{prefix}.xml"
|
||||
json_path = reports / f"{ts}-{prefix}.json"
|
||||
xml_path.write_text(xml_text, encoding="utf-8", newline="\n")
|
||||
data["xmlReport"] = str(xml_path)
|
||||
data["jsonReport"] = str(json_path)
|
||||
json_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
|
||||
return data
|
||||
|
||||
|
||||
def run_scan(args: argparse.Namespace) -> int:
|
||||
project = Path(args.project).expanduser().resolve()
|
||||
ensure_files(project)
|
||||
setup = setup_cppcheck(project, auto=not args.no_auto_configure)
|
||||
if setup.get("status") != "ok":
|
||||
print(f"airsdb_scan_status=blocked")
|
||||
for key, value in setup.items():
|
||||
print(f"{key}={value}")
|
||||
return 2
|
||||
tool = setup["tool"]
|
||||
env = read_env(project / "AirPlan" / "state" / "airsdb" / "tool.env")
|
||||
extra = list(args.extra or [])
|
||||
if env.get("AIRSDB_CPPCHECK_OPTIONS"):
|
||||
extra.append(env["AIRSDB_CPPCHECK_OPTIONS"])
|
||||
cmd = build_args(project, tool, args.project_file, args.target, args.enable, args.check_level, args.jobs, args.std, extra)
|
||||
if args.action == "command":
|
||||
print("airsdb_command_status=ok")
|
||||
print(f"command={command_text(cmd)}")
|
||||
return 0
|
||||
try:
|
||||
run = subprocess.run(cmd, cwd=project, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=args.timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
data = {
|
||||
"target": "local",
|
||||
"status": "timeout",
|
||||
"tool": tool,
|
||||
"version": setup.get("version", ""),
|
||||
"checkLevel": args.check_level,
|
||||
"command": command_text(cmd),
|
||||
"returnCode": "timeout",
|
||||
"stdout": exc.stdout or "",
|
||||
"stderr": exc.stderr or "",
|
||||
"capturedAt": iso(),
|
||||
"counts": {},
|
||||
"findings": [],
|
||||
"findingCount": 0,
|
||||
"topFindings": [],
|
||||
}
|
||||
data = write_reports(project, data, str(exc.stderr or ""), "cppcheck-timeout")
|
||||
append_staticanalysis(project, data)
|
||||
print("airsdb_scan_status=timeout")
|
||||
print(f"jsonReport={data['jsonReport']}")
|
||||
return 1
|
||||
|
||||
xml_text = extract_xml(run.stderr or "")
|
||||
parsed = parse_cppcheck_xml(xml_text, project)
|
||||
findings = parsed.get("findings", [])
|
||||
data = {
|
||||
"target": "local",
|
||||
"status": "ok" if run.returncode == 0 else "failed",
|
||||
"tool": tool,
|
||||
"version": setup.get("version", ""),
|
||||
"checkLevel": args.check_level,
|
||||
"command": command_text(cmd),
|
||||
"returnCode": run.returncode,
|
||||
"stdout": run.stdout.strip(),
|
||||
"stderrPreview": (run.stderr or "")[:2000],
|
||||
"capturedAt": iso(),
|
||||
"counts": parsed.get("counts", {}),
|
||||
"findings": findings,
|
||||
"findingCount": len(findings),
|
||||
"topFindings": top_findings(findings, args.limit),
|
||||
"parseError": parsed.get("parseError", ""),
|
||||
}
|
||||
data = write_reports(project, data, xml_text, "cppcheck")
|
||||
append_staticanalysis(project, data)
|
||||
print(f"airsdb_scan_status={data['status']}")
|
||||
print(f"findingCount={data['findingCount']}")
|
||||
print(f"xmlReport={data['xmlReport']}")
|
||||
print(f"jsonReport={data['jsonReport']}")
|
||||
return 0 if run.returncode == 0 else 1
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run local AirSDB cppcheck analysis.")
|
||||
parser.add_argument("--project", default=".")
|
||||
parser.add_argument("--action", choices=["setup", "status", "command", "scan"], default="scan")
|
||||
parser.add_argument("--project-file", default="")
|
||||
parser.add_argument("--target", default="")
|
||||
parser.add_argument("--enable", default=DEFAULT_ENABLE)
|
||||
parser.add_argument("--check-level", default=DEFAULT_CHECK_LEVEL)
|
||||
parser.add_argument("--std", default="")
|
||||
parser.add_argument("--jobs", type=int, default=1)
|
||||
parser.add_argument("--timeout", type=int, default=900)
|
||||
parser.add_argument("--limit", type=int, default=12)
|
||||
parser.add_argument("--extra", action="append", default=[])
|
||||
parser.add_argument("--no-auto-configure", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
project = Path(args.project).expanduser().resolve()
|
||||
if args.action in ["setup", "status"]:
|
||||
ensure_files(project)
|
||||
setup = setup_cppcheck(project, auto=(args.action == "setup" and not args.no_auto_configure))
|
||||
print(f"airsdb_cppcheck_status={setup.get('status', 'unknown')}")
|
||||
for key, value in setup.items():
|
||||
if key != "status":
|
||||
print(f"{key}={value}")
|
||||
raise SystemExit(0 if setup.get("status") == "ok" else 2)
|
||||
raise SystemExit(run_scan(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user