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)