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