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 or
7clang_getSkippedRanges, which both wheels' native libraries export: they are bound
8the same way. And their ``Cursor.get_included_file`` asserts on the NULL that
9clang_getIncludedFile returns for an ``#include`` libclang could not resolve, so
10:func:`included_file` reads it through a binding of its own.
11"""
12
13from __future__ import annotations
14
15import ctypes
16from pathlib import Path
17from typing import Any, NamedTuple
18
19_INSTALL_HINT = (
20 "The libclang engine requires clang.cindex. Install the extra:\n"
21 " pip install 'sphinx-codelinks[libclang]'"
22)
23
24
[docs] 25# @Guard the optional libclang dependency with an install hint, IMPL_PREPROC_6, impl, [FE_PREPROC]
26def load_clang_cindex() -> Any:
27 """Return the clang.cindex module or raise a clear install error."""
28 try:
29 import clang.cindex as cx
30 except ImportError as exc: # pragma: no cover - exercised via patched import
31 raise ImportError(_INSTALL_HINT) from exc
32 return cx
33
34
35# Production parse flags (design "Parse flags codelinks should use").
36def _parse_options() -> int:
37 cx = load_clang_cindex()
38 # ``cx`` is untyped (clang ships no stubs), so the bit-or is ``Any``; coerce
39 # back to ``int`` to satisfy the declared return type.
40 return int(
41 cx.TranslationUnit.PARSE_INCOMPLETE
42 | cx.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES
43 | cx.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
44 )
45
46
47PARSE_OPTIONS: int = _parse_options()
48
49
50class SkippedRange(NamedTuple):
51 file: Path | None
52 start_line: int
53 start_col: int
54 end_line: int
55 end_col: int
56 from_main_file: bool
57 """Whether the range starts in the translation unit's main file, in the main
58 file's own pass: a later inclusion of the same path is a pass of its own, and
59 reports ``False`` here (``clang_Location_isFromMainFile``)."""
60
61
62class _CXSourceRangeList(ctypes.Structure):
63 pass
64
65
66_BOUND = False
67_INCLUDED_FILE: Any = None
68
69
70def _bind() -> None:
71 global _BOUND
72 if _BOUND:
73 return
74 cx = load_clang_cindex()
75 _CXSourceRangeList._fields_ = [
76 ("count", ctypes.c_uint),
77 ("ranges", ctypes.POINTER(cx.SourceRange)),
78 ]
79 lib = cx.conf.lib
80 lib.clang_getAllSkippedRanges.argtypes = [ctypes.c_void_p]
81 lib.clang_getAllSkippedRanges.restype = ctypes.POINTER(_CXSourceRangeList)
82 lib.clang_getSkippedRanges.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
83 lib.clang_getSkippedRanges.restype = ctypes.POINTER(_CXSourceRangeList)
84 lib.clang_disposeSourceRangeList.argtypes = [ctypes.POINTER(_CXSourceRangeList)]
85 lib.clang_disposeSourceRangeList.restype = None
86 # Registered as cindex registers clang_Location_isInSystemHeader, but with a
87 # ctypes restype rather than ``bool`` (a non-ctypes restype is deprecated).
88 lib.clang_Location_isFromMainFile.argtypes = [cx.SourceLocation]
89 lib.clang_Location_isFromMainFile.restype = ctypes.c_int
90 _BOUND = True
91
92
93def included_file(cursor: Any) -> Path | None:
94 """The file an inclusion-directive cursor includes, or ``None`` when libclang did
95 not find it (``<stddef.h>`` without clang's builtin headers, a header not generated
96 yet).
97
98 The name is the one libclang knows the file by: relative to the unit's working
99 directory when it was found through a relative include directory.
100 """
101 global _INCLUDED_FILE
102 cx = load_clang_cindex()
103 if _INCLUDED_FILE is None:
104 # A function pointer of its own (``CDLL.__getitem__`` makes a new one), so
105 # the ``clang.cindex`` binding of the same function is left as it is.
106 function = cx.conf.lib["clang_getIncludedFile"]
107 function.argtypes = [cx.Cursor]
108 function.restype = ctypes.c_void_p
109 _INCLUDED_FILE = function
110 pointer = _INCLUDED_FILE(cursor)
111 if not pointer:
112 return None
113 return Path(cx.File(ctypes.cast(pointer, cx.c_object_p)).name)
114
115
116def get_all_skipped_ranges(tu: Any) -> list[SkippedRange]:
117 """Return every source range the preprocessor skipped in this TU."""
118 _bind()
119 cx = load_clang_cindex()
120 return _read_range_list(cx, cx.conf.lib.clang_getAllSkippedRanges(tu.obj))
121
122
123def get_first_inclusion_skipped_ranges(tu: Any, file: Any) -> list[SkippedRange]:
124 """Return the ranges the preprocessor skipped in ``file``'s FIRST pass.
125
126 ``clang_getSkippedRanges`` selects the ranges of one file ID, the one the source
127 manager gives for the file (``SourceManager::translateFile``): its first inclusion
128 in the translation unit. A later inclusion of the same file is a file ID of its
129 own, and its ranges are not returned.
130
131 :param tu: The parsed translation unit.
132 :param file: A ``clang.cindex.File`` of ``tu`` (``tu.get_file``).
133 """
134 _bind()
135 cx = load_clang_cindex()
136 return _read_range_list(cx, cx.conf.lib.clang_getSkippedRanges(tu.obj, file))
137
138
139def _read_range_list(cx: Any, ptr: Any) -> list[SkippedRange]:
140 """Read and dispose of a ``CXSourceRangeList``."""
141 if not ptr:
142 return []
143 try:
144 rl = ptr.contents
145 out: list[SkippedRange] = []
146 for i in range(rl.count):
147 r = rl.ranges[i]
148 start = r.start
149 end = r.end
150 f = Path(start.file.name) if start.file else None
151 from_main = bool(cx.conf.lib.clang_Location_isFromMainFile(start))
152 out.append(
153 SkippedRange(
154 f, start.line, start.column, end.line, end.column, from_main
155 )
156 )
157 return out
158 finally:
159 cx.conf.lib.clang_disposeSourceRangeList(ptr)