Skip to content
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

fix: Fixed a bug where double ? would be generated for stubs #103

Merged
merged 16 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/safeds_stubgen/api_analyzer/_ast_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions src/safeds_stubgen/stubs_generator/_generate_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"]]

Expand All @@ -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]}?"
Expand All @@ -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":
Expand All @@ -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)}>"
Expand Down
7 changes: 7 additions & 0 deletions tests/data/various_modules_package/function_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
): ...


Expand Down
238 changes: 238 additions & 0 deletions tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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([
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ fun params(
@PythonName("tuple_") tuple: Tuple<Int, String, Boolean>,
`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<Int>?,
@PythonName("dict_none") dictNone: Map<String, Int>?,
@PythonName("named_class_none") namedClassNone: FunctionModuleClassA?,
@PythonName("list_class_none") listClassNone: List<Float>?,
@PythonName("tuple_class_none") tupleClassNone: Tuple<Int, String>?
)

// TODO Result type information missing.
Expand Down
Loading