From ee231b04bf46294790d3f6c7555345605822989b Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 6 Aug 2026 14:12:28 -0400 Subject: [PATCH] feat: report clang diagnostics and fail on errors A header that fails to include a file used to parse "successfully" with an incomplete AST, producing missing or empty docstrings. Diagnostics are now printed to stderr, and an error diagnostic aborts the run so no output file is written. Assisted-by: ClaudeCode:claude-fable-5 --- pybind11_mkdoc/mkdoc_lib.py | 16 +++++++++++++++- tests/cli_test.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index 0a553d7..ffc5755 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -552,9 +552,23 @@ def extract(filename, node, prefix, output, file_cache): return None +def _report_diagnostics(filename, tu): + """Print clang diagnostics to stderr and fail if any of them is an error.""" + errors = 0 + for diagnostic in tu.diagnostics: + sys.stderr.write(diagnostic.format() + "\n") + if diagnostic.severity >= cindex.Diagnostic.Error: + errors += 1 + if errors: + msg = f"Clang reported {errors} error(s) while parsing {filename}" + raise RuntimeError(msg) + + def _extract_file(filename, parameters): - index = cindex.Index(cindex.conf.lib.clang_createIndex(False, True)) + # Diagnostics are printed by _report_diagnostics, not by libclang itself. + index = cindex.Index(cindex.conf.lib.clang_createIndex(False, False)) tu = index.parse(filename, parameters) + _report_diagnostics(filename, tu) output = [] extract(filename, tu.cursor, "", output, {}) return output diff --git a/tests/cli_test.py b/tests/cli_test.py index 0abf36d..7d86c7f 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -37,3 +37,18 @@ def test_parse_failure_sets_exit_code(tmp_path: Path) -> None: assert result.returncode != 0 assert not tf.exists() + + +def test_missing_include_reports_diagnostics(tmp_path: Path) -> None: + header = tmp_path / "bad_header.h" + header.write_text('#include "does_not_exist.h"\n', encoding="utf-8") + tf = tmp_path / "tmp.h" + result = subprocess.run( + [sys.executable, "-m", "pybind11_mkdoc", "-o", tf, header], + check=False, + capture_output=True, + ) + + assert result.returncode != 0 + assert not tf.exists() + assert "does_not_exist.h" in result.stderr.decode()