mirror of
https://github.com/nlohmann/json.git
synced 2026-07-12 05:25:09 +00:00
f23b3c63a2
Adds tools/api_checker/: extract_api.py derives the public API surface directly from the libclang AST (independent of documentation status), check_docs.py flags public entries missing @sa links (and @sa on non-public ones), diff_api.py does an overload-aware breaking/feature diff between two refs, check_macros.py cross- checks documented macros against #define sites, and snapshot_release.py backfills immutable per-release surface snapshots into tools/api_checker/history/ (v3.1.0 through v3.12.0) so diff_api.py can compare releases without live extraction. POLICY.md documents what counts as public API and what stability is guaranteed. Running this tooling against the current tree found and fixed a real documentation backlog: ~25 new API doc pages (ordered_map's methods, json_sax's ctor/dtor/ operator=, byte_container_with_subtype's comparison operators, several orphaned type aliases), each with a compiled and output-verified example, plus missing @sa comments and stale/incorrect Version History entries on several existing pages (found by diffing consecutive release pairs and checking whether the resulting change was actually reflected in the target page's history section). Also adds docs/home/api_changes.md, a per-release, per-function reference of public API changes (v3.1.0 through v3.12.0) generated from the history/ snapshots, complementing (not replacing) the existing release notes. Along the way, found and fixed several extractor bugs by testing against real release tags rather than trusting the algorithm in isolation -- most notably an identity-key scheme based on libclang's USR that encoded the enclosing class template's own arity, and a since-renamed ABI inline-namespace pattern (json_v3_11_0 vs. today's json_abi_v3_11_2) that neither of two earlier regex attempts stripped correctly. Both are documented in extract_api.py's docstrings and tools/api_checker/history/README.md so the failure mode doesn't recur silently. .github/workflows/check_api_docs.yml runs extract_api.py + check_docs.py in CI, advisory-only for now (documented backlog may not be at zero for entities this PR didn't touch), plus a blocking drift check on the committed tools/api_checker/api_surface.json. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me>
107 lines
4.3 KiB
Python
Executable File
107 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Capture immutable, per-release API surface records into tools/api_checker/history/.
|
|
|
|
These are the durable, committed counterpart to diff_api.py's live git-archive-and-extract path:
|
|
once a release is tagged, run this once to capture tools/api_checker/history/<tag>.json, commit
|
|
it, and future diffs against that tag hit the fast, no-libclang-needed stored-file path in
|
|
diff_api.py automatically. See tools/api_checker/README.md's "Workflow: Release Checklist" and
|
|
POLICY.md for the full policy (manual step, not CI-automated; files are immutable once committed
|
|
-- regenerate only via --force, and only as a deliberate, reviewed choice).
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
HISTORY_DIR = os.path.join(SCRIPT_DIR, 'history')
|
|
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
from diff_api import extract_surface_for_ref # noqa: E402
|
|
|
|
|
|
def discover_v3_tags() -> list:
|
|
"""List every v3.* git tag, sorted by dotted version (not lexicographically -- v3.9.0 must
|
|
sort before v3.10.0)."""
|
|
result = subprocess.run(['git', 'tag', '--list', 'v3.*'], capture_output=True, text=True, check=True)
|
|
tags = [t for t in result.stdout.splitlines() if t.strip()]
|
|
|
|
def version_key(tag):
|
|
parts = tag.lstrip('v').replace('-', '.').split('.')
|
|
return tuple(int(p) if p.isdigit() else p for p in parts)
|
|
|
|
return sorted(tags, key=version_key)
|
|
|
|
|
|
def snapshot_one(ref: str, output_dir: str, force: bool, header: str, include: str) -> tuple:
|
|
"""Capture one ref's surface. Returns (ref, success, message)."""
|
|
output_path = os.path.join(output_dir, f"{ref}.json")
|
|
if os.path.exists(output_path) and not force:
|
|
return ref, True, f"skipped (already exists at {output_path}; use --force to overwrite)"
|
|
|
|
try:
|
|
surface = extract_surface_for_ref(ref, header, include)
|
|
except SystemExit:
|
|
return ref, False, "extraction failed (see output above)"
|
|
except Exception as e: # noqa: BLE001 -- a batch backfill must not die on one bad tag
|
|
return ref, False, f"unexpected error: {e}"
|
|
|
|
surface['meta']['generated_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
surface['meta']['generator'] = 'tools/api_checker/extract_api.py'
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
with open(output_path, 'w') as f:
|
|
json.dump(surface, f, indent=2, sort_keys=True)
|
|
f.write('\n')
|
|
|
|
return ref, True, f"{len(surface['public_api'])} entries -> {output_path}"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Capture per-release API surface snapshots into tools/api_checker/history/')
|
|
parser.add_argument('--ref', action='append', default=[],
|
|
help='Git ref (tag) to snapshot; repeatable')
|
|
parser.add_argument('--all-tags', action='store_true',
|
|
help='Snapshot every v3.* tag (existing history files are skipped unless --force)')
|
|
parser.add_argument('--output-dir', default=HISTORY_DIR,
|
|
help='Directory to write history files into (default: tools/api_checker/history/)')
|
|
parser.add_argument('--force', action='store_true',
|
|
help='Overwrite an existing history file (history files are immutable by '
|
|
'convention -- only pass this for a deliberate, reviewed regeneration)')
|
|
parser.add_argument('--header', default='include/nlohmann/json.hpp')
|
|
parser.add_argument('--include', default='include')
|
|
|
|
args = parser.parse_args()
|
|
|
|
refs = list(args.ref)
|
|
if args.all_tags:
|
|
refs = discover_v3_tags()
|
|
|
|
if not refs:
|
|
parser.error('specify at least one --ref, or --all-tags')
|
|
|
|
print(f"Snapshotting {len(refs)} ref(s)...")
|
|
results = []
|
|
for ref in refs:
|
|
print(f"\n=== {ref} ===")
|
|
result = snapshot_one(ref, args.output_dir, args.force, args.header, args.include)
|
|
print(result[2])
|
|
results.append(result)
|
|
|
|
failures = [(ref, msg) for ref, ok, msg in results if not ok]
|
|
print("\n" + "=" * 70)
|
|
print(f"Done: {len(results) - len(failures)} succeeded, {len(failures)} failed")
|
|
if failures:
|
|
print("\nFailures (review and record accepted gaps in tools/api_checker/history/README.md):")
|
|
for ref, msg in failures:
|
|
print(f" {ref}: {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|