Skip to content

Commit

Permalink
fix: Fixed a bug where double ? would be generated for stubs (#103)
Browse files Browse the repository at this point in the history
Closes #87

### Summary of Changes

Fixed the bug described in #87.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
Masara and megalinter-bot committed Apr 13, 2024
1 parent 08e345f commit c35c6ac
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 5 deletions.
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

0 comments on commit c35c6ac

Please sign in to comment.