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)