Author: Jannick Kremer
Date: 2025-05-16T10:11:03+02:00
New Revision: c2045f24eab06960e0418d7d82856407b19156ad

URL: 
https://github.com/llvm/llvm-project/commit/c2045f24eab06960e0418d7d82856407b19156ad
DIFF: 
https://github.com/llvm/llvm-project/commit/c2045f24eab06960e0418d7d82856407b19156ad.diff

LOG: [libclang/python] Add typing annotations for the Cursor class (#138103)

This fully annotates the Cursor class, resolving 95 strict typing errors
as the next step towards #76664

These changes are a superset of the typing annotation changes from
#120590

Added: 
    

Modified: 
    clang/bindings/python/clang/cindex.py
    clang/bindings/python/tests/cindex/test_cursor.py
    clang/docs/ReleaseNotes.rst

Removed: 
    


################################################################################
diff  --git a/clang/bindings/python/clang/cindex.py 
b/clang/bindings/python/clang/cindex.py
index 7a10f5618aad0..a49441e815004 100644
--- a/clang/bindings/python/clang/cindex.py
+++ b/clang/bindings/python/clang/cindex.py
@@ -90,6 +90,7 @@
     Callable,
     cast as Tcast,
     Generic,
+    Iterator,
     Optional,
     Sequence,
     Type as TType,
@@ -1561,6 +1562,24 @@ class ExceptionSpecificationKind(BaseEnumeration):
 ### Cursors ###
 
 
+def cursor_null_guard(func):
+    """
+    This decorator is used to ensure that no methods are called on 
null-cursors.
+    The bindings map null cursors to `None`, so users are not expected
+    to encounter them.
+
+    If necessary, you can check whether a cursor is the null-cursor by
+    calling its `is_null` method.
+    """
+
+    def inner(self, *args, **kwargs):
+        if self.is_null():
+            raise Exception("Tried calling method on a null-cursor.")
+        return func(self, *args, **kwargs)
+
+    return inner
+
+
 class Cursor(Structure):
     """
     The Cursor class represents a reference to an element within the AST. It
@@ -1569,68 +1588,81 @@ class Cursor(Structure):
 
     _fields_ = [("_kind_id", c_int), ("xdata", c_int), ("data", c_void_p * 3)]
 
-    @staticmethod
-    def from_location(tu, location):
-        # We store a reference to the TU in the instance so the TU won't get
-        # collected before the cursor.
-        cursor = conf.lib.clang_getCursor(tu, location)
-        cursor._tu = tu
+    _tu: TranslationUnit
 
-        return cursor
+    @staticmethod
+    def from_location(tu: TranslationUnit, location: SourceLocation) -> Cursor 
| None:
+        return Cursor.from_result(conf.lib.clang_getCursor(tu, location), tu)
 
-    def __eq__(self, other):
+    # This function is not null-guarded because it is used in 
cursor_null_guard itself
+    def __eq__(self, other: object) -> bool:
         if not isinstance(other, Cursor):
             return False
         return conf.lib.clang_equalCursors(self, other)  # type: ignore 
[no-any-return]
 
-    def __ne__(self, other):
+    # Not null-guarded for consistency with __eq__
+    def __ne__(self, other: object) -> bool:
         return not self.__eq__(other)
 
+    @cursor_null_guard
     def __hash__(self) -> int:
         return self.hash
 
-    def is_definition(self):
+    # This function is not null-guarded because it is used in 
cursor_null_guard itself
+    def is_null(self) -> bool:
+        return self == conf.null_cursor
+
+    @cursor_null_guard
+    def is_definition(self) -> bool:
         """
         Returns true if the declaration pointed at by the cursor is also a
         definition of that entity.
         """
         return conf.lib.clang_isCursorDefinition(self)  # type: ignore 
[no-any-return]
 
-    def is_const_method(self):
+    @cursor_null_guard
+    def is_const_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared 'const'.
         """
         return conf.lib.clang_CXXMethod_isConst(self)  # type: ignore 
[no-any-return]
 
-    def is_converting_constructor(self):
+    @cursor_null_guard
+    def is_converting_constructor(self) -> bool:
         """Returns True if the cursor refers to a C++ converting 
constructor."""
         return conf.lib.clang_CXXConstructor_isConvertingConstructor(self)  # 
type: ignore [no-any-return]
 
-    def is_copy_constructor(self):
+    @cursor_null_guard
+    def is_copy_constructor(self) -> bool:
         """Returns True if the cursor refers to a C++ copy constructor."""
         return conf.lib.clang_CXXConstructor_isCopyConstructor(self)  # type: 
ignore [no-any-return]
 
-    def is_default_constructor(self):
+    @cursor_null_guard
+    def is_default_constructor(self) -> bool:
         """Returns True if the cursor refers to a C++ default constructor."""
         return conf.lib.clang_CXXConstructor_isDefaultConstructor(self)  # 
type: ignore [no-any-return]
 
-    def is_move_constructor(self):
+    @cursor_null_guard
+    def is_move_constructor(self) -> bool:
         """Returns True if the cursor refers to a C++ move constructor."""
         return conf.lib.clang_CXXConstructor_isMoveConstructor(self)  # type: 
ignore [no-any-return]
 
-    def is_default_method(self):
+    @cursor_null_guard
+    def is_default_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared '= default'.
         """
         return conf.lib.clang_CXXMethod_isDefaulted(self)  # type: ignore 
[no-any-return]
 
-    def is_deleted_method(self):
+    @cursor_null_guard
+    def is_deleted_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared '= delete'.
         """
         return conf.lib.clang_CXXMethod_isDeleted(self)  # type: ignore 
[no-any-return]
 
-    def is_copy_assignment_operator_method(self):
+    @cursor_null_guard
+    def is_copy_assignment_operator_method(self) -> bool:
         """Returnrs True if the cursor refers to a copy-assignment operator.
 
         A copy-assignment operator `X::operator=` is a non-static,
@@ -1655,7 +1687,8 @@ class Bar {
         """
         return conf.lib.clang_CXXMethod_isCopyAssignmentOperator(self)  # 
type: ignore [no-any-return]
 
-    def is_move_assignment_operator_method(self):
+    @cursor_null_guard
+    def is_move_assignment_operator_method(self) -> bool:
         """Returnrs True if the cursor refers to a move-assignment operator.
 
         A move-assignment operator `X::operator=` is a non-static,
@@ -1680,7 +1713,8 @@ class Bar {
         """
         return conf.lib.clang_CXXMethod_isMoveAssignmentOperator(self)  # 
type: ignore [no-any-return]
 
-    def is_explicit_method(self):
+    @cursor_null_guard
+    def is_explicit_method(self) -> bool:
         """Determines if a C++ constructor or conversion function is
         explicit, returning 1 if such is the case and 0 otherwise.
 
@@ -1725,41 +1759,48 @@ class Foo {
         """
         return conf.lib.clang_CXXMethod_isExplicit(self)  # type: ignore 
[no-any-return]
 
-    def is_mutable_field(self):
+    @cursor_null_guard
+    def is_mutable_field(self) -> bool:
         """Returns True if the cursor refers to a C++ field that is declared
         'mutable'.
         """
         return conf.lib.clang_CXXField_isMutable(self)  # type: ignore 
[no-any-return]
 
-    def is_pure_virtual_method(self):
+    @cursor_null_guard
+    def is_pure_virtual_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared pure virtual.
         """
         return conf.lib.clang_CXXMethod_isPureVirtual(self)  # type: ignore 
[no-any-return]
 
-    def is_static_method(self):
+    @cursor_null_guard
+    def is_static_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared 'static'.
         """
         return conf.lib.clang_CXXMethod_isStatic(self)  # type: ignore 
[no-any-return]
 
-    def is_virtual_method(self):
+    @cursor_null_guard
+    def is_virtual_method(self) -> bool:
         """Returns True if the cursor refers to a C++ member function or member
         function template that is declared 'virtual'.
         """
         return conf.lib.clang_CXXMethod_isVirtual(self)  # type: ignore 
[no-any-return]
 
-    def is_abstract_record(self):
+    @cursor_null_guard
+    def is_abstract_record(self) -> bool:
         """Returns True if the cursor refers to a C++ record declaration
         that has pure virtual member functions.
         """
         return conf.lib.clang_CXXRecord_isAbstract(self)  # type: ignore 
[no-any-return]
 
-    def is_scoped_enum(self):
+    @cursor_null_guard
+    def is_scoped_enum(self) -> bool:
         """Returns True if the cursor refers to a scoped enum declaration."""
         return conf.lib.clang_EnumDecl_isScoped(self)  # type: ignore 
[no-any-return]
 
-    def get_definition(self):
+    @cursor_null_guard
+    def get_definition(self) -> Cursor | None:
         """
         If the cursor is a reference to a declaration or a declaration of
         some entity, return a cursor that points to the definition of that
@@ -1769,7 +1810,8 @@ def get_definition(self):
         # declaration prior to issuing the lookup.
         return Cursor.from_result(conf.lib.clang_getCursorDefinition(self), 
self)
 
-    def get_usr(self):
+    @cursor_null_guard
+    def get_usr(self) -> str:
         """Return the Unified Symbol Resolution (USR) for the entity referenced
         by the given cursor.
 
@@ -1780,19 +1822,22 @@ def get_usr(self):
         another translation unit."""
         return _CXString.from_result(conf.lib.clang_getCursorUSR(self))
 
-    def get_included_file(self):
+    @cursor_null_guard
+    def get_included_file(self) -> File:
         """Returns the File that is included by the current inclusion 
cursor."""
         assert self.kind == CursorKind.INCLUSION_DIRECTIVE
 
         return File.from_result(conf.lib.clang_getIncludedFile(self), self)
 
     @property
-    def kind(self):
+    @cursor_null_guard
+    def kind(self) -> CursorKind:
         """Return the kind of this cursor."""
         return CursorKind.from_id(self._kind_id)
 
     @property
-    def spelling(self):
+    @cursor_null_guard
+    def spelling(self) -> str:
         """Return the spelling of the entity pointed at by the cursor."""
         if not hasattr(self, "_spelling"):
             self._spelling = _CXString.from_result(
@@ -1801,7 +1846,8 @@ def spelling(self):
 
         return self._spelling
 
-    def pretty_printed(self, policy):
+    @cursor_null_guard
+    def pretty_printed(self, policy: PrintingPolicy) -> str:
         """
         Pretty print declarations.
         Parameters:
@@ -1812,7 +1858,8 @@ def pretty_printed(self, policy):
         )
 
     @property
