#!/usr/bin/env python3
"""Advisory-only cross-check between documented macros and their #define sites.
Macros have no C++ access-specifier concept, so the AST-based public/private test that
extract_api.py uses for classes doesn't transfer -- see tools/api_checker/POLICY.md's "Known
limitations" section. This script only checks one direction: that every macro documented under
docs/mkdocs/docs/api/macros/ still has a matching #define somewhere under include/nlohmann/,
catching stale or renamed doc pages. It does NOT check the converse (undocumented macros) --
no reliable signal exists for that direction given this codebase's conventions.
Never blocks CI -- always exits 0, even when it reports findings.
"""
import argparse
import glob
import os
import re
import sys
MACRO_TOKEN_RE = re.compile(r'\b([A-Z][A-Z0-9_]*)\b')
warnings = 0
def report(location: str, description: str):
global warnings
warnings += 1
print(f'{warnings:3}. {location}: {description}')
def extract_macro_names(md_path: str) -> list:
"""Extract macro name(s) from a doc page's H1 heading.
Most pages use a single-line markdown heading ('# JSON_ASSERT'). A few document a family of
related macros under one page using a multi-line HTML heading listing comma-separated names
(e.g. NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE, ..._WITH_DEFAULT, ..._ONLY_SERIALIZE). Handle
both: collect the heading text (markdown '# ...' line, or everything between
and
,
which may span multiple lines), then split on commas.
"""
with open(md_path) as f:
content = f.read()
heading_text = None
html_match = re.search(r'(.*?)
', content, re.DOTALL)
if html_match:
heading_text = html_match.group(1)
else:
for line in content.splitlines():
if line.startswith('# '):
heading_text = line[2:]
break
if not heading_text:
return []
names = []
for part in heading_text.split(','):
match = MACRO_TOKEN_RE.search(part)
if match:
names.append(match.group(1))
return names
def macro_is_referenced(macro_name: str, include_dir: str) -> bool:
"""Check whether macro_name is defined OR referenced (#ifdef/#ifndef/defined()) anywhere
under include_dir.
Some documented macros (e.g. JSON_NOEXCEPTION, JSON_THROW_USER) are user-supplied overrides:
the library only checks whether they're defined, it never #defines them itself. Requiring a
literal #define would make this advisory check permanently noisy for that whole category, so
presence of any reference is treated as "this macro still exists in the codebase."
"""
pattern = re.compile(
r'#\s*define\s+' + re.escape(macro_name) + r'\b'
r'|#\s*ifn?def\s+' + re.escape(macro_name) + r'\b'
r'|defined\s*\(\s*' + re.escape(macro_name) + r'\s*\)'
r'|defined\s+' + re.escape(macro_name) + r'\b'
)
for root, _dirs, files in os.walk(include_dir):
for fname in files:
if not fname.endswith('.hpp'):
continue
path = os.path.join(root, fname)
with open(path, encoding='utf-8', errors='ignore') as f:
if pattern.search(f.read()):
return True
return False
def check_macros(macros_dir: str, include_dir: str) -> bool:
md_files = sorted(glob.glob(os.path.join(macros_dir, '*.md')))
md_files = [f for f in md_files if os.path.basename(f) != 'index.md']
print(f"Checking {len(md_files)} documented macros against #define sites in {include_dir}...")
print(120 * "-")
for md_path in md_files:
macro_names = extract_macro_names(md_path)
if not macro_names:
report(md_path, "could not extract macro name(s) from H1 heading")
continue
for macro_name in macro_names:
if not macro_is_referenced(macro_name, include_dir):
report(md_path, f'documented macro "{macro_name}" is not defined or referenced under {include_dir}')
print(120 * "-")
if warnings > 0:
print(f"\nFound {warnings} stale/renamed macro doc pages (advisory only, not blocking)")
else:
print("\nAll documented macros have a matching #define ✓")
return warnings == 0
def main():
parser = argparse.ArgumentParser(description='Advisory cross-check of documented macros vs. #define sites')
parser.add_argument('--macros-dir', default='docs/mkdocs/docs/api/macros',
help='Directory containing macro documentation pages')
parser.add_argument('--include-dir', default='include/nlohmann',
help='Directory to search for #define sites')
args = parser.parse_args()
check_macros(args.macros_dir, args.include_dir)
# Advisory only -- never fail CI regardless of findings.
sys.exit(0)
if __name__ == '__main__':
main()