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)