-    def displayname(self):
+    @cursor_null_guard
+    def displayname(self) -> str:
         """
         Return the display name for the entity referenced by this cursor.
 
@@ -1828,7 +1875,8 @@ def displayname(self):
         return self._displayname
 
     @property
-    def mangled_name(self):
+    @cursor_null_guard
+    def mangled_name(self) -> str:
         """Return the mangled name for the entity referenced by this cursor."""
         if not hasattr(self, "_mangled_name"):
             self._mangled_name = _CXString.from_result(
@@ -1838,18 +1886,20 @@ def mangled_name(self):
         return self._mangled_name
 
     @property
-    def location(self):
+    @cursor_null_guard
+    def location(self) -> SourceLocation:
         """
         Return the source location (the starting character) of the entity
         pointed at by the cursor.
         """
         if not hasattr(self, "_loc"):
-            self._loc = conf.lib.clang_getCursorLocation(self)
+            self._loc: SourceLocation = conf.lib.clang_getCursorLocation(self)
 
         return self._loc
 
     @property
-    def linkage(self):
+    @cursor_null_guard
+    def linkage(self) -> LinkageKind:
         """Return the linkage of this cursor."""
         if not hasattr(self, "_linkage"):
             self._linkage = conf.lib.clang_getCursorLinkage(self)
@@ -1857,7 +1907,8 @@ def linkage(self):
         return LinkageKind.from_id(self._linkage)
 
     @property
-    def tls_kind(self):
+    @cursor_null_guard
+    def tls_kind(self) -> TLSKind:
         """Return the thread-local storage (TLS) kind of this cursor."""
         if not hasattr(self, "_tls_kind"):
             self._tls_kind = conf.lib.clang_getCursorTLSKind(self)
@@ -1865,18 +1916,20 @@ def tls_kind(self):
         return TLSKind.from_id(self._tls_kind)
 
     @property
-    def extent(self):
+    @cursor_null_guard
+    def extent(self) -> SourceRange:
         """
         Return the source range (the range of text) occupied by the entity
         pointed at by the cursor.
         """
         if not hasattr(self, "_extent"):
-            self._extent = conf.lib.clang_getCursorExtent(self)
+            self._extent: SourceRange = conf.lib.clang_getCursorExtent(self)
 
         return self._extent
 
     @property
-    def storage_class(self):
+    @cursor_null_guard
+    def storage_class(self) -> StorageClass:
         """
         Retrieves the storage class (if any) of the entity pointed at by the
         cursor.
