Skip to content

✨ Preprocessor-aware C/C++ extraction via libclang#85

Open
ubmarco wants to merge 25 commits into
mainfrom
feat/libclang-preprocessor
Open

✨ Preprocessor-aware C/C++ extraction via libclang#85
ubmarco wants to merge 25 commits into
mainfrom
feat/libclang-preprocessor

Conversation

@ubmarco

@ubmarco ubmarco commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Adds an optional libclang preprocessor engine for C/C++ marker extraction.
The default tree-sitter path sees every comment; the libclang engine evaluates
the preprocessor (#if / #ifdef / defines) and emits only the markers in
active branches — so a need behind an inactive #else is correctly dropped.

  • New analyse/preproc/ — libclang loader, compile_commands.json walk-up
    discovery + per-file flag resolution, and an active-branch parser.
  • Opt-in via the libclang extra (pip install sphinx-codelinks[libclang]);
    plain tree-sitter extraction is unaffected when it is absent.
  • Extraction-config wiring (PreprocessorConfig) + analyse / configuration
    docs.

Testing — declarative fixtures

Tests use the declarative fixture + snapshot harness from #83. A new
tests/data/extraction/preproc_variants.yaml fixture runs the same source
through both engines with per-engine golden snapshots:

  • …[preproc_variants-variants_branching_treesitter] keeps every branch;
  • …[preproc_variants-variants_branching_libclang] drops the inactive #else
    and keeps only the active set under -DVARIANT_A -DPLATFORM_LINUX -DPROTOCOL_VERSION=3.

Plus focused unit tests for the loader, compile_commands handling, and the
parser. Full extraction-fixture + preproc suites pass locally.

Rebase note

Rebased onto current main (post-#83). The branch carried its own copy of the
declarative harness (developed before #83 merged); those commits were dropped
in the rebase, so this PR is purely the libclang additions layered on top of
the merged harness.

@codecov-commenter

codecov-commenter commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.94163% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.93%. Comparing base (43927d0) to head (8c5407a).

Files with missing lines Patch % Lines
src/sphinx_codelinks/analyse/preproc/compile_db.py 85.71% 5 Missing and 5 partials ⚠️
src/sphinx_codelinks/analyse/analyse.py 81.25% 6 Missing and 3 partials ⚠️
...phinx_codelinks/analyse/preproc/libclang_parser.py 89.74% 2 Missing and 2 partials ⚠️
src/sphinx_codelinks/analyse/preproc/loader.py 96.00% 1 Missing and 1 partial ⚠️
tests/test_preproc_libclang.py 97.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #85      +/-   ##
==========================================
+ Coverage   91.40%   91.93%   +0.53%     
==========================================
  Files          34       43       +9     
  Lines        3012     3521     +509     
  Branches      322      364      +42     
==========================================
+ Hits         2753     3237     +484     
- Misses        160      174      +14     
- Partials       99      110      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ubmarco ubmarco marked this pull request as ready for review June 30, 2026 17:58
ubmarco added a commit that referenced this pull request Jun 30, 2026
…ls.STYLE_TYPES) (#86)

## Problem

`sphinxcontrib-typer 0.9.1` (released after #84) imports
`typer.rich_utils.STYLE_TYPES`, which does not exist in the
`typer<0.26.8` range
pinned by #84. Any fresh docs build now resolves `sphinxcontrib-typer
0.9.1` and
fails during `sphinx-build`:

```
AttributeError: module 'typer.rich_utils' has no attribute 'STYLE_TYPES'
```

This affects `main` and every open PR (e.g. #85) on a fresh dependency
resolution. Note #84 capped *typer* for a **different** removed
attribute
(`STYLE_METAVAR`); this is a new break originating from the
sphinxcontrib-typer
side.

## Fix

Cap `sphinxcontrib-typer>=0.5.1,<0.9.1`. `0.9.0` builds cleanly with the
capped
typer `0.26.7`; verified locally that `sphinx-build -nW` no longer
raises the
`AttributeError`.

Dependency-only change — no source, tests, or typing impact.
ubmarco added 21 commits June 30, 2026 20:01
Pass file_field (raw entry["file"]) instead of abs_file to filter_args so
that when compile_commands.json encodes the input as a relative path (e.g.
"src/a.cpp") the argument is correctly stripped rather than leaking to
libclang as a spurious positional argument.

Also fix the existing command-form test whose entry had an inconsistent
absolute path in the command string but a relative "file" field; both now
use the same relative form.

Regression test added: directory=tmp, file="src/a.cpp",
arguments=[..., "src/a.cpp"] -> flags == ["-DA=1"].
…harden test

_resolve_preproc_args now returns None when a compile DB is found but the
source file is not listed in it, causing create_src_objects_libclang to
skip the file with a debug log rather than falling back to defines_to_args.
No-DB path (defines fallback) is unchanged.

Also: add IMPL_ALWAYS vacuous-pass guard to test_libclang_engine_other_variant
and a new test_libclang_skip_file_absent_from_compile_commands that verifies
the skip behaviour end-to-end.
Add test_libclang_active_matches_golden to assert Python libclang engine
produces the same 4 active needs as the shared Rust golden (IMPL_ALWAYS,
IMPL_VAR_A, IMPL_PROTO_3, IMPL_LINUX_A) with defines VARIANT_A=1,
PLATFORM_LINUX=1, PROTOCOL_VERSION=3, excluding IMPL_VAR_B as expected.
SourceAnalyseConfig.preprocessor carried a flat metadata schema
{type: [object, null]}, so check_schema validated the constructed
PreprocessorConfig *instance* against JSON type object and raised at
config-inited: 'PreprocessorConfig(...) is not of type object, null'.

Nested dataclass config fields (need_id_refs_config, marked_rst_config,
oneline_comment_style) carry no flat schema; the preprocessor field must
match. Its structure is enforced by convert_analyse_config. Adds a
regression test exercising the validation path.
The opt-in libclang engine adds preprocessor-aware extraction (skipping comments inside inactive #if/#ifdef regions) for C, C++, and Objective-C. Tree-sitter already covers C/C++ extraction, so the extra was misnamed `cpp`: it implied "C++ support" rather than the libclang backend it actually pulls in. Rename it to `libclang`, matching the dependency and the install hint.

Add tests/test_libclang_optional.py, a regression guard that blocks `clang` in a fresh interpreter (a tree-sitter-only install, by construction) and asserts: the default tree-sitter path imports and runs without libclang; and selecting the libclang engine without the extra raises the documented "pip install 'sphinx-codelinks[libclang]'" hint. It proves the block is effective first, so it cannot pass vacuously under CI, which installs the extra.
codelinks is the OSS reference implementation; cross-impl feature parity is
ubCode's concern and lives there. Remove the shared Rust golden + its test;
codelinks keeps its own behavioral tests (inactive-branch exclusion, resilience,
compile_commands, header extraction).
Restore the libclang golden snapshot (its earlier removal was a mistake) and add
a tree-sitter golden beside it. Both engines are now pinned to declarative JSON
snapshots through a shared projection helper:
- variants_branching.expected.json             (libclang active set, 4 markers)
- variants_branching.treesitter.expected.json  (tree-sitter, all 5 markers)

The snapshots are language-agnostic declarative data so any conforming
implementation can assert against the same fixtures.
Add an 'engine' dimension to the declarative extraction harness: a fixture case
may set 'engine: libclang' + 'defines' to exercise the preprocessor-aware path
(inactive #if/#ifdef branches excluded); libclang cases skip when the clang
bindings are absent. variants_branching now lives as declarative fixtures under
both engines (tests/data/extraction/preproc_variants.yaml): tree-sitter -> all 5
markers, libclang -> the 4-marker active set.

Drop the bespoke goldens (variants_branching{,.treesitter}.expected.json and
test_libclang_active_matches_golden / test_treesitter_matches_golden); the
libclang behavioral tests (inactive-branch exclusion, resilience, compile_commands,
§3.3 skip, header-standalone) remain.
- mypy: ignore missing clang.cindex stubs; int() the libclang parse flags;
  import CommentType from its defining module; fix preproc TOML ignore codes
- ruff-format the preproc engine + its tests

Fixes the MyPy and Pre-commit CI checks on the libclang PR.
@ubmarco ubmarco force-pushed the feat/libclang-preprocessor branch from af5c95c to b97c66a Compare June 30, 2026 18:01
…ers, don't crash)

A header carrying oneline markers never appears in compile_commands.json, so the
engine parses it standalone with the global defines. libclang infers the C
language from a .h/.c/.inc extension, and a C++ -std flag then makes clang reject
the combination and return a NULL TU (TranslationUnitLoadError) — which aborted
the whole Sphinx build (notably 'sphinx-build -b ubtrace -W' on the presets).

Pin -x to match -std in defines_to_args so such files parse as C++ and their
markers are EXTRACTED rather than dropped. Keep a last-resort
TranslationUnitLoadError guard (logged at info, not warning, so -W stays green)
for files that still cannot load as a TU at all.

Adds a regression test + .h fixture proving a C-extension header's markers extract.
@ubmarco ubmarco force-pushed the feat/libclang-preprocessor branch from 893e9dd to 3618ba4 Compare July 1, 2026 19:01
ubmarco added 2 commits July 2, 2026 22:19
Adds a .h header case to the shared declarative extraction suite: a header is
parsed standalone under libclang (pinned to C++ via defines_to_args) and its
active-branch markers extract while inactive-branch markers are excluded. Adds a
'cpp_header' lang -> .h mapping. This is the engine-agnostic golden that surfaces
the same requirement for the Rust engine (ubc_codelinks).
Declarative libclang cases covering nested #ifdef, #elif chains, #if 0 dead
blocks, defined()/boolean logic, arithmetic comparisons, and an in-source
#define driving a branch. Each keeps markers in both the taken and untaken
branches; only the active-region markers survive, proving the preprocessor is
evaluated (tree-sitter would keep them all).
The declarative harness can now materialize a compile_commands.json (inline
`compile_commands` entries) or point at an explicit `compile_commands_path`.
New preproc_compile_db cases cover DB-provided flags, a header absent from a
present DB (parsed standalone), a translation-unit source absent from the DB
(skipped per spec §3.3), and an unreadable explicit DB path (defines fallback).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants