canary/tools/check_lua_api_quality.py
Eduardo Dantas dd1a95c157
feat: add Lua API documentation and doc generator (#3771)
Add Canary's official Lua API documentation generator and keep the generated Lua API reference synchronized with the C++ Lua binding surface.

Main changes:
- Add LuaApiDocGenerator and LuaBindingScanner to discover Lua classes, methods, globals, constants, parameters, returns, fields, overloads, aliases, source files, and class inheritance from the C++ binding layer.
- Generate docs/lua-api/lua_api.d.lua for Lua Language Server and VSCode IntelliSense.
- Generate docs/lua-api/lua_api.md for human-readable API documentation.
- Generate docs/lua-api/lua_api.json for structured tooling and CI metadata.
- Generate docs/lua-api/lua_api_quality_baseline.json for weak-signature regression tracking.
- Integrate documentation generation into the startup after loadConfigLua, controlled by generateLuaApiDocs and luaApiDocsOutputDirectory.
- Add --generate-lua-api-docs-only so CI can regenerate docs without starting the game server, loading maps, connecting to the database, or running shutdown/save paths.

Generator behavior:
- Force documentation generation in docgen-only mode so CI sync checks cannot silently become no-ops.
- Warn instead of crashing the startup when doc generation fails.
- Write generated files atomically.
- Skip unchanged writes.
- Use deterministic ordering and normalized file endings.
- Keep source paths relative and portable.
- Normalize inferred C++ types into Lua and LuaLS-friendly types.
- Avoid exposing raw C++ types in generated stubs.

LuaLS support:
- Emit LuaLS-compatible annotations, including meta, aliases, classes, fields, overloads, params, returns, inheritance, callable constructors, typed arrays, and operators for supported metamethods.
- Avoid exposing internal metamethods such as __eq, __add, and __gc as normal public Lua methods.
- Add explicit signature overlays with docblocks for APIs where automatic inference is not precise enough.
- Add overlays for high-impact APIs such as Player, Game, Result, db async calls, Actions, TalkActions, Spells, Weapons, MoveEvents, GlobalEvents, CreatureEvents, NpcType, Position, and NetworkMessage.

Editor and documentation:
- Add .luarc.json so LuaLS loads docs/lua-api by default.
- Raise LuaLS preload limits for lua_api.d.lua.
- Exclude generated build, cache, Visual Studio, and vcpkg directories from LuaLS workspace indexing.
- Add tools/setup_vscode_lua_api.ps1 to configure VSCode and LuaLS locally.
- Add docs/systems/lua-api-docgen.md explaining configuration, binding documentation, docblocks, quality baselines, and CI checks.
- Mention the generated Lua API docs from the README and systems index.
- Add Visual Studio project and CMake integration for the generator.

CI and regression protection:
- Add CI sync validation that runs --generate-lua-api-docs-only and checks docs/lua-api for diffs.
- Add tools/check_lua_api_quality.py to compare weak-signature metrics against the committed baseline.
- Add tools/check_lua_api_binding_docs.py to require explicit docblocks when new bindings would generate weak signatures.
- Exclude generated Lua API outputs from Sonar duplication/noise while keeping the generator and tooling checked.

Validation:
- Verified docs/lua-api/lua_api.json parses successfully.
- Verified luac -p passes for docs/lua-api/lua_api.d.lua.
- Verified Lua API binding docs and quality checks pass.
- Verified tools/setup_vscode_lua_api.ps1 -WhatIf succeeds.
- Verified VSCode and LuaLS resolve generated Canary classes and methods from lua_api.d.lua.
- Checked generated docs for stale root-level references, temporary paths, local machine paths, and obvious raw C++ types.

This gives Canary a repeatable source-of-truth pipeline for Lua API documentation, editor IntelliSense, and CI enforcement while keeping the generated docs synchronized with the C++ binding layer.
2026-05-25 14:39:39 -03:00

120 lines
3.6 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import re
import sys
from pathlib import Path
DOC_PATH = Path("docs/lua-api/lua_api.json")
BASELINE_PATH = Path("docs/lua-api/lua_api_quality_baseline.json")
METRIC_KEYS = (
"param_any",
"param_argn",
"param_vararg",
"return_any",
"return_plain_table",
)
PARAMETER_METRICS = (
("param_any", re.compile(r":\s*any\b")),
("param_argn", re.compile(r"\barg\d+\b")),
)
RETURN_METRICS = {
"any": "return_any",
"table": "return_plain_table",
}
def iter_functions(data):
for class_name, methods in data.get("classes", {}).items():
for method in methods:
yield f"{class_name}.{method.get('name', '')}", method
for function in data.get("globals", []):
yield function.get("name", ""), function
def collect_metrics(data):
metrics = dict.fromkeys(METRIC_KEYS, 0)
examples = {key: [] for key in METRIC_KEYS}
for function_name, function in iter_functions(data):
collect_parameter_metrics(function_name, function.get("params", []), metrics, examples)
collect_return_metrics(function_name, function.get("return", ""), metrics, examples)
return metrics, examples
def collect_parameter_metrics(function_name, parameters, metrics, examples):
for parameter in parameters:
for metric_key, pattern in PARAMETER_METRICS:
if pattern.search(parameter):
increment_metric(metric_key, metrics, examples, function_name, parameter)
if parameter.startswith("..."):
increment_metric("param_vararg", metrics, examples, function_name, parameter)
def collect_return_metrics(function_name, return_type, metrics, examples):
metric_key = RETURN_METRICS.get(return_type)
if metric_key:
increment_metric(metric_key, metrics, examples, function_name, return_type)
def increment_metric(metric_key, metrics, examples, function_name, value):
metrics[metric_key] += 1
append_example(examples[metric_key], function_name, value)
def append_example(examples, function_name, value):
if len(examples) < 5:
examples.append(f"{function_name}: {value}")
def load_json(path):
with path.open("r", encoding="utf-8") as file:
return json.load(file)
def write_json(path, data):
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description="Check generated Lua API documentation quality metrics.")
parser.add_argument(
"--update-baseline",
action="store_true",
help="write the current metrics to docs/lua-api/lua_api_quality_baseline.json",
)
args = parser.parse_args()
current, examples = collect_metrics(load_json(DOC_PATH))
if args.update_baseline:
write_json(BASELINE_PATH, current)
print(f"Updated Lua API quality baseline: {current}")
return 0
if not BASELINE_PATH.exists():
print(f"::error::{BASELINE_PATH} is missing. Run tools/check_lua_api_quality.py --update-baseline and commit it.")
return 1
baseline = load_json(BASELINE_PATH)
failed = False
for key in METRIC_KEYS:
value = current[key]
allowed = baseline.get(key, value)
if value > allowed:
print(f"::error::Lua API quality regression: {key} increased from {allowed} to {value}")
for example in examples[key]:
print(f" {example}")
failed = True
if failed:
return 1
print(f"Lua API quality check passed: {current}")
return 0
if __name__ == "__main__":
sys.exit(main())