@@ -1887,7 +1940,8 @@ def storage_class(self):
         return StorageClass.from_id(self._storage_class)
 
     @property
-    def availability(self):
+    @cursor_null_guard
+    def availability(self) -> AvailabilityKind:
         """
         Retrieves the availability of the entity pointed at by the cursor.
         """
@@ -1897,7 +1951,8 @@ def availability(self):
         return AvailabilityKind.from_id(self._availability)
 
     @property
-    def binary_operator(self):
+    @cursor_null_guard
+    def binary_operator(self) -> BinaryOperator:
         """
         Retrieves the opcode if this cursor points to a binary operator
         :return:
@@ -1909,7 +1964,8 @@ def binary_operator(self):
         return BinaryOperator.from_id(self._binopcode)
 
     @property
-    def access_specifier(self):
+    @cursor_null_guard
+    def access_specifier(self) -> AccessSpecifier:
         """
         Retrieves the access specifier (if any) of the entity pointed at by the
         cursor.
@@ -1920,7 +1976,8 @@ def access_specifier(self):
         return AccessSpecifier.from_id(self._access_specifier)
 
     @property
-    def type(self):
+    @cursor_null_guard
+    def type(self) -> Type:
         """
         Retrieve the Type (if any) of the entity pointed at by the cursor.
         """
@@ -1930,7 +1987,8 @@ def type(self):
         return self._type
 
     @property
-    def canonical(self):
+    @cursor_null_guard
+    def canonical(self) -> Cursor:
         """Return the canonical Cursor corresponding to this Cursor.
 
         The canonical cursor is the cursor which is representative for the
@@ -1939,14 +1997,15 @@ def canonical(self):
         declarations will be identical.
         """
         if not hasattr(self, "_canonical"):
-            self._canonical = Cursor.from_cursor_result(
+            self._canonical = Cursor.from_non_null_cursor_result(
                 conf.lib.clang_getCanonicalCursor(self), self
             )
 
         return self._canonical
 
     @property
-    def result_type(self):
+    @cursor_null_guard
+    def result_type(self) -> Type:
         """Retrieve the Type of the result for this Cursor."""
         if not hasattr(self, "_result_type"):
             self._result_type = Type.from_result(
@@ -1956,7 +2015,8 @@ def result_type(self):
         return self._result_type
 
     @property
-    def exception_specification_kind(self):
+    @cursor_null_guard
+    def exception_specification_kind(self) -> ExceptionSpecificationKind:
         """
         Retrieve the exception specification kind, which is one of the values
         from the ExceptionSpecificationKind enumeration.
@@ -1970,7 +2030,8 @@ def exception_specification_kind(self):
         return self._exception_specification_kind
 
     @property
-    def underlying_typedef_type(self):
+    @cursor_null_guard
+    def underlying_typedef_type(self) -> Type:
         """Return the underlying type of a typedef declaration.
 
         Returns a Type for the typedef this cursor is a declaration for. If
@@ -1985,7 +2046,8 @@ def underlying_typedef_type(self):
         return self._underlying_type
 
     @property
-    def enum_type(self):
+    @cursor_null_guard
+    def enum_type(self) -> Type:
         """Return the integer type of an enum declaration.
 
         Returns a Type corresponding to an integer. If the cursor is not for an
@@ -2000,9 +2062,11 @@ def enum_type(self):
         return self._enum_type
 
     @property
-    def enum_value(self):
+    @cursor_null_guard
+    def enum_value(self) -> int:
         """Return the value of an enum constant."""
         if not hasattr(self, "_enum_value"):
+            self._enum_value: int
             assert self.kind == CursorKind.ENUM_CONSTANT_DECL
             # Figure out the underlying type of the enum to know if it
             # is a signed or unsigned quantity.
@@ -2026,7 +2090,8 @@ def enum_value(self):
         return self._enum_value
 
     @property
-    def objc_type_encoding(self):
+    @cursor_null_guard
+    def objc_type_encoding(self) -> str:
         """Return the Objective-C type encoding as a str."""
         if not hasattr(self, "_objc_type_encoding"):
             self._objc_type_encoding = _CXString.from_result(
@@ -2036,15 +2101,17 @@ def objc_type_encoding(self):
         return self._objc_type_encoding
 
     @property
-    def hash(self):
+    @cursor_null_guard
+    def hash(self) -> int:
         """Returns a hash of the cursor as an int."""
         if not hasattr(self, "_hash"):
-            self._hash = conf.lib.clang_hashCursor(self)
+            self._hash: int = conf.lib.clang_hashCursor(self)
 
         return self._hash
 
     @property
-    def semantic_parent(self):
+    @cursor_null_guard
+    def semantic_parent(self) -> Cursor | None:
         """Return the semantic parent for this cursor."""
         if not hasattr(self, "_semantic_parent"):
             self._semantic_parent = Cursor.from_cursor_result(
@@ -2054,7 +2121,8 @@ def semantic_parent(self):
         return self._semantic_parent
 
     @property
-    def lexical_parent(self):
+    @cursor_null_guard
+    def lexical_parent(self) -> Cursor | None:
         """Return the lexical parent for this cursor."""
         if not hasattr(self, "_lexical_parent"):
             self._lexical_parent = Cursor.from_cursor_result(
@@ -2064,6 +2132,7 @@ def lexical_parent(self):
         return self._lexical_parent
 
     @property
+    @cursor_null_guard
     def specialized_template(self) -> Cursor | None:
         """Return the primary template that this cursor is a specialization 
of, if any."""
         return Cursor.from_cursor_result(
@@ -2071,14 +2140,16 @@ def specialized_template(self) -> Cursor | None:
         )
 
     @property
-    def translation_unit(self):
+    @cursor_null_guard
+    def translation_unit(self) -> TranslationUnit:
         """Returns the TranslationUnit to which this Cursor belongs."""
         # If this triggers an AttributeError, the instance was not properly
         # created.
         return self._tu
 
     @property
-    def referenced(self):
+    @cursor_null_guard
+    def referenced(self) -> Cursor | None:
         """
         For a cursor that is a reference, returns a cursor
         representing the entity that it references.
@@ -2091,54 +2162,62 @@ def referenced(self):
         return self._referenced
 
     @property
-    def brief_comment(self):
+    @cursor_null_guard
+    def brief_comment(self) -> str:
         """Returns the brief comment text associated with that Cursor"""
         return 
_CXString.from_result(conf.lib.clang_Cursor_getBriefCommentText(self))
 
     @property
-    def raw_comment(self):
+    @cursor_null_guard
+    def raw_comment(self) -> str:
         """Returns the raw comment text associated with that Cursor"""
         return 
_CXString.from_result(conf.lib.clang_Cursor_getRawCommentText(self))
 
-    def get_arguments(self):
+    @cursor_null_guard
+    def get_arguments(self) -> Iterator[Cursor | None]:
         """Return an iterator for accessing the arguments of this cursor."""
         num_args = conf.lib.clang_Cursor_getNumArguments(self)
         for i in range(0, num_args):
             yield Cursor.from_result(conf.lib.clang_Cursor_getArgument(self, 
i), self)
 
-    def get_num_template_arguments(self):
+    @cursor_null_guard
+    def get_num_template_arguments(self) -> int:
         """Returns the number of template args associated with this cursor."""
         return conf.lib.clang_Cursor_getNumTemplateArguments(self)  # type: 
ignore [no-any-return]
 
-    def get_template_argument_kind(self, num):
+    @cursor_null_guard
+    def get_template_argument_kind(self, num: int) -> TemplateArgumentKind:
         """Returns the TemplateArgumentKind for the indicated template
         argument."""
         return TemplateArgumentKind.from_id(
             conf.lib.clang_Cursor_getTemplateArgumentKind(self, num)
         )
 
-    def get_template_argument_type(self, num):
+    @cursor_null_guard
+    def get_template_argument_type(self, num: int) -> Type:
         """Returns the CXType for the indicated template argument."""
         return Type.from_result(
             conf.lib.clang_Cursor_getTemplateArgumentType(self, num), (self, 
num)
         )
 
-    def get_template_argument_value(self, num):
+    @cursor_null_guard
+    def get_template_argument_value(self, num: int) -> int:
         """Returns the value of the indicated arg as a signed 64b integer."""
         return conf.lib.clang_Cursor_getTemplateArgumentValue(self, num)  # 
type: ignore [no-any-return]
 
-    def get_template_argument_unsigned_value(self, num):
+    @cursor_null_guard
+    def get_template_argument_unsigned_value(self, num: int) -> int:
         """Returns the value of the indicated arg as an unsigned 64b 
integer."""
         return conf.lib.clang_Cursor_getTemplateArgumentUnsignedValue(self, 
num)  # type: ignore [no-any-return]
 
-    def get_children(self):
+    @cursor_null_guard
+    def get_children(self) -> Iterator[Cursor]:
         """Return an iterator for accessing the children of this cursor."""
 
         # FIXME: Expose iteration from CIndex, PR6125.
-        def visitor(child, parent, children):
+        def visitor(child: Cursor, _: Cursor, children: list[Cursor]) -> int:
             # FIXME: Document this assertion in API.
-            # FIXME: There should just be an isNull method.
-            assert child != conf.lib.clang_getNullCursor()
+            assert not child.is_null()
 
             # Create reference to TU so it isn't GC'd before Cursor.
             child._tu = self._tu
@@ -2149,7 +2228,8 @@ def visitor(child, parent, children):
         conf.lib.clang_visitChildren(self, cursor_visit_callback(visitor), 
children)
         return iter(children)
 
-    def walk_preorder(self):
+    @cursor_null_guard
+    def walk_preorder(self) -> Iterator[Cursor]:
         """Depth-first preorder walk over the cursor and its descendants.
 
         Yields cursors.
@@ -2159,7 +2239,8 @@ def walk_preorder(self):
             for descendant in child.walk_preorder():
                 yield descendant
 
-    def get_tokens(self):
+    @cursor_null_guard
+    def get_tokens(self) -> Iterator[Token]:
         """Obtain Token instances formulating that compose this Cursor.
 
         This is a generator for Token instances. It returns all tokens which
@@ -2167,19 +2248,23 @@ def get_tokens(self):
         """
         return TokenGroup.get_tokens(self._tu, self.extent)
 
-    def get_field_offsetof(self):
+    @cursor_null_guard
+    def get_field_offsetof(self) -> int:
         """Returns the offsetof the FIELD_DECL pointed by this Cursor."""
         return conf.lib.clang_Cursor_getOffsetOfField(self)  # type: ignore 
[no-any-return]
 
-    def get_base_offsetof(self, parent):
+    @cursor_null_guard
+    def get_base_offsetof(self, parent: Cursor) -> int:
         """Returns the offsetof the CXX_BASE_SPECIFIER pointed by this 
Cursor."""
         return conf.lib.clang_getOffsetOfBase(parent, self)  # type: ignore 
[no-any-return]
 
-    def is_virtual_base(self):
+    @cursor_null_guard
+    def is_virtual_base(self) -> bool:
         """Returns whether the CXX_BASE_SPECIFIER pointed by this Cursor is 
virtual."""
         return conf.lib.clang_isVirtualBase(self)  # type: ignore 
[no-any-return]
 
-    def is_anonymous(self):
+    @cursor_null_guard
+    def is_anonymous(self) -> bool:
         """
         Check whether this is a record type without a name, or a field where
         the type is a record type without a name.
@@ -2191,7 +2276,8 @@ def is_anonymous(self):
             return self.type.get_declaration().is_anonymous()
         return conf.lib.clang_Cursor_isAnonymous(self)  # type: ignore 
[no-any-return]
 
-    def is_anonymous_record_decl(self):
+    @cursor_null_guard
+    def is_anonymous_record_decl(self) -> bool:
         """
         Check if the record is an anonymous union as defined in the C/C++ 
standard
         (or an "anonymous struct", the corresponding non-standard extension for
@@ -2201,18 +2287,21 @@ def is_anonymous_record_decl(self):
             return self.type.get_declaration().is_anonymous_record_decl()
         return conf.lib.clang_Cursor_isAnonymousRecordDecl(self)  # type: 
ignore [no-any-return]
 
-    def is_bitfield(self):
+    @cursor_null_guard
+    def is_bitfield(self) -> bool:
         """
         Check if the field is a bitfield.
         """
         return conf.lib.clang_Cursor_isBitField(self)  # type: ignore 
[no-any-return]
 
-    def get_bitfield_width(self):
+    @cursor_null_guard
+    def get_bitfield_width(self) -> int:
         """
         Retrieve the width of a bitfield.
         """
         return conf.lib.clang_getFieldDeclBitWidth(self)  # type: ignore 
[no-any-return]
 
+    @cursor_null_guard
     def has_attrs(self) -> bool:
         """
         Determine whether the given cursor has any attributes.
@@ -2220,10 +2309,9 @@ def has_attrs(self) -> bool:
         return bool(conf.lib.clang_Cursor_hasAttrs(self))
 
     @staticmethod
-    def from_result(res, arg):
+    def from_result(res: Cursor, arg: Cursor | TranslationUnit | Type) -> 
Cursor | None:
         assert isinstance(res, Cursor)
-        # FIXME: There should just be an isNull method.
-        if res == conf.lib.clang_getNullCursor():
+        if res.is_null():
             return None
 
         # Store a reference to the TU in the Python object so it won't get GC'd
@@ -2240,14 +2328,22 @@ def from_result(res, arg):
         return res
 
     @staticmethod
-    def from_cursor_result(res, arg):
+    def from_cursor_result(res: Cursor, arg: Cursor) -> Cursor | None:
         assert isinstance(res, Cursor)
-        if res == conf.lib.clang_getNullCursor():
+        if res.is_null():
             return None
 
         res._tu = arg._tu
         return res
 
+    @staticmethod
+    def from_non_null_cursor_result(res: Cursor, arg: Cursor | Type) -> Cursor:
+        assert isinstance(res, Cursor)
+        assert not res.is_null()
+
+        res._tu = arg._tu
+        return res
+
 
 class BinaryOperator(BaseEnumeration):
     """
@@ -2681,7 +2777,9 @@ def get_declaration(self):
         """
         Return the cursor for the declaration of the given type.
         """
-        return Cursor.from_result(conf.lib.clang_getTypeDeclaration(self), 
self)
+        return Cursor.from_non_null_cursor_result(
+            conf.lib.clang_getTypeDeclaration(self), self
+        )
 
     def get_result(self):
         """
@@ -2741,7 +2839,7 @@ def get_fields(self):
         """Return an iterator for accessing the fields of this type."""
 
         def visitor(field, children):
-            assert field != conf.lib.clang_getNullCursor()
+            assert not field.is_null()
 
             # Create reference to TU so it isn't GC'd before Cursor.
             field._tu = self._tu
@@ -2756,7 +2854,7 @@ def get_bases(self):
         """Return an iterator for accessing the base classes of this type."""
 
         def visitor(base, children):
-            assert base != conf.lib.clang_getNullCursor()
+            assert not base.is_null()
 
             # Create reference to TU so it isn't GC'd before Cursor.
             base._tu = self._tu
@@ -2771,7 +2869,7 @@ def get_methods(self):
         """Return an iterator for accessing the methods of this type."""
 
         def visitor(method, children):
-            assert method != conf.lib.clang_getNullCursor()
+            assert not method.is_null()
 
             # Create reference to TU so it isn't GC'd before Cursor.
             method._tu = self._tu
@@ -4247,6 +4345,7 @@ def set_compatibility_check(check_status: bool) -> None:
     def lib(self) -> CDLL:
         lib = self.get_cindex_library()
         register_functions(lib, not Config.compatibility_check)
+        self.null_cursor = lib.clang_getNullCursor()
         Config.loaded = True
         return lib
 

diff  --git a/clang/bindings/python/tests/cindex/test_cursor.py 
b/clang/bindings/python/tests/cindex/test_cursor.py
index b90a0495ca7be..eb0d1d50601a6 100644
--- a/clang/bindings/python/tests/cindex/test_cursor.py
+++ b/clang/bindings/python/tests/cindex/test_cursor.py
@@ -12,6 +12,7 @@
     TemplateArgumentKind,
     TranslationUnit,
     TypeKind,
+    conf,
 )
 
 if "CLANG_LIBRARY_PATH" in os.environ:
@@ -1050,3 +1051,16 @@ def test_equality(self):
         self.assertEqual(cursor1, cursor1_2)
         self.assertNotEqual(cursor1, cursor2)
         self.assertNotEqual(cursor1, "foo")
+
+    def test_null_cursor(self):
+        tu = get_tu("int a = 729;")
+
+        for cursor in tu.cursor.walk_preorder():
+            self.assertFalse(cursor.is_null())
+
+        nc = conf.lib.clang_getNullCursor()
+        self.assertTrue(nc.is_null())
+        with self.assertRaises(Exception):
+            nc.is_definition()
+        with self.assertRaises(Exception):
+            nc.spelling

diff  --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index cfe2a0277b226..8896a02eb0e18 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -79,6 +79,11 @@ Clang Frontend Potentially Breaking Changes
 
 Clang Python Bindings Potentially Breaking Changes
 --------------------------------------------------
+- ``Cursor.from_location`` now returns ``None`` instead of a null cursor.
+  This eliminates the last known source of null cursors.
+- Almost all ``Cursor`` methods now assert that they are called on non-null 
cursors.
+  Most of the time null cursors were mapped to ``None``,
+  so no widespread breakages are expected.
 
 What's New in Clang |release|?
 ==============================


        
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to