diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 4bff282d..c5f41d4b 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -66,7 +66,7 @@ def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: child_definitions = [ _definition for _definition in get_mypyfile_definitions(node) - if _definition.__class__.__name__ not in ["FuncDef", "Decorator", "ClassDef", "AssignmentStmt"] + if _definition.__class__.__name__ not in {"FuncDef", "Decorator", "ClassDef", "AssignmentStmt"} ] # Imports diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 600ae384..f82a83d0 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -2,6 +2,7 @@ from enum import IntEnum from pathlib import Path +from types import NoneType from typing import TYPE_CHECKING from safeds_stubgen.api_analyzer import ( @@ -751,12 +752,18 @@ def _create_type_string(self, type_data: dict | None) -> str: # and we have to join them for the stubs. literal_data = [] other_type_data = [] + has_named_type = False for type_information in type_data["types"]: if type_information["kind"] == "LiteralType": literal_data.append(type_information) else: other_type_data.append(type_information) + if type_information["kind"] in {"NamedType", "TupleType", "ListType", "SetType", "DictType"} and not ( + type_information["kind"] == "NamedType" and type_information["qname"] == "builtins.None" + ): + has_named_type = True + if len(literal_data) >= 2: all_literals = [literal_type for literal in literal_data for literal_type in literal["literals"]] @@ -769,15 +776,28 @@ def _create_type_string(self, type_data: dict | None) -> str: }, ) + if len(type_data["types"]) == 2 and literal_data: + # If we have a LiteralType and a None we combine them to a "Literal[..., null]" + has_none = (type_data["types"][0]["kind"] == "NamedType" and type_data["types"][0]["kind"]) or ( + type_data["types"][1]["kind"] == "NamedType" and type_data["types"][1]["kind"] + ) + if has_none: + _types = type_data["types"] + literal_type_data = _types[0] if _types[0]["kind"] == "LiteralType" else _types[1] + + literal_type_data["literals"].append(None) + return self._create_type_string(literal_type_data) + # Union items have to be unique, therefore we use sets. But the types set has to be a sorted list, since # otherwise the snapshot tests would fail b/c element order in sets is non-deterministic. types = list({self._create_type_string(type_) for type_ in type_data["types"]}) types.sort() if types: - if len(types) == 2 and none_type_name in types: + if len(types) == 2 and none_type_name in types and has_named_type: # if None is at least one of the two possible types, we can remove the None and just return the - # other type with a question mark + # other type with a question mark. But only named types (class/enum/enum variant) support the ? + # syntax for nullability in Safe-DS, therefore we handle callable types here. if types[0] == none_type_name: return f"{types[1]}?" return f"{types[0]}?" @@ -786,6 +806,11 @@ def _create_type_string(self, type_data: dict | None) -> str: elif len(types) == 1: return types[0] + if none_type_name in types and types[-1] != none_type_name: + # Make sure Nones are always at the end of Unions + types.pop(types.index(none_type_name)) + types.append(none_type_name) + return f"union<{', '.join(types)}>" return "" elif kind == "TupleType": @@ -807,6 +832,8 @@ def _create_type_string(self, type_data: dict | None) -> str: types.append("true") else: types.append("false") + elif isinstance(literal_type, NoneType): + types.append("null") else: types.append(f"{literal_type}") return f"literal<{', '.join(types)}>" diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 5a781233..6c180bd4 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -56,6 +56,13 @@ def params( literal: Literal["Some String"], any_: Any, callable_none: Callable[[int, float], None] | None, + literal_none: Literal["1", 2] | None, + literal_none2: None | Literal["1", 2], + set_none: set[int] | None, + dict_none: dict[str, int] | None, + named_class_none: FunctionModuleClassA | None, + list_class_none: list[float] | None, + tuple_class_none: tuple[int, str] | None, ): ... 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 542702e1..390ce9b9 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -3786,6 +3786,41 @@ 'name': 'callexpr', 'type': None, }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/dict_none', + 'is_optional': False, + 'name': 'dict_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'key_type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + 'kind': 'DictType', + 'value_type': dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -3877,6 +3912,38 @@ ]), }), }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/list_class_none', + 'is_optional': False, + 'name': 'list_class_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'ListType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'float', + 'qname': 'builtins.float', + }), + ]), + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -3895,6 +3962,101 @@ ]), }), }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/literal_none', + 'is_optional': False, + 'name': 'literal_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'LiteralType', + 'literals': list([ + '1', + ]), + }), + dict({ + 'kind': 'LiteralType', + 'literals': list([ + 2, + ]), + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/literal_none2', + 'is_optional': False, + 'name': 'literal_none2', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + dict({ + 'kind': 'LiteralType', + 'literals': list([ + '1', + ]), + }), + dict({ + 'kind': 'LiteralType', + 'literals': list([ + 2, + ]), + }), + ]), + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/named_class_none', + 'is_optional': False, + 'name': 'named_class_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'FunctionModuleClassA', + 'qname': 'tests.data.various_modules_package.function_module.FunctionModuleClassA', + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -3978,6 +4140,38 @@ ]), }), }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/set_none', + 'is_optional': False, + 'name': 'set_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'SetType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + ]), + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -4027,6 +4221,43 @@ ]), }), }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': None, + }), + 'id': 'tests/data/various_modules_package/function_module/params/tuple_class_none', + 'is_optional': False, + 'name': 'tuple_class_none', + 'type': dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'TupleType', + 'types': list([ + dict({ + 'kind': 'NamedType', + 'name': 'int', + 'qname': 'builtins.int', + }), + dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + ]), + }), + dict({ + 'kind': 'NamedType', + 'name': 'None', + 'qname': 'builtins.None', + }), + ]), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -5942,6 +6173,13 @@ 'tests/data/various_modules_package/function_module/params/literal', 'tests/data/various_modules_package/function_module/params/any_', 'tests/data/various_modules_package/function_module/params/callable_none', + 'tests/data/various_modules_package/function_module/params/literal_none', + 'tests/data/various_modules_package/function_module/params/literal_none2', + 'tests/data/various_modules_package/function_module/params/set_none', + 'tests/data/various_modules_package/function_module/params/dict_none', + 'tests/data/various_modules_package/function_module/params/named_class_none', + 'tests/data/various_modules_package/function_module/params/list_class_none', + 'tests/data/various_modules_package/function_module/params/tuple_class_none', ]), 'reexported_by': list([ ]), diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub index 2d914a70..7dd02f00 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub @@ -31,7 +31,14 @@ fun params( @PythonName("tuple_") tuple: Tuple, `literal`: literal<"Some String">, @PythonName("any_") any: Any, - @PythonName("callable_none") callableNone: (param1: Int, param2: Float) -> ()? + @PythonName("callable_none") callableNone: union<(param1: Int, param2: Float) -> (), Nothing?>, + @PythonName("literal_none") literalNone: literal<"1", 2, null>, + @PythonName("literal_none2") literalNone2: literal<"1", 2, null>, + @PythonName("set_none") setNone: Set?, + @PythonName("dict_none") dictNone: Map?, + @PythonName("named_class_none") namedClassNone: FunctionModuleClassA?, + @PythonName("list_class_none") listClassNone: List?, + @PythonName("tuple_class_none") tupleClassNone: Tuple? ) // TODO Result type information missing. diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub index 806bc96f..07ac05f5 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub @@ -51,7 +51,7 @@ class InferMyTypes( static fun inferFunction( @PythonName("infer_param") inferParam: Int = 1, @PythonName("infer_param_2") inferParam2: Int = "Something" - ) -> (result1: union, result2: union, result3: Float?) + ) -> (result1: union, result2: union, result3: Float?) /** * Test for inferring results with just one possible result, and not a tuple of results.