-
Notifications
You must be signed in to change notification settings - Fork 13.4k
[libclang/python] Add typing annotations for the Cursor class #138103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@llvm/pr-subscribers-clang Author: Jannick Kremer (DeinAlptraum) ChangesPatch is 22.11 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/138103.diff 1 Files Affected:
diff --git a/clang/bindings/python/clang/cindex.py b/clang/bindings/python/clang/cindex.py
index a5227df093e73..bdff8ae9edc7b 100644
--- a/clang/bindings/python/clang/cindex.py
+++ b/clang/bindings/python/clang/cindex.py
@@ -72,6 +72,7 @@
Any,
Callable,
Generic,
+ Iterator,
Optional,
Type as TType,
TypeVar,
@@ -1546,66 +1547,68 @@ class Cursor(Structure):
_fields_ = [("_kind_id", c_int), ("xdata", c_int), ("data", c_void_p * 3)]
+ _tu: TranslationUnit
+
@staticmethod
- def from_location(tu, location):
+ def from_location(tu: TranslationUnit, location: SourceLocation) -> Cursor:
# 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: Cursor = conf.lib.clang_getCursor(tu, location)
cursor._tu = tu
return cursor
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return conf.lib.clang_equalCursors(self, other) # type: ignore [no-any-return]
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
def __hash__(self) -> int:
return self.hash
- def is_definition(self):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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,
@@ -1630,7 +1633,7 @@ class Bar {
"""
return conf.lib.clang_CXXMethod_isCopyAssignmentOperator(self) # type: ignore [no-any-return]
- def is_move_assignment_operator_method(self):
+ 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,
@@ -1655,7 +1658,7 @@ class Bar {
"""
return conf.lib.clang_CXXMethod_isMoveAssignmentOperator(self) # type: ignore [no-any-return]
- def is_explicit_method(self):
+ 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.
@@ -1700,41 +1703,41 @@ class Foo {
"""
return conf.lib.clang_CXXMethod_isExplicit(self) # type: ignore [no-any-return]
- def is_mutable_field(self):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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
@@ -1744,7 +1747,7 @@ def get_definition(self):
# declaration prior to issuing the lookup.
return Cursor.from_result(conf.lib.clang_getCursorDefinition(self), self)
- def get_usr(self):
+ def get_usr(self) -> str:
"""Return the Unified Symbol Resolution (USR) for the entity referenced
by the given cursor (or None).
@@ -1755,19 +1758,19 @@ def get_usr(self):
another translation unit."""
return _CXString.from_result(conf.lib.clang_getCursorUSR(self))
- def get_included_file(self):
+ 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):
+ def kind(self) -> CursorKind:
"""Return the kind of this cursor."""
return CursorKind.from_id(self._kind_id)
@property
- def spelling(self):
+ 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(
@@ -1776,7 +1779,7 @@ def spelling(self):
return self._spelling
- def pretty_printed(self, policy):
+ def pretty_printed(self, policy: PrintingPolicy) -> str:
"""
Pretty print declarations.
Parameters:
@@ -1787,7 +1790,7 @@ def pretty_printed(self, policy):
)
@property
- def displayname(self):
+ def displayname(self) -> str:
"""
Return the display name for the entity referenced by this cursor.
@@ -1803,7 +1806,7 @@ def displayname(self):
return self._displayname
@property
- def mangled_name(self):
+ 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(
@@ -1813,18 +1816,18 @@ def mangled_name(self):
return self._mangled_name
@property
- def location(self):
+ 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):
+ def linkage(self) -> LinkageKind:
"""Return the linkage of this cursor."""
if not hasattr(self, "_linkage"):
self._linkage = conf.lib.clang_getCursorLinkage(self)
@@ -1832,7 +1835,7 @@ def linkage(self):
return LinkageKind.from_id(self._linkage)
@property
- def tls_kind(self):
+ 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)
@@ -1840,18 +1843,18 @@ def tls_kind(self):
return TLSKind.from_id(self._tls_kind)
@property
- def extent(self):
+ 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):
+ def storage_class(self) -> StorageClass:
"""
Retrieves the storage class (if any) of the entity pointed at by the
cursor.
@@ -1862,7 +1865,7 @@ def storage_class(self):
return StorageClass.from_id(self._storage_class)
@property
- def availability(self):
+ def availability(self) -> AvailabilityKind:
"""
Retrieves the availability of the entity pointed at by the cursor.
"""
@@ -1872,7 +1875,7 @@ def availability(self):
return AvailabilityKind.from_id(self._availability)
@property
- def binary_operator(self):
+ def binary_operator(self) -> BinaryOperator:
"""
Retrieves the opcode if this cursor points to a binary operator
:return:
@@ -1884,7 +1887,7 @@ def binary_operator(self):
return BinaryOperator.from_id(self._binopcode)
@property
- def access_specifier(self):
+ def access_specifier(self) -> AccessSpecifier:
"""
Retrieves the access specifier (if any) of the entity pointed at by the
cursor.
@@ -1895,7 +1898,7 @@ def access_specifier(self):
return AccessSpecifier.from_id(self._access_specifier)
@property
- def type(self):
+ def type(self) -> Type:
"""
Retrieve the Type (if any) of the entity pointed at by the cursor.
"""
@@ -1905,7 +1908,7 @@ def type(self):
return self._type
@property
- def canonical(self):
+ def canonical(self) -> Cursor | None:
"""Return the canonical Cursor corresponding to this Cursor.
The canonical cursor is the cursor which is representative for the
@@ -1921,7 +1924,7 @@ def canonical(self):
return self._canonical
@property
- def result_type(self):
+ 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(
@@ -1931,7 +1934,7 @@ def result_type(self):
return self._result_type
@property
- def exception_specification_kind(self):
+ def exception_specification_kind(self) -> ExceptionSpecificationKind:
"""
Retrieve the exception specification kind, which is one of the values
from the ExceptionSpecificationKind enumeration.
@@ -1945,7 +1948,7 @@ def exception_specification_kind(self):
return self._exception_specification_kind
@property
- def underlying_typedef_type(self):
+ 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
@@ -1960,7 +1963,7 @@ def underlying_typedef_type(self):
return self._underlying_type
@property
- def enum_type(self):
+ 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
@@ -1975,9 +1978,10 @@ def enum_type(self):
return self._enum_type
@property
- def enum_value(self):
+ 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.
@@ -2001,7 +2005,7 @@ def enum_value(self):
return self._enum_value
@property
- def objc_type_encoding(self):
+ 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(
@@ -2011,15 +2015,15 @@ def objc_type_encoding(self):
return self._objc_type_encoding
@property
- def hash(self):
+ 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):
+ 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(
@@ -2029,7 +2033,7 @@ def semantic_parent(self):
return self._semantic_parent
@property
- def lexical_parent(self):
+ 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(
@@ -2046,14 +2050,14 @@ def specialized_template(self) -> Cursor | None:
)
@property
- def translation_unit(self):
+ 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):
+ def referenced(self) -> Cursor | None:
"""
For a cursor that is a reference, returns a cursor
representing the entity that it references.
@@ -2066,51 +2070,51 @@ def referenced(self):
return self._referenced
@property
- def brief_comment(self):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ 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()
@@ -2124,7 +2128,7 @@ def visitor(child, parent, children):
conf.lib.clang_visitChildren(self, cursor_visit_callback(visitor), children)
return iter(children)
- def walk_preorder(self):
+ def walk_preorder(self) -> Iterator[Cursor]:
"""Depth-first preorder walk over the cursor and its descendants.
Yields cursors.
@@ -2134,7 +2138,7 @@ def walk_preorder(self):
for descendant in child.walk_preorder():
yield descendant
- def get_tokens(self):
+ 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
@@ -2142,19 +2146,19 @@ def get_tokens(self):
"""
return TokenGroup.get_tokens(self._tu, self.extent)
- def get_field_offsetof(self):
+ 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):
+ 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):
+ def is_virtual_base(self) -> bool:
"""Returns whether t...
[truncated]
|
✅ With the latest revision this PR passed the Python code formatter. |
180dbe7
to
17b059d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this is merged we can finally close #120590
Some other notes:
def canonical(self): | ||
def canonical(self) -> Cursor | None: | ||
"""Return the canonical Cursor corresponding to this Cursor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function uses Cursor.from_cursor_result
to create the return value. That function returns None
if it is passed the Null-Cursor, leading to the Cursor | None
return type.
That said, the input to it comes from clang_getCanonicalCursor
which does not seem to ever return the Null-Cursor, so we could theoretically constrain the return type here to just Cursor
, by asserting isinstance(the_return_value, Cursor)
, but this is a bit ugly... what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, the input to it comes from clang_getCanonicalCursor which does not seem to ever return the Null-Cursor
Are you sure? It seems that if you pass null cursor to it, you get null cursor back:
llvm-project/clang/tools/libclang/CIndex.cpp
Lines 7438 to 7440 in ec29f83
CXCursor clang_getCanonicalCursor(CXCursor C) { | |
if (!clang_isDeclaration(C.kind)) | |
return C; |
so we could theoretically constrain the return type here to just Cursor, by asserting isinstance(the_return_value, Cursor), but this is a bit ugly... what do you think?
Looking at Cursor.from_cursor_result
and Cursor.from_result
, it seems that Python bindings make a serious effort to map null cursor to None
. Which means there should be no way to call Cursor
methods on a null cursor. Which means that clang_getCanonicalCursor
will never return null cursor back. So I agree that we should drop | None
part of type hint.
Instead of the assert you propose, I think all Cursor
methods should assert that self
is not a null cursor, and ideally avoid calling getNullCursor
on every method invocation. That said, we're already are not shy to call it in from_result
and from_cursor_result
, so it might be an acceptable overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you're right, I overlooked that.
I've just looked into asserting on all methods, and it seems there's just no way to wrap all methods of a class in Python, you can only add a decorator or the assertion directly to all methods of Cursor
... that works I guess, but is a bit difficult to maintain
|
||
return self._loc | ||
|
||
@property | ||
def linkage(self): | ||
def linkage(self) -> LinkageKind: | ||
"""Return the linkage of this cursor.""" | ||
if not hasattr(self, "_linkage"): | ||
self._linkage = conf.lib.clang_getCursorLinkage(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not directly related to this PR, but it seems that clang_getCursorLinkage
, as well as clang_getCursorTLSKind
and clang_Cursor_getStorageClass
are missing from the FUNCTION_LIST
and thus never registered to the conf.lib
object. From what I've tested they still seem to work just fine, we also have working tests for these...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is strange. Can you investigate whether we still need FUNCTION_LIST
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CDLL type loads all symbols from the library, but apparently needs help determining the argument and return types (which FUNCTION_LIST
is for). So while all exposed functions are already available as attributes of the conf.lib
in Python, many of them cannot be called until these are set correctly.
It seems that the default value for restype
is c_int
which works for our use-case with compatible types (i.e. c_uint
, c_int
, bool
, void
). Functions with those return types can be safely deleted from FUNCTION_LIST
according to my experiments. Everything else leads to bugs or segmentation faults.
Due to that, I believe it makes sense to strictly add all functions here, just for consistency, and also so we don't rely on the default (and other behaviors that I don't understand). So I'd suggest adding the missing functions here as well.
There's one more strange case: clang_getCursorTLSKind
is not an attribute of conf.lib
it seems and I have no idea why calling it doesn't lead to a missing attribute error.
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