Skip to content

Commit 63beedb

Browse files
committed
Add Cursor.is_null() method
Guard all Cursor methods against being used with null cursors
1 parent 17b059d commit 63beedb

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
@@ -4232,6 +4246,7 @@ def set_compatibility_check(check_status: bool) -> None:
42324246
def lib(self) -> CDLL:
42334247
lib = self.get_cindex_library()
42344248
register_functions(lib, not Config.compatibility_check)
4249+
self.null_cursor = lib.clang_getNullCursor()
42354250
Config.loaded = True
42364251
return lib
42374252

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:
@@ -1035,3 +1036,16 @@ def test_specialized_template(self):
10351036
self.assertNotEqual(foos[0], foos[1])
10361037
self.assertEqual(foos[0], prime_foo)
10371038
self.assertIsNone(tu.cursor.specialized_template)
1039+
1040+
def test_null_cursor(self):
1041+
tu = get_tu("int a = 729;")
1042+
1043+
for cursor in tu.cursor.walk_preorder():
1044+
self.assertFalse(cursor.is_null())
1045+
1046+
nc = conf.lib.clang_getNullCursor()
1047+
self.assertTrue(nc.is_null())
1048+
with self.assertRaises(Exception):
1049+
nc.is_definition()
1050+
with self.assertRaises(Exception):
1051+
nc.spelling

0 commit comments

Comments
 (0)