xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/classes/_cursor.py (revision 9c5460f9064ca60dd71a234a1f6faf93e7a6b0c9)
1#!/usr/bin/env python3
2"""
3# Created: Mon Jun 20 18:42:44 2022 (-0400)
4# @author: Jacob Faibussowitsch
5"""
6from __future__ import annotations
7
8import enum
9import ctypes
10import clang.cindex as clx # type: ignore[import]
11
12from .._typing import *
13
14from ._src_pos    import SourceRange, SourceLocation
15from ._attr_cache import AttributeCache
16from ._path       import Path
17from .            import _util
18
19from .._error import KnownUnhandleableCursorError, ParsingError
20
21from ..util._clang import (
22  get_clang_function,
23  clx_math_cursor_kinds, clx_cast_cursor_kinds, clx_pointer_type_kinds, clx_literal_cursor_kinds,
24  clx_var_token_kinds, clx_function_type_kinds
25)
26
27class CtypesEnum(enum.IntEnum):
28  """
29  A ctypes-compatible IntEnum superclass
30  """
31  @classmethod
32  def from_param(cls, obj: SupportsInt) -> int:
33    return int(obj)
34
35@enum.unique
36class CXChildVisitResult(CtypesEnum):
37  # see
38  # https://clang.llvm.org/doxygen/group__CINDEX__CURSOR__TRAVERSAL.html#ga99a9058656e696b622fbefaf5207d715
39  # Terminates the cursor traversal.
40  Break    = 0
41  # Continues the cursor traversal with the next sibling of the cursor just visited,
42  # without visiting its children.
43  Continue = 1
44  # Recursively traverse the children of this cursor, using the same visitor and client
45  # data.
46  Recurse  = 2
47
48CXCursorAndRangeVisitorCallBackProto = ctypes.CFUNCTYPE(
49  ctypes.c_uint, ctypes.py_object, clx.Cursor, clx.SourceRange
50)
51
52class PetscCXCursorAndRangeVisitor(ctypes.Structure):
53  # see https://clang.llvm.org/doxygen/structCXCursorAndRangeVisitor.html
54  #
55  # typedef struct CXCursorAndRangeVisitor {
56  #   void *context;
57  #   enum CXVisitorResult (*visit)(void *context, CXCursor, CXSourceRange);
58  # } CXCursorAndRangeVisitor;
59  #
60  # Note this is not a  strictly accurate recreation, as this struct expects a
61  # (void *) but since C lets anything be a (void *) we can pass in a (PyObject *)
62  _fields_ = [
63    ('context', ctypes.py_object),
64    ('visit',   CXCursorAndRangeVisitorCallBackProto)
65  ]
66
67def make_cxcursor_and_range_callback(cursor: CursorLike, parsing_error_handler: Optional[Callable[[ParsingError], None]] = None) -> tuple[PetscCXCursorAndRangeVisitor, list[Cursor]]:
68  r"""Make a clang cxcursor and range callback functor
69
70  Parameters
71  ----------
72  cursor : cursor_like
73    the cursor to create the callback visitor for
74  found_cursors : array_like, optional
75    an array or list to append found cursors to, None to create a new list
76  parsing_error_handler : callable, optional
77    an error handler to handle petsclinter.ParsingError exceptions, which takes the exception object
78    as a single parameter
79
80  Returns
81  -------
82  cx_callback, found_cursors : callable, array_like
83    the callback and found_cursors list
84  """
85  if parsing_error_handler is None:
86    parsing_error_handler = lambda exc: None
87
88  found_cursors = []
89  def visitor(ctx: Any, raw_clx_cursor: clx.Cursor, src_range: clx.SourceRange) -> CXChildVisitResult:
90    # The "raw_clx_cursor" returned here is actually a raw clang 'CXCursor' c-struct, not
91    # the a clx.Cursor that we lead python to believe in our function prototype. Luckily
92    # we have all we need to remake the python object from scratch
93    try:
94      clx_cursor = clx.Cursor.from_location(ctx.translation_unit, src_range.start)
95      found_cursors.append(Cursor(clx_cursor))
96    except ParsingError as pe:
97      assert callable(parsing_error_handler)
98      parsing_error_handler(pe)
99    except Exception:
100      import petsclinter as pl
101      import traceback
102
103      string = 'Full error full error message below:'
104      pl.sync_print('=' * 30, 'CXCursorAndRangeVisitor Error', '=' * 30)
105      pl.sync_print("It is possible that this is a false positive! E.g. some 'unexpected number of tokens' errors are due to macro instantiation locations being misattributed.\n", string, '\n', '-' * len(string), '\n', traceback.format_exc(), sep='')
106      pl.sync_print('=' * 30, 'CXCursorAndRangeVisitor End Error', '=' * 26)
107    return CXChildVisitResult.Continue # continue, recursively
108
109  cx_callback = PetscCXCursorAndRangeVisitor(
110    # (PyObject *)cursor;
111    ctypes.py_object(cursor),
112    # (enum CXVisitorResult(*)(void *, CXCursor, CXSourceRange))visitor;
113    CXCursorAndRangeVisitorCallBackProto(visitor)
114  )
115  return cx_callback, found_cursors
116
117class Cursor(AttributeCache):
118  """
119  A utility wrapper around clang.cindex.Cursor that makes retrieving certain useful properties
120  (such as demangled names) from a cursor easier.
121  Also provides a host of utility functions that get and (optionally format) the source code
122  around a particular cursor. As it is a wrapper any operation done on a clang Cursor may be
123  performed directly on a Cursor (although this object does not pass the isinstance() check).
124
125  See __getattr__ below for more info.
126  """
127  __slots__ = '__cursor', 'extent', 'name', 'typename', 'derivedtypename', 'argidx'
128
129  __cursor: clx.Cursor
130  extent: SourceRange
131  name: str
132  typename: str
133  derivedtypename: str
134  argidx: int
135
136  def __init__(self, cursor: CursorLike, idx: int = -12345) -> None:
137    r"""Construct a `Cursor`
138
139    Parameters
140    ----------
141    cursor :
142      the cursor to construct this cursor from, can be a `clang.cindex.Cursor` or another `Cursor`
143    id : optional
144      the index into the parent functions arguments for this cursor, if applicable
145
146    Raises
147    ------
148    ValueError
149      if `cursor` is not a `Cursor` or a `clang.cindex.Cursor`
150    """
151    if isinstance(cursor, Cursor):
152      super().__init__(cursor._cache)
153      self.__cursor        = cursor.clang_cursor()
154      self.extent          = cursor.extent
155      self.name            = cursor.name
156      self.typename        = cursor.typename
157      self.derivedtypename = cursor.derivedtypename
158      self.argidx          = cursor.argidx if idx == -12345 else idx
159    elif isinstance(cursor, clx.Cursor):
160      super().__init__()
161      self.__cursor        = cursor
162      self.extent          = SourceRange.cast(cursor.extent)
163      self.name            = self.get_name_from_cursor(cursor)
164      self.typename        = self.get_typename_from_cursor(cursor)
165      self.derivedtypename = self.get_derived_typename_from_cursor(cursor)
166      self.argidx          = idx
167    else:
168      raise ValueError(type(cursor))
169    return
170
171  def __getattr__(self, attr: str) -> Any:
172    """
173    Allows us to essentialy fake being a clang cursor, if __getattribute__ fails
174    (i.e. the value wasn't found in self), then we try the cursor. So we can do things
175    like self.translation_unit, but keep all of our variables out of the cursors
176    namespace
177    """
178    return getattr(self.__cursor, attr)
179
180  def __str__(self) -> str:
181    return f'{self.get_formatted_location_string()}\n{self.get_formatted_blurb()}'
182
183  def __hash__(self) -> int:
184    return hash(self.__cursor.hash)
185
186  @classmethod
187  def _unhandleable_cursor(cls, cursor: CursorLike) -> NoReturn:
188    r"""Given a `cursor`, try to construct as useful an error message as possible from it before
189    self destructing
190
191    Parameters
192    ----------
193    cursor :
194      the cursor to construct the message from
195
196    Raises
197    ------
198    KnownUnhandleableCursorError
199      if the cursor is known not to be handleable
200    RuntimeError
201      this is raised in all other cases
202
203    Notes
204    -----
205    This function is 'noreturn'
206    """
207    # For whatever reason (perhaps because its macro stringization hell) PETSC_HASH_MAP
208    # and PetscKernel_XXX absolutely __brick__ the AST. The resultant cursors have no
209    # children, no name, no tokens, and a completely incorrect SourceLocation.
210    # They are for all intents and purposes uncheckable :)
211    srcstr = cls.get_raw_source_from_cursor(cursor)
212    errstr = cls.error_view_from_cursor(cursor)
213    if 'PETSC_HASH' in srcstr:
214      if '_MAP' in srcstr:
215        raise KnownUnhandleableCursorError(f'Encountered unparsable PETSC_HASH_MAP for cursor {errstr}')
216      if '_SET' in srcstr:
217        raise KnownUnhandleableCursorError(f'Encountered unparsable PETSC_HASH_SET for cursor {errstr}')
218      raise KnownUnhandleableCursorError(f'Unhandled unparsable PETSC_HASH_XXX for cursor {errstr}')
219    if 'PetscKernel_' in srcstr:
220      raise KnownUnhandleableCursorError(f'Encountered unparsable PetscKernel_XXX for cursor {errstr}')
221    if ('PetscOptions' in srcstr) or ('PetscObjectOptions' in srcstr):
222      raise KnownUnhandleableCursorError(f'Encountered unparsable Petsc[Object]OptionsBegin for cursor {errstr}')
223    try:
224      cursor_view = '\n'.join(_util.view_cursor_full(cursor, max_depth=10))
225    except Exception as exc:
226      cursor_view = f'ERROR GENERATING CURSOR VIEW\n{str(exc)}'
227    raise RuntimeError(
228      f'Could not determine useful name for cursor {errstr}\nxxx {"-" * 80} xxx\n{cursor_view}'
229    )
230
231  @classmethod
232  def cast(cls, cursor: CursorLike) -> Cursor:
233    r"""like numpy.asanyarray but for `Cursor`s
234
235    Parameters
236    ----------
237    cursor :
238      the cursor object to cast
239
240    Returns
241    -------
242    cursor :
243      either a newly constructed `Cursor` or `cursor` unchanged
244    """
245    return cursor if isinstance(cursor, cls) else cls(cursor)
246
247  @classmethod
248  def error_view_from_cursor(cls, cursor: CursorLike) -> str:
249    r"""Get error handling information from a cursor
250
251    Parameters
252    ----------
253    cursor :
254      the cursor to extract information from
255
256    Returns
257    -------
258    ret :
259      a hopefully useful string to pass to an exception
260
261    Notes
262    -----
263    Something has gone wrong, and we try to extract as much information from the cursor as
264    possible for the exception. Nothing is guaranteed to be useful here.
265    """
266    # Does not yet raise exception so we can call it here
267    loc_str  = cls.get_formatted_location_string_from_cursor(cursor)
268    typename = cls.get_typename_from_cursor(cursor)
269    src_str  = cls.get_formatted_source_from_cursor(cursor, num_context=2)
270    return f"'{cursor.displayname}' of kind '{cursor.kind}' of type '{typename}' at {loc_str}:\n{src_str}"
271
272  @classmethod
273  def get_name_from_cursor(cls, cursor: CursorLike) -> str:
274    r"""Try to convert **&(PetscObject)obj[i]+73 to obj
275
276    Parameters
277    ----------
278    cursor :
279      the cursor
280
281    Returns
282    -------
283    name :
284      the sanitized name of `cursor`
285    """
286    if isinstance(cursor, cls):
287      return cursor.name
288
289    def cls_get_name_from_cursor_safe_call(cursor: CursorLike) -> str:
290      try:
291        return cls.get_name_from_cursor(cursor)
292      except (RuntimeError, ParsingError):
293        return ''
294
295    name = ''
296    if cursor.spelling:
297      name = cursor.spelling
298    elif cursor.kind in clx_math_cursor_kinds:
299      if cursor.kind == clx.CursorKind.BINARY_OPERATOR:
300        # we arbitrarily use the first token here since we assume that it is the important
301        # one.
302        operands = list(cursor.get_children())
303        # its certainly funky when a binary operation doesn't have a binary system of
304        # operands
305        assert len(operands) == 2, f'Found {len(operands)} operands for binary operator when only expecting 2 for cursor {cls.error_view_from_cursor(cursor)}'
306        for name in map(cls_get_name_from_cursor_safe_call, operands):
307          if name:
308            break
309      else:
310        # just a plain old number or unary operator
311        name = ''.join(t.spelling for t in cursor.get_tokens())
312    elif cursor.kind in clx_cast_cursor_kinds:
313      # Need to extract the castee from the caster
314      castee = [c for c in cursor.get_children() if c.kind == clx.CursorKind.UNEXPOSED_EXPR]
315      # If we don't have 1 symbol left then we're in trouble, as we probably didn't
316      # pick the right cursors above
317      assert len(castee) == 1, f'Cannot determine castee from the caster for cursor {cls.error_view_from_cursor(cursor)}'
318      # Easer to do some mild recursion to figure out the naming for us than duplicate
319      # the code. Perhaps this should have some sort of recursion check
320      name = cls_get_name_from_cursor_safe_call(castee[0])
321    elif (cursor.type.get_canonical().kind == clx.TypeKind.POINTER) or (cursor.kind == clx.CursorKind.UNEXPOSED_EXPR):
322      if clx.CursorKind.ARRAY_SUBSCRIPT_EXPR in {c.kind for c in cursor.get_children()}:
323        # in the form of obj[i], so we try and weed out the iterator variable
324        pointees = [
325          c for c in cursor.walk_preorder() if c.type.get_canonical().kind in clx_pointer_type_kinds
326        ]
327      elif cursor.type.get_pointee().kind == clx.TypeKind.CHAR_S:
328        # For some reason preprocessor macros that contain strings don't propagate
329        # their spelling up to the primary cursor, so we need to plumb through
330        # the various sub-cursors to find it.
331        pointees = [c for c in cursor.walk_preorder() if c.kind in clx_literal_cursor_kinds]
332      else:
333        pointees = []
334      pointees = list({p.spelling: p for p in pointees}.values())
335      if len(pointees) > 1:
336        # sometimes array subscripts can creep in
337        subscript_operator_kinds = clx_math_cursor_kinds | {clx.CursorKind.ARRAY_SUBSCRIPT_EXPR}
338        pointees                 = [c for c in pointees if c.kind not in subscript_operator_kinds]
339      if len(pointees) == 1:
340        name = cls_get_name_from_cursor_safe_call(pointees[0])
341    elif cursor.kind == clx.CursorKind.ENUM_DECL:
342      # have a
343      # typedef enum { ... } Foo;
344      # so the "name" of the cursor is actually the name of the type itself
345      name = cursor.type.get_canonical().spelling
346    elif cursor.kind == clx.CursorKind.PAREN_EXPR:
347      possible_names = {n for n in map(cls_get_name_from_cursor_safe_call, cursor.get_children()) if n}
348      try:
349        name = possible_names.pop()
350      except KeyError:
351        # *** KeyError: 'pop from an empty set'
352        pass
353    elif cursor.kind == clx.CursorKind.COMPOUND_STMT:
354      # we have a cursor pointing to a '{'. clang treats these cursors a bit weirdly, they
355      # essentially encompass _all_ of the statements between the brackets, but curiously
356      # do not
357      name = SourceLocation(cursor.extent.start, cursor.translation_unit).raw().strip()
358
359    if not name:
360      if cursor.kind  == clx.CursorKind.PARM_DECL:
361        # have a parameter declaration, these are allowed to be unnamed!
362        return TYPE_CAST(str, cursor.spelling)
363      # Catchall last attempt, we become the very thing we swore to destroy and parse the
364      # tokens ourselves
365      token_list = [t for t in cursor.get_tokens() if t.kind in clx_var_token_kinds]
366      # Remove iterator variables
367      token_list = [t for t in token_list if t.cursor.kind not in clx_math_cursor_kinds]
368      # removes all cursors that have duplicate spelling
369      token_list = list({t.spelling: t for t in token_list}.values())
370      if len(token_list) != 1:
371        cls._unhandleable_cursor(cursor)
372      name = token_list[0].spelling
373      assert name, f'Cannot determine name of symbol from cursor {cls.error_view_from_cursor(cursor)}'
374    return name
375
376  @classmethod
377  def get_raw_name_from_cursor(cls, cursor: CursorLike) -> str:
378    r"""If get_name_from_cursor tries to convert **&(PetscObject)obj[i]+73 to obj then this function
379    tries to extract **&(PetscObject)obj[i]+73 in the cleanest way possible
380
381    Parameters
382    ----------
383    cursor :
384      the cursor
385
386    Returns
387    -------
388    name :
389      the un-sanitized name of `cursor`
390    """
391    def get_name() -> str:
392      name = ''.join(t.spelling for t in cursor.get_tokens())
393      if not name:
394        try:
395          # now we try for the formatted name
396          name = cls.get_name_from_cursor(cursor)
397        except ParsingError:
398          # noreturn
399          cls._unhandleable_cursor(cursor)
400      return name
401
402    if isinstance(cursor, cls):
403      return cursor._get_cached('name', get_name)
404    return get_name()
405
406  @classmethod
407  def get_typename_from_cursor(cls, cursor: CursorLike) -> str:
408    r"""Try to get the most canonical type from a cursor so DM -> _p_DM *
409
410    Parameters
411    ----------
412    cursor :
413      the cursor
414
415    Returns
416    -------
417    name :
418      the canonical type of `cursor`
419    """
420    if isinstance(cursor, cls):
421      return cursor.typename
422    canon: str = cursor.type.get_canonical().spelling
423    return canon if canon else cls.get_derived_typename_from_cursor(cursor)
424
425  @staticmethod
426  def get_derived_typename_from_cursor(cursor: CursorLike) -> str:
427    r"""Get the least canonical type form a cursor so DM -> DM
428
429    Parameters
430    ----------
431    cursor :
432      the cursor
433
434    Returns
435    -------
436    name :
437      the least canonical type of `cursor`
438    """
439    return TYPE_CAST(str, cursor.type.spelling)
440
441  @classmethod
442  def has_internal_linkage_from_cursor(cls, cursor: CursorLike) -> tuple[bool, str, Optional[clx.Cursor]]:
443    r"""Determine whether `cursor` has internal linkage
444
445    Parameters
446    ----------
447    cursor :
448      the cursor to check
449
450    Returns
451    -------
452    is_internal :
453      True if `cursor` has internal linkage, False otherwise
454    internal_attr_src :
455      the raw text of the internal linkage attribute
456    internal_cursor :
457      the cursor corresponding to the internal linkage designation
458    """
459    def check() -> tuple[bool, str, Optional[clx.Cursor]]:
460      if cursor.linkage == clx.LinkageKind.INTERNAL:
461        # is a static function or variable
462        return True, cursor.storage_class.name, cursor.get_definition()
463
464      hidden_visibility = {'hidden', 'protected'}
465      for child in cursor.get_children():
466        if child.kind.is_attribute() and child.spelling in hidden_visibility:
467          # is PETSC_INTERN
468          return True, SourceRange(child.extent).raw(tight=True), child
469      return False, '', None
470
471    if isinstance(cursor, cls):
472      return cursor._get_cached('internal_linkage', check)
473    return check()
474
475  def has_internal_linkage(self) -> tuple[bool, str, Optional[clx.Cursor]]:
476    r"""See `Cursor.has_internal_linkage_from_cursor()`"""
477    return self.has_internal_linkage_from_cursor(self)
478
479  @staticmethod
480  def get_raw_source_from_cursor(cursor: CursorLike, **kwargs) -> str:
481    r"""Get the raw source from a `cursor`
482
483    Parameters
484    ----------
485    cursor :
486      the cursor to get from
487    **kwargs : dict
488      additional keyword arguments to `petsclinter.classes._util.get_raw_source_from_cursor()`
489
490    Returns
491    -------
492    src :
493      the raw source
494    """
495    return _util.get_raw_source_from_cursor(cursor, **kwargs)
496
497  def raw(self, **kwargs) -> str:
498    r"""See `Cursor.get_raw_source_from_cursor()`"""
499    return self.get_raw_source_from_cursor(self, **kwargs)
500
501  @classmethod
502  def get_formatted_source_from_cursor(cls, cursor: CursorLike, **kwargs) -> str:
503    r"""Get the formatted source from a `cursor`
504
505    Parameters
506    ----------
507    cursor :
508      the cursor to get from
509    **kwargs : dict
510      additional keyword arguments to `petsclinter.classes._util.get_formatted_source_from_cursor()`
511
512    Returns
513    -------
514    src :
515      the formatted source
516    """
517    # __extent_final attribute set in getIncludedFileFromCursor() since the translation
518    # unit is wrong!
519    extent = cursor.extent
520    if cursor.kind == clx.CursorKind.FUNCTION_DECL:
521      if not isinstance(cursor, cls) or not cursor._cache.get('__extent_final'):
522        begin  = extent.start
523        # -1 gives you EOL
524        fnline = SourceLocation.from_position(cursor.translation_unit, begin.line, -1)
525        extent = SourceRange.from_locations(begin, fnline)
526    return _util.get_formatted_source_from_source_range(extent, **kwargs)
527
528  def formatted(self, **kwargs) -> str:
529    r"""See `Cursor.get_formatted_source_from_cursor()`"""
530    return self.get_formatted_source_from_cursor(self, **kwargs)
531
532  def view(self, **kwargs) -> None:
533    r"""View a `Cursor`
534
535    Parameters
536    ----------
537    **kwargs :
538      keyword arguments to pass to `Cursor.formatted()`
539    """
540    import petsclinter as pl
541
542    kwargs.setdefault('num_context', 5)
543    pl.sync_print(self.formatted(**kwargs))
544    return
545
546  @classmethod
547  def get_formatted_location_string_from_cursor(cls, cursor: CursorLike) -> str:
548    r"""Return the file:func:line for `cursor`
549
550    Parameters
551    ----------
552    cursor :
553      the cursor to get it from
554
555    Returns
556    locstr :
557      the location string
558    """
559    loc = cursor.location
560    if isinstance(loc, SourceLocation):
561      return str(loc)
562    return f'{cls.get_file_from_cursor(cursor)}:{loc.line}:{loc.column}'
563
564  def get_formatted_location_string(self) -> str:
565    r"""See `Cursor.get_formatted_location_string_from_cursor()`"""
566    return self.get_formatted_location_string_from_cursor(self)
567
568  @classmethod
569  def get_formatted_blurb_from_cursor(cls, cursor: CursorLike, **kwargs) -> str:
570    r"""Get a formatted blurb for `cursor` suitable for viewing
571
572    Parameters
573    ----------
574    cursor :
575      the cursor
576    **kwargs :
577      additional keyword arguments to pass to `Cursor.formatted()`
578
579    Returns
580    -------
581    blurb :
582      the formatted blurb
583    """
584    kwargs.setdefault('num_context', 2)
585    cursor   = cls.cast(cursor)
586    aka_mess = '' if cursor.typename == cursor.derivedtypename else f' (a.k.a. \'{cursor.typename}\')'
587    return f'\'{cursor.name}\' of type \'{cursor.derivedtypename}\'{aka_mess}\n{cursor.formatted(**kwargs)}'
588
589  def get_formatted_blurb(self, **kwargs) -> str:
590    r"""See `Cursor.get_formatted_blurb_from_cursor()`"""
591    return self.get_formatted_blurb_from_cursor(self, **kwargs)
592
593  @staticmethod
594  def view_ast_from_cursor(cursor: CursorLike) -> None:
595    r"""View the AST for a cursor
596
597    Parameters
598    ----------
599    cursor :
600      the cursor to view
601
602    Notes
603    -----
604    Shows a lot of useful information, but is unsuitable for showing the user. Essentially a developer
605    debug tool
606    """
607    import petsclinter as pl
608
609    pl.sync_print('\n'.join(_util.view_ast_from_cursor(cursor)))
610    return
611
612  def view_ast(self) -> None:
613    r"""See `Cursor.view_ast_from_cursor()`"""
614    self.view_ast_from_cursor(self)
615    return
616
617  @classmethod
618  def find_cursor_references_from_cursor(cls, cursor: CursorLike) -> list[Cursor]:
619    r"""Brute force find and collect all references in a file that pertain to a particular
620    cursor.
621
622    Essentially refers to finding every reference to the symbol that the cursor represents, so
623    this function is only useful for first-class symbols (i.e. variables, functions)
624
625    Parameters
626    ----------
627    cursor :
628      the cursor to search for references
629
630    Returns
631    -------
632    found_cursors :
633      a list of references to the cursor in the file
634    """
635    cx_callback, found_cursors = make_cxcursor_and_range_callback(cursor)
636    get_clang_function(
637      'clang_findReferencesInFile', [clx.Cursor, clx.File, PetscCXCursorAndRangeVisitor]
638    )(cls.get_clang_cursor_from_cursor(cursor), cls.get_clang_file_from_cursor(cursor), cx_callback)
639    return found_cursors
640
641  def find_cursor_references(self) -> list[Cursor]:
642    r"""See `Cursor.find_cursor_references_from_cursor()`"""
643    return self.find_cursor_references_from_cursor(self)
644
645  @classmethod
646  def get_comment_and_range_from_cursor(cls, cursor: CursorLike) -> tuple[str, clx.SourceRange]:
647    r"""Get the docstring comment and its source range from a cursor
648
649    Parameters
650    ----------
651    cursor :
652      the cursor to get it from
653
654    Returns
655    -------
656    raw_comment :
657      the raw comment text
658    cursor_range :
659      the source range for the comment
660    """
661    cursor_range = get_clang_function('clang_Cursor_getCommentRange', [clx.Cursor], clx.SourceRange)(
662      cls.get_clang_cursor_from_cursor(cursor)
663    )
664    raw_comment = cursor.raw_comment
665    if raw_comment is None:
666      raw_comment = ''
667    return raw_comment, cursor_range
668
669  def get_comment_and_range(self) -> tuple[str, clx.SourceRange]:
670    r"""See `Cursor.get_comment_and_range_from_cursor()`"""
671    return self.get_comment_and_range_from_cursor(self)
672
673  @classmethod
674  def get_clang_file_from_cursor(cls, cursor: Union[CursorLike, clx.TranslationUnit]) -> clx.File:
675    r"""Get the `clang.cindex.File` from a cursor
676
677    Parameters
678    ----------
679    cursor :
680      the cursor
681
682    Returns
683    -------
684    clx_file :
685      the `clang.cindex.File` object
686
687    Raises
688    ------
689    ValueError
690      if `cursor` is not one of `Cursor`, `clang.cindex.Cursor` or a `clang.cindex.TranslationUnit`
691
692    Notes
693    -----
694    Instantiating one of these files is for whatever reason stupidly expensive, and does not appear to
695    be cached by clang at all, so this function serves to cache that
696    """
697    if isinstance(cursor, cls):
698      return TYPE_CAST(clx.File, cursor._get_cached('file', lambda c: c.location.file, cursor))
699    if isinstance(cursor, clx.Cursor):
700      return cursor.location.file
701    if isinstance(cursor, clx.TranslationUnit):
702      return clx.File.from_name(cursor, cursor.spelling)
703    raise ValueError(type(cursor))
704
705  @classmethod
706  def get_file_from_cursor(cls, cursor: Union[CursorLike, clx.TranslationUnit]) -> Path:
707    r"""See `Cursor.get_clang_file_from_cursor()`"""
708    return Path(str(cls.get_clang_file_from_cursor(cursor)))
709
710  def get_file(self) -> Path:
711    r"""See `Cursor.get_file_from_cursor()`"""
712    return self.get_file_from_cursor(self)
713
714  @staticmethod
715  def is_variadic_function_from_cursor(cursor: CursorLike) -> bool:
716    r"""Answers the question 'is this cursor variadic'?
717
718    Parameters
719    ----------
720    cursor :
721      the cursor
722
723    Returns
724    -------
725    variadic :
726      True if `cursor` is a variadic function, False otherwise
727    """
728    return TYPE_CAST(bool, cursor.type.is_function_variadic())
729
730  def is_variadic_function(self) -> bool:
731    r"""See `Cursor.is_variadic_function_from_cursor()`"""
732    return self.is_variadic_function_from_cursor(self)
733
734  @classmethod
735  def get_declaration_from_cursor(cls, cursor: CursorLike) -> Cursor:
736    r"""Get the declaration cursor for a cursor
737
738    Parameters
739    ----------
740    cursor :
741      the cursor
742
743    Returns
744    -------
745    decl :
746      The original declaration of the cursor
747
748    Notes
749    -----
750    I don't believe this fully works yet
751    """
752    if cursor.type.kind in clx_function_type_kinds:
753      cursor_file = cls.get_clang_file_from_cursor(cursor)
754      canon       = cursor.canonical
755      if canon.location.file != cursor_file:
756        return Cursor.cast(canon)
757      for child in canon.get_children():
758        if child.kind.is_attribute() and child.location.file != cursor_file:
759          if refs := cls.find_cursor_references_from_cursor(child):
760            assert len(refs) == 1, 'Don\'t know how to handle >1 ref!'
761            return Cursor.cast(refs[0])
762    return TYPE_CAST(Cursor, cursor)
763
764  def get_declaration(self) -> Cursor:
765    r"""See `Cursor.get_declaration_from_cursor()`"""
766    return self.get_declaration_from_cursor(self)
767
768  @classmethod
769  def get_clang_cursor_from_cursor(cls, cursor: CursorLike) -> clx.Cursor:
770    r"""Given a cursor, return the underlying clang cursor
771
772    Parameters
773    ----------
774    cursor :
775      the cursor
776
777    Returns
778    -------
779    clang_cursor :
780      the `clang.cindex.Cursor`
781
782    Raises
783    ------
784    ValueError
785      if `cursor` is not a `Cursor` or `clang.cindex.Cursor`
786    """
787    if isinstance(cursor, cls):
788      return cursor.__cursor
789    if isinstance(cursor, clx.Cursor):
790      return cursor
791    raise ValueError(type(cursor))
792
793  def clang_cursor(self) -> clx.Cursor:
794    r"""See `Cursor.get_clang_cursor_from_cursor()`"""
795    return self.get_clang_cursor_from_cursor(self)
796