Skip to content

Commit e5299ee

Browse files
committed
Add Cursor.is_null() method
Guard all Cursor methods against being used with null cursors
1 parent 41b7247 commit e5299ee

File tree

2 files changed

+36
-7
lines changed

2 files changed

+36
-7
lines changed

clang/bindings/python/clang/cindex.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,18 @@ class Cursor(Structure):
15551555

15561556
_tu: TranslationUnit
15571557

1558+
# This ensures that no operations are possible on null cursors
1559+
# by guarding all method calls with a not-null assert
1560+
def __getattribute__(self, key: str) -> object:
1561+
value = super().__getattribute__(key)
1562+
is_property = isinstance(getattr(Cursor, key, None), property)
1563+
# Don't guard the is_null method, since it is part of the guard
1564+
# and leads to infinite recursion otherwise
1565+
if is_property or callable(value):
1566+
if key != "is_null" and self.is_null():
1567+
raise Exception("Tried calling method on a null-cursor.")
1568+
return value
1569+
15581570
@staticmethod
15591571
def from_location(tu: TranslationUnit, location: SourceLocation) -> Cursor:
15601572
# We store a reference to the TU in the instance so the TU won't get
@@ -1575,6 +1587,9 @@ def __ne__(self, other: object) -> bool:
15751587
def __hash__(self) -> int:
15761588
return self.hash
15771589

1590+
def is_null(self) -> bool:
1591+
return self == conf.null_cursor
1592+
15781593
def is_definition(self) -> bool:
15791594
"""
15801595
Returns true if the declaration pointed at by the cursor is also a
@@ -2124,8 +2139,7 @@ def get_children(self) -> Iterator[Cursor]:
21242139
# FIXME: Expose iteration from CIndex, PR6125.
21252140
def visitor(child: Cursor, _: Cursor, children: list[Cursor]) -> int:
21262141
# FIXME: Document this assertion in API.
2127-
# FIXME: There should just be an isNull method.
2128-
assert child != conf.lib.clang_getNullCursor()
2142+
assert not child.is_null()
21292143

21302144
# Create reference to TU so it isn't GC'd before Cursor.
21312145
child._tu = self._tu
@@ -2210,7 +2224,7 @@ def has_attrs(self) -> bool:
22102224
def from_result(res: Cursor, arg: Cursor | TranslationUnit | Type) -> Cursor | None:
22112225
assert isinstance(res, Cursor)
22122226
# FIXME: There should just be an isNull method.
2213-
if res == conf.lib.clang_getNullCursor():
2227+
if res.is_null():
22142228
return None
22152229

22162230
# Store a reference to the TU in the Python object so it won't get GC'd
@@ -2229,7 +2243,7 @@ def from_result(res: Cursor, arg: Cursor | TranslationUnit | Type) -> Cursor | N
22292243
@staticmethod
22302244
def from_cursor_result(res: Cursor, arg: Cursor) -> Cursor | None:
22312245
assert isinstance(res, Cursor)
2232-
if res == conf.lib.clang_getNullCursor():
2246+
if res.is_null():
22332247
return None
22342248

22352249
res._tu = arg._tu
@@ -2728,7 +2742,7 @@ def get_fields(self):
27282742
"""Return an iterator for accessing the fields of this type."""
27292743

27302744
def visitor(field, children):
2731-
assert field != conf.lib.clang_getNullCursor()
2745+
assert not field.is_null()
27322746

27332747
# Create reference to TU so it isn't GC'd before Cursor.
27342748
field._tu = self._tu
@@ -2743,7 +2757,7 @@ def get_bases(self):
27432757
"""Return an iterator for accessing the base classes of this type."""
27442758

27452759
def visitor(base, children):
2746-
assert base != conf.lib.clang_getNullCursor()
2760+
assert not base.is_null()
27472761

27482762
# Create reference to TU so it isn't GC'd before Cursor.
27492763
base._tu = self._tu
@@ -2758,7 +2772,7 @@ def get_methods(self):
27582772
"""Return an iterator for accessing the methods of this type."""
27592773

27602774
def visitor(method, children):
2761-
assert method != conf.lib.clang_getNullCursor()
2775+
assert not method.is_null()
27622776

27632777
# Create reference to TU so it isn't GC'd before Cursor.
27642778
method._tu = self._tu
@@ -4228,6 +4242,7 @@ def set_compatibility_check(check_status: bool) -> None:
42284242
def lib(self) -> CDLL:
42294243
lib = self.get_cindex_library()
42304244
register_functions(lib, not Config.compatibility_check)
4245+
self.null_cursor = lib.clang_getNullCursor()
42314246
Config.loaded = True
42324247
return lib
42334248

clang/bindings/python/tests/cindex/test_cursor.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
TemplateArgumentKind,
1313
TranslationUnit,
1414
TypeKind,
15+
conf,
1516
)
1617

1718
if "CLANG_LIBRARY_PATH" in os.environ:
@@ -1050,3 +1051,16 @@ def test_equality(self):
10501051
self.assertEqual(cursor1, cursor1_2)
10511052
self.assertNotEqual(cursor1, cursor2)
10521053
self.assertNotEqual(cursor1, "foo")
1054+
1055+
def test_null_cursor(self):
1056+
tu = get_tu("int a = 729;")
1057+
1058+
for cursor in tu.cursor.walk_preorder():
1059+
self.assertFalse(cursor.is_null())
1060+
1061+
nc = conf.lib.clang_getNullCursor()
1062+
self.assertTrue(nc.is_null())
1063+
with self.assertRaises(Exception):
1064+
nc.is_definition()
1065+
with self.assertRaises(Exception):
1066+
nc.spelling

0 commit comments

Comments
 (0)