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. Nor do
6the 16.0 and 18.1 bindings expose clang_Location_isFromMainFile, which both
7wheels' native libraries export: it is bound the same way.
8"""
9
10from __future__ import annotations
11
12import ctypes
13from pathlib import Path
14from typing import Any, NamedTuple
15
16_INSTALL_HINT = (
17 "The libclang engine requires clang.cindex. Install the extra:\n"
18 " pip install 'sphinx-codelinks[libclang]'"
19)
20
21
[docs] 22# @Guard the optional libclang dependency with an install hint, IMPL_PREPROC_6, impl, [FE_PREPROC]
23def load_clang_cindex() -> Any:
24 """Return the clang.cindex module or raise a clear install error."""
25 try:
26 import clang.cindex as cx
27 except ImportError as exc: # pragma: no cover - exercised via patched import
28 raise ImportError(_INSTALL_HINT) from exc
29 return cx
30
31
32# Production parse flags (design "Parse flags codelinks should use").
33def _parse_options() -> int:
34 cx = load_clang_cindex()
35 # ``cx`` is untyped (clang ships no stubs), so the bit-or is ``Any``; coerce
36 # back to ``int`` to satisfy the declared return type.
37 return int(
38 cx.TranslationUnit.PARSE_INCOMPLETE
39 | cx.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
40 | cx.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
41 )
42
43
44PARSE_OPTIONS: int = _parse_options()
45
46
47class SkippedRange(NamedTuple):
48 file: Path | None
49 start_line: int
50 start_col: int
51 end_line: int
52 end_col: int
53 from_main_file: bool
54 """Whether the range starts in the translation unit's main file, in the main
55 file's own pass: a later inclusion of the same path is a pass of its own, and
56 reports ``False`` here (``clang_Location_isFromMainFile``)."""
57
58
59class _CXSourceRangeList(ctypes.Structure):
60 pass
61
62
63_BOUND = False
64
65
66def _bind() -> None:
67 global _BOUND
68 if _BOUND:
69 return
70 cx = load_clang_cindex()
71 _CXSourceRangeList._fields_ = [
72 ("count", ctypes.c_uint),
73 ("ranges", ctypes.POINTER(cx.SourceRange)),
74 ]
75 lib = cx.conf.lib
76 lib.clang_getAllSkippedRanges.argtypes = [ctypes.c_void_p]
77 lib.clang_getAllSkippedRanges.restype = ctypes.POINTER(_CXSourceRangeList)
78 lib.clang_disposeSourceRangeList.argtypes = [ctypes.POINTER(_CXSourceRangeList)]
79 lib.clang_disposeSourceRangeList.restype = None
80 # Registered as cindex registers clang_Location_isInSystemHeader, but with a
81 # ctypes restype rather than ``bool`` (a non-ctypes restype is deprecated).
82 lib.clang_Location_isFromMainFile.argtypes = [cx.SourceLocation]
83 lib.clang_Location_isFromMainFile.restype = ctypes.c_int
84 _BOUND = True
85
86
87def get_all_skipped_ranges(tu: Any) -> list[SkippedRange]:
88 """Return every source range the preprocessor skipped in this TU."""
89 _bind()
90 cx = load_clang_cindex()
91 ptr = cx.conf.lib.clang_getAllSkippedRanges(tu.obj)
92 if not ptr:
93 return []
94 try:
95 rl = ptr.contents
96 out: list[SkippedRange] = []
97 for i in range(rl.count):
98 r = rl.ranges[i]
99 start = r.start
100 end = r.end
101 f = Path(start.file.name) if start.file else None
102 from_main = bool(cx.conf.lib.clang_Location_isFromMainFile(start))
103 out.append(
104 SkippedRange(
105 f, start.line, start.column, end.line, end.column, from_main
106 )
107 )
108 return out
109 finally:
110 cx.conf.lib.clang_disposeSourceRangeList(ptr)