1"""Import guard for clang.cindex + ctypes shim for clang_getAllSkippedRanges.
 2
 3clang.cindex (shipped by the PyPI ``libclang`` wheel, which bundles the native
 4library  no compiler required) does not expose clang_getAllSkippedRanges, so
 5we bind it via ctypes, exactly as the design concept's _common.py did.
 6"""
 7
 8from __future__ import annotations
 9
10import ctypes
11from pathlib import Path
12from typing import Any, NamedTuple
13
14_INSTALL_HINT = (
15    "The libclang engine requires clang.cindex. Install the extra:\n"
16    "    pip install 'sphinx-codelinks[libclang]'"
17)
18
19
[docs]20# @Guard the optional libclang dependency with an install hint, IMPL_PREPROC_6, impl, [FE_PREPROC]
21def load_clang_cindex() -> Any:  # type: ignore[explicit-any]
22    """Return the clang.cindex module or raise a clear install error."""
23    try:
24        import clang.cindex as cx  # noqa: PLC0415
25    except ImportError as exc:  # pragma: no cover - exercised via patched import
26        raise ImportError(_INSTALL_HINT) from exc
27    return cx
28
29
30# Production parse flags (design "Parse flags codelinks should use").
31def _parse_options() -> int:
32    cx = load_clang_cindex()
33    # ``cx`` is untyped (clang ships no stubs), so the bit-or is ``Any``; coerce
34    # back to ``int`` to satisfy the declared return type.
35    return int(
36        cx.TranslationUnit.PARSE_INCOMPLETE
37        | cx.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
38        | cx.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
39    )
40
41
42PARSE_OPTIONS: int = _parse_options()
43
44
45class SkippedRange(NamedTuple):
46    file: Path | None
47    start_line: int
48    start_col: int
49    end_line: int
50    end_col: int
51
52
53class _CXSourceRangeList(ctypes.Structure):
54    pass
55
56
57_BOUND = False
58
59
60def _bind() -> None:
61    global _BOUND  # noqa: PLW0603
62    if _BOUND:
63        return
64    cx = load_clang_cindex()
65    _CXSourceRangeList._fields_ = [
66        ("count", ctypes.c_uint),
67        ("ranges", ctypes.POINTER(cx.SourceRange)),
68    ]
69    lib = cx.conf.lib
70    lib.clang_getAllSkippedRanges.argtypes = [ctypes.c_void_p]
71    lib.clang_getAllSkippedRanges.restype = ctypes.POINTER(_CXSourceRangeList)
72    lib.clang_disposeSourceRangeList.argtypes = [ctypes.POINTER(_CXSourceRangeList)]
73    lib.clang_disposeSourceRangeList.restype = None
74    _BOUND = True
75
76
77def get_all_skipped_ranges(tu: Any) -> list[SkippedRange]:  # type: ignore[explicit-any]
78    """Return every source range the preprocessor skipped in this TU."""
79    _bind()
80    cx = load_clang_cindex()
81    ptr = cx.conf.lib.clang_getAllSkippedRanges(tu.obj)
82    if not ptr:
83        return []
84    try:
85        rl = ptr.contents
86        out: list[SkippedRange] = []
87        for i in range(rl.count):
88            r = rl.ranges[i]
89            start = r.start
90            end = r.end
91            f = Path(start.file.name) if start.file else None
92            out.append(SkippedRange(f, start.line, start.column, end.line, end.column))
93        return out
94    finally:
95        cx.conf.lib.clang_disposeSourceRangeList(ptr)