Skip to content

Commit 5917852

Browse files
Add support for @abstract functions (#388)
1 parent b1bcaa1 commit 5917852

File tree

9 files changed

+138
-15
lines changed

9 files changed

+138
-15
lines changed

gdtoolkit/common/ast.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def _load_sub_statements(self):
3939
raise NotImplementedError
4040
if self.kind in ["func_def", "static_func_def"]:
4141
self.sub_statements = [Statement(n) for n in self.lark_node.children[1:]]
42+
elif self.kind == "abstract_func_def":
43+
# Abstract functions don't have a body, so no sub-statements
44+
pass
4245
elif self.kind == "if_stmt":
4346
for branch in self.lark_node.children:
4447
if branch.data in ["if_branch", "elif_branch"]:
@@ -141,6 +144,10 @@ def _load_data_from_node_children(self, node: Tree) -> None:
141144
function = Function(stmt)
142145
self.functions.append(function)
143146
self.all_functions.append(function)
147+
if stmt.data == "abstract_func_def":
148+
function = Function(stmt)
149+
self.functions.append(function)
150+
self.all_functions.append(function)
144151

145152
def _load_data_from_class_def(self, class_def: Tree) -> None:
146153
name_token = find_name_token_among_children(class_def)

gdtoolkit/formatter/annotation.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .expression_to_str import expression_to_str
1010

1111
_STANDALONE_ANNOTATIONS = [
12+
"abstract",
1213
"export_category",
1314
"export_group",
1415
"export_subgroup",
@@ -110,6 +111,16 @@
110111
]
111112

112113

114+
def is_abstract_annotation_for_statement(statement: Tree, next_statement: Tree) -> bool:
115+
"""Check if this is an @abstract annotation that should be combined with the next statement."""
116+
if statement.data != "annotation":
117+
return False
118+
name = statement.children[0].value
119+
if name != "abstract":
120+
return False
121+
return next_statement.data in ["abstract_func_def", "classname_stmt", "class_def"]
122+
123+
113124
def is_non_standalone_annotation(statement: Tree) -> bool:
114125
if statement.data != "annotation":
115126
return False
@@ -135,9 +146,25 @@ def prepend_annotations_to_formatted_line(
135146
single_line_length = (
136147
context.indent + len(annotations_string) + len(whitelineless_line)
137148
)
138-
standalone_formatting_enforced = whitelineless_line.startswith(
139-
"func"
140-
) or whitelineless_line.startswith("static func")
149+
# Check if this is an abstract function or class_name annotation
150+
is_abstract_func = (
151+
len(context.annotations) == 1
152+
and context.annotations[0].children[0].value == "abstract"
153+
and whitelineless_line.startswith("func")
154+
)
155+
is_abstract_class_name = (
156+
len(context.annotations) == 1
157+
and context.annotations[0].children[0].value == "abstract"
158+
and whitelineless_line.startswith("class_name")
159+
)
160+
standalone_formatting_enforced = (
161+
(
162+
whitelineless_line.startswith("func")
163+
or whitelineless_line.startswith("static func")
164+
)
165+
and not is_abstract_func
166+
and not is_abstract_class_name
167+
)
141168
if (
142169
not _annotations_have_standalone_comments(
143170
context.annotations, context.standalone_comments, line_to_prepend_to[0]

gdtoolkit/formatter/block.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .annotation import (
1515
is_non_standalone_annotation,
1616
prepend_annotations_to_formatted_line,
17+
is_abstract_annotation_for_statement,
1718
)
1819

1920

@@ -26,12 +27,20 @@ def format_block(
2627
previous_statement_name = None
2728
formatted_lines = [] # type: FormattedLines
2829
previously_processed_line_number = context.previously_processed_line_number
29-
for statement in statements:
30-
if is_non_standalone_annotation(statement):
30+
31+
for i, statement in enumerate(statements):
32+
# Check if this is an abstract annotation followed by an abstract function or class_name
33+
next_statement = statements[i + 1] if i + 1 < len(statements) else None
34+
is_abstract_for_statement = (
35+
next_statement is not None
36+
and is_abstract_annotation_for_statement(statement, next_statement)
37+
)
38+
39+
if is_non_standalone_annotation(statement) or is_abstract_for_statement:
3140
context.annotations.append(statement)
32-
is_first_annotation = len(context.annotations) == 1
33-
if not is_first_annotation:
41+
if len(context.annotations) > 1:
3442
continue
43+
3544
blank_lines = reconstruct_blank_lines_in_range(
3645
previously_processed_line_number, get_line(statement), context
3746
)
@@ -46,29 +55,35 @@ def format_block(
4655
blank_lines = _add_extra_blanks_due_to_next_statement(
4756
blank_lines, statement.data, surrounding_empty_lines_table
4857
)
49-
is_first_annotation = len(context.annotations) == 1
50-
if is_non_standalone_annotation(statement) and is_first_annotation:
58+
59+
# Handle first annotation case
60+
if (
61+
is_non_standalone_annotation(statement) or is_abstract_for_statement
62+
) and len(context.annotations) == 1:
5163
formatted_lines += blank_lines
5264
continue
65+
5366
if len(context.annotations) == 0:
5467
formatted_lines += blank_lines
68+
5569
lines, previously_processed_line_number = statement_formatter(
5670
statement, context
5771
)
5872
if len(context.annotations) > 0:
5973
lines = prepend_annotations_to_formatted_line(lines[0], context) + lines[1:]
6074
formatted_lines += lines
6175
previous_statement_name = statement.data
76+
77+
# Handle end of block
6278
dedent_line_number = _find_dedent_line_number(
6379
previously_processed_line_number, context
6480
)
65-
lines_at_the_end = reconstruct_blank_lines_in_range(
66-
previously_processed_line_number, dedent_line_number, context
81+
formatted_lines += _remove_empty_strings_from_end(
82+
reconstruct_blank_lines_in_range(
83+
previously_processed_line_number, dedent_line_number, context
84+
)
6785
)
68-
lines_at_the_end = _remove_empty_strings_from_end(lines_at_the_end)
69-
formatted_lines += lines_at_the_end
70-
previously_processed_line_number = dedent_line_number - 1
71-
return (formatted_lines, previously_processed_line_number)
86+
return (formatted_lines, dedent_line_number - 1)
7287

7388

7489
def reconstruct_blank_lines_in_range(

gdtoolkit/formatter/class_statement.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def format_class_statement(statement: Tree, context: Context) -> Outcome:
4040
"static_func_def": lambda s, c: _format_func_statement(
4141
s.children[0], c, "static "
4242
),
43+
"abstract_func_def": _format_abstract_func_statement,
4344
"annotation": format_standalone_annotation,
4445
"property_body_def": format_property_body,
4546
} # type: Dict[str, Callable]
@@ -203,3 +204,21 @@ def _format_enum_statement(statement: Tree, context: Context) -> Outcome:
203204
)
204205
enum_body = actual_enum.children[-1]
205206
return format_concrete_expression(enum_body, expression_context, context)
207+
208+
209+
def _format_abstract_func_statement(statement: Tree, context: Context) -> Outcome:
210+
abstract_func_header = statement.children[0]
211+
return _format_abstract_func_header(abstract_func_header, context)
212+
213+
214+
def _format_abstract_func_header(statement: Tree, context: Context) -> Outcome:
215+
name = statement.children[0].value
216+
has_return_type = len(statement.children) > 2
217+
expression_context = ExpressionContext(
218+
f"func {name}",
219+
get_line(statement),
220+
f" -> {statement.children[2].value}" if has_return_type else "",
221+
get_end_line(statement),
222+
)
223+
func_args = statement.children[1]
224+
return format_concrete_expression(func_args, expression_context, context)

gdtoolkit/gd2py/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def _convert_statement(statement: Tree, context: Context) -> List[str]:
6464
)
6565
],
6666
"static_func_def": _convert_first_child_as_statement,
67+
"abstract_func_def": _convert_abstract_func_def,
6768
"docstr_stmt": _pass,
6869
# func statements:
6970
"func_var_stmt": _convert_first_child_as_statement,
@@ -164,6 +165,16 @@ def _convert_func_def(statement: Tree, context: Context) -> List[str]:
164165
] + _convert_block(statement.children[1:], context.create_child_context(-1))
165166

166167

168+
def _convert_abstract_func_def(statement: Tree, context: Context) -> List[str]:
169+
# Abstract functions don't have a body, so we create a function that raises NotImplementedError
170+
func_header = statement.children[0]
171+
func_name = func_header.children[0].value
172+
return [
173+
f"{context.indent_string}def {func_name}():",
174+
f"{context.indent_string} raise NotImplementedError('Abstract method not implemented')",
175+
]
176+
177+
167178
def _convert_branch_with_expression(
168179
prefix: str, statement: Tree, context: Context
169180
) -> List[str]:

gdtoolkit/linter/class_checks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ def _map_statement_to_section(statement: Statement) -> str:
122122
return "others"
123123
if statement.kind == "static_func_def":
124124
return "others"
125+
if statement.kind == "abstract_func_def":
126+
return "others"
125127
if statement.kind == "docstr_stmt":
126128
return "docstrings"
127129
if statement.kind == "static_class_var_stmt":

gdtoolkit/parser/gdscript.lark

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ _simple_class_stmt: annotation* single_class_stmt (";" annotation* single_class_
1818
| property_body_def
1919
| func_def
2020
| "static" func_def -> static_func_def
21+
| abstract_func_def
2122
annotation: "@" NAME [annotation_args]
2223
annotation_args: "(" [test_expr ("," test_expr)* [trailing_comma]] ")"
2324

@@ -71,7 +72,9 @@ _property_delegates: property_delegate_set ["," [_NL] property_delegate_get]
7172
property_custom_getter_args: "(" ")"
7273

7374
func_def: func_header _func_suite
75+
abstract_func_def: abstract_func_header
7476
func_header: "func" NAME func_args ["->" TYPE_HINT] ":"
77+
abstract_func_header: "func" NAME func_args ["->" TYPE_HINT]
7578
func_args: "(" [func_arg ("," func_arg)* [trailing_comma]] ")"
7679
?func_arg: func_arg_regular
7780
| func_arg_inf
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@abstract
2+
class_name BaseClass
3+
4+
@abstract
5+
class TestClass:
6+
@abstract
7+
func test_func()
8+
9+
@abstract
10+
func simple_abstract()
11+
12+
@abstract
13+
func abstract_with_params(param1: String, param2: int)
14+
15+
@abstract
16+
func abstract_with_return_type() -> String
17+
18+
@abstract
19+
func abstract_with_params_and_return(input: String) -> int
20+
21+
func concrete_method():
22+
pass
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@abstract class_name BaseClass
2+
3+
@abstract class TestClass:
4+
@abstract func test_func()
5+
6+
7+
@abstract func simple_abstract()
8+
9+
@abstract func abstract_with_params(param1: String, param2: int)
10+
11+
@abstract func abstract_with_return_type() -> String
12+
13+
@abstract func abstract_with_params_and_return(input: String) -> int
14+
15+
16+
func concrete_method():
17+
pass

0 commit comments

Comments
 (0)