diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 3c017c33..095b2160 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -174,7 +174,10 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: variance_type = mypy_variance_parser(generic_type.variance) variance_values: sds_types.AbstractType | None = None if variance_type == VarianceKind.INVARIANT: - values = [self.mypy_type_to_abstract_type(value) for value in generic_type.values] + values = [] + if hasattr(generic_type, "values"): + values = [self.mypy_type_to_abstract_type(value) for value in generic_type.values] + if values: variance_values = sds_types.UnionType( [self.mypy_type_to_abstract_type(value) for value in generic_type.values], @@ -840,10 +843,11 @@ def _parse_attributes( elif hasattr(lvalue, "items"): lvalues = list(lvalue.items) for lvalue_ in lvalues: - if not hasattr(lvalue_, "name"): # pragma: no cover - raise AttributeError("Expected value to have attribute 'name'.") - - if self._is_attribute_already_defined(lvalue_.name): + if ( + hasattr(lvalue_, "name") + and self._is_attribute_already_defined(lvalue_.name) + or isinstance(lvalue_, mp_nodes.IndexExpr) + ): continue attributes.append( @@ -920,7 +924,9 @@ def _create_attribute( if unanalyzed_type is not None and hasattr(unanalyzed_type, "args"): attribute_type.args = unanalyzed_type.args else: # pragma: no cover - raise AttributeError("Could not get argument information for attribute.") + logging.warning("Could not get argument information for attribute.") + attribute_type = None + type_ = sds_types.UnknownType() # Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase if ( @@ -966,8 +972,10 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis default_is_none = False # Get type information for parameter - if mypy_type is None: # pragma: no cover - raise ValueError("Argument has no type.") + if mypy_type is None: + msg = f"Could not parse the type for parameter {argument.variable.name} of function {node.fullname}." + logging.warning(msg) + arg_type = sds_types.UnknownType() elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any): # We try to infer the type through the default value later, if possible pass @@ -1122,8 +1130,24 @@ def mypy_type_to_abstract_type( types = [self.mypy_type_to_abstract_type(arg) for arg in getattr(unanalyzed_type, "args", [])] if len(types) == 1: return sds_types.FinalType(type_=types[0]) - elif len(types) == 0: # pragma: no cover - raise ValueError("Final type has no type arguments.") + elif len(types) == 0: + if hasattr(mypy_type, "items"): + literals = [ + self.mypy_type_to_abstract_type(item.last_known_value) + for item in mypy_type.items + if isinstance(item.last_known_value, mp_types.LiteralType) + ] + + if literals: + all_literals = [] + for literal_type in literals: + if isinstance(literal_type, sds_types.LiteralType): + all_literals += literal_type.literals + + return sds_types.FinalType(type_=sds_types.LiteralType(literals=all_literals)) + + logging.warning("Final type has no type arguments.") # pragma: no cover + return sds_types.FinalType(type_=sds_types.UnknownType()) # pragma: no cover return sds_types.FinalType(type_=sds_types.UnionType(types=types)) elif unanalyzed_type_name in {"list", "set"}: type_args = getattr(mypy_type, "args", []) @@ -1168,6 +1192,10 @@ def mypy_type_to_abstract_type( if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type: # If the Any type is generated b/c of from_unimported_type, then we can parse the type # from the import information + if mypy_type.missing_import_name is None: # pragma: no cover + logging.warning("Could not parse a type, added unknown type instead.") + return sds_types.UnknownType() + missing_import_name = mypy_type.missing_import_name.split(".")[-1] # type: ignore[union-attr] name, qname = self._find_alias(missing_import_name) diff --git a/src/safeds_stubgen/docstring_parsing/_docstring_parser.py b/src/safeds_stubgen/docstring_parsing/_docstring_parser.py index 14c098f5..092daabd 100644 --- a/src/safeds_stubgen/docstring_parsing/_docstring_parser.py +++ b/src/safeds_stubgen/docstring_parsing/_docstring_parser.py @@ -55,12 +55,16 @@ def get_class_documentation(self, class_node: nodes.ClassDef) -> ClassDocstring: if griffe_node.docstring is not None: docstring = griffe_node.docstring.value.strip("\n") - for docstring_section in griffe_node.docstring.parsed: - if docstring_section.kind == DocstringSectionKind.text: - description = docstring_section.value.strip("\n") - elif docstring_section.kind == DocstringSectionKind.examples: - for example_data in docstring_section.value: - examples.append(example_data[1].strip("\n")) + try: + for docstring_section in griffe_node.docstring.parsed: + if docstring_section.kind == DocstringSectionKind.text: + description = docstring_section.value.strip("\n") + elif docstring_section.kind == DocstringSectionKind.examples: + for example_data in docstring_section.value: + examples.append(example_data[1].strip("\n")) + except IndexError as _: # pragma: no cover + msg = f"There was an error while parsing the following docstring:\n{docstring}." + logging.warning(msg) return ClassDocstring( description=description, @@ -75,12 +79,17 @@ def get_function_documentation(self, function_node: nodes.FuncDef) -> FunctionDo griffe_docstring = self.__get_cached_docstring(function_node.fullname) if griffe_docstring is not None: docstring = griffe_docstring.value.strip("\n") - for docstring_section in griffe_docstring.parsed: - if docstring_section.kind == DocstringSectionKind.text: - description = docstring_section.value.strip("\n") - elif docstring_section.kind == DocstringSectionKind.examples: - for example_data in docstring_section.value: - examples.append(example_data[1].strip("\n")) + + try: + for docstring_section in griffe_docstring.parsed: + if docstring_section.kind == DocstringSectionKind.text: + description = docstring_section.value.strip("\n") + elif docstring_section.kind == DocstringSectionKind.examples: + for example_data in docstring_section.value: + examples.append(example_data[1].strip("\n")) + except IndexError as _: # pragma: no cover + msg = f"There was an error while parsing the following docstring:\n{docstring}." + logging.warning(msg) return FunctionDocstring( description=description, @@ -193,10 +202,15 @@ def get_result_documentation(self, function_qname: str) -> list[ResultDocstring] return [] all_returns = None - for docstring_section in griffe_docstring.parsed: - if docstring_section.kind == DocstringSectionKind.returns: - all_returns = docstring_section - break + try: + for docstring_section in griffe_docstring.parsed: + if docstring_section.kind == DocstringSectionKind.returns: + all_returns = docstring_section + break + except IndexError as _: # pragma: no cover + msg = f"There was an error while parsing the following docstring:\n{griffe_docstring.value}." + logging.warning(msg) + return [] if not all_returns: return [] @@ -240,13 +254,18 @@ def _get_matching_docstrings( type_: Literal["attr", "param"], ) -> list[DocstringAttribute | DocstringParameter]: all_docstrings = None - for docstring_section in function_doc.parsed: - section_kind = docstring_section.kind - if (type_ == "attr" and section_kind == DocstringSectionKind.attributes) or ( - type_ == "param" and section_kind == DocstringSectionKind.parameters - ): - all_docstrings = docstring_section - break + try: + for docstring_section in function_doc.parsed: + section_kind = docstring_section.kind + if (type_ == "attr" and section_kind == DocstringSectionKind.attributes) or ( + type_ == "param" and section_kind == DocstringSectionKind.parameters + ): + all_docstrings = docstring_section + break + except IndexError as _: # pragma: no cover + msg = f"There was an error while parsing the following docstring:\n{function_doc.value}." + logging.warning(msg) + return [] if all_docstrings: name = name.lstrip("*") diff --git a/src/safeds_stubgen/stubs_generator/_stub_string_generator.py b/src/safeds_stubgen/stubs_generator/_stub_string_generator.py index 537e4075..d0f2fce2 100644 --- a/src/safeds_stubgen/stubs_generator/_stub_string_generator.py +++ b/src/safeds_stubgen/stubs_generator/_stub_string_generator.py @@ -860,6 +860,9 @@ def _create_internal_class_string( ) -> str: superclass_class = self._get_class_in_package(superclass) + if superclass_class is None: # pragma: no cover + return "" + # Methods superclass_methods_text, existing_names = self._create_class_method_string( superclass_class.methods, @@ -881,6 +884,10 @@ def _create_internal_class_string( already_defined_names = already_defined_names.union(existing_names) for superclass_superclass in superclass_class.superclasses: + if superclass_superclass == superclass: # pragma: no cover + # If the class somehow has itself as a superclass + continue + name = superclass_superclass.split(".")[-1] if is_internal(name): superclass_methods_text += self._create_internal_class_string( @@ -1107,7 +1114,7 @@ def _create_todo_msg(self, indentations: str) -> str: return indentations + f"\n{indentations}".join(todo_msgs) + "\n" - def _get_class_in_package(self, class_qname: str) -> Class: + def _get_class_in_package(self, class_qname: str) -> Class | None: class_qname = class_qname.replace(".", "/") class_path = "/".join(class_qname.split("/")[:-1]) class_name = class_qname.split("/")[-1] @@ -1122,9 +1129,9 @@ def _get_class_in_package(self, class_qname: str) -> Class: ): return self.api.classes[class_] - raise LookupError( - f"Expected finding class '{class_name}' in module '{self._get_module_id(get_actual_id=True)}'.", - ) # pragma: no cover + msg = f"Expected finding class '{class_name}' in module '{self._get_module_id(get_actual_id=True)}'." # pragma: no cover + logging.warning(msg) # pragma: no cover + return None # pragma: no cover @staticmethod def _create_docstring_description_part(description: str, indentations: str) -> str: diff --git a/tests/data/various_modules_package/attribute_module.py b/tests/data/various_modules_package/attribute_module.py index d8432018..ead7c834 100644 --- a/tests/data/various_modules_package/attribute_module.py +++ b/tests/data/various_modules_package/attribute_module.py @@ -51,6 +51,8 @@ def some_func() -> bool: str_attr_with_none_value: str = None optional: Optional[int] + final_int: Final = (101, 32741, 2147483621) + no_final_type: Final final: Final[str] = "Value" finals: Final[str, int] = "Value" final_union: Final[str | int] = "Value" diff --git a/tests/data/various_modules_package/class_module.py b/tests/data/various_modules_package/class_module.py index ff62a509..cb80586a 100644 --- a/tests/data/various_modules_package/class_module.py +++ b/tests/data/various_modules_package/class_module.py @@ -1,4 +1,4 @@ -from typing import Self, overload +from typing import Self, overload, no_type_check from . import unknown_source from tests.data.main_package.another_path.another_module import yetAnotherClass @@ -18,6 +18,10 @@ def __init__(self, a: int, b: ClassModuleEmptyClassA | None): def __enter__(self): return self + @no_type_check + def _apply(self, f, *args, **kwargs): + pass + def f(self): ... diff --git a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr index e06f14de..876ed4f7 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -270,6 +270,27 @@ }), }), }), + dict({ + 'docstring': dict({ + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/final_int', + 'is_public': True, + 'is_static': True, + 'name': 'final_int', + 'type': dict({ + 'kind': 'FinalType', + 'type': dict({ + 'kind': 'LiteralType', + 'literals': list([ + 101, + 32741, + 2147483621, + ]), + }), + }), + }), dict({ 'docstring': dict({ 'description': '', @@ -685,6 +706,17 @@ ]), }), }), + dict({ + 'docstring': dict({ + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/attribute_module/AttributesClassB/no_final_type', + 'is_public': True, + 'is_static': True, + 'name': 'no_final_type', + 'type': None, + }), dict({ 'docstring': dict({ 'description': '', @@ -1237,6 +1269,30 @@ 'tests/data/various_modules_package/class_module/ClassModuleClassB/__enter__/result_1', ]), }), + dict({ + 'docstring': dict({ + 'description': '', + 'examples': list([ + ]), + 'full_docstring': '', + }), + 'id': 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '_apply', + 'parameters': list([ + 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/self', + 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/f', + 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/args', + 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply/kwargs', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), dict({ 'docstring': dict({ 'description': '', @@ -2138,6 +2194,7 @@ 'is_public': True, 'methods': list([ 'tests/data/various_modules_package/class_module/ClassModuleClassB/__enter__', + 'tests/data/various_modules_package/class_module/ClassModuleClassB/_apply', 'tests/data/various_modules_package/class_module/ClassModuleClassB/f', ]), 'name': 'ClassModuleClassB', @@ -7485,6 +7542,10 @@ 'alias': None, 'qualified_name': 'typing.overload', }), + dict({ + 'alias': None, + 'qualified_name': 'typing.no_type_check', + }), dict({ 'alias': None, 'qualified_name': 'unknown_source', diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub index e39e7027..a1ff3bde 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub @@ -70,6 +70,11 @@ class AttributesClassB() { @PythonName("str_attr_with_none_value") static attr strAttrWithNoneValue: String static attr optional: Int? + @PythonName("final_int") + static attr finalInt: literal<101, 32741, 2147483621> + // TODO Attribute has no type information. + @PythonName("no_final_type") + static attr noFinalType static attr final: String static attr finals: union @PythonName("final_union")