Skip to content
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
7 changes: 7 additions & 0 deletions gdtoolkit/common/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def _load_sub_statements(self):
raise NotImplementedError
if self.kind in ["func_def", "static_func_def"]:
self.sub_statements = [Statement(n) for n in self.lark_node.children[1:]]
elif self.kind == "abstract_func_def":
# Abstract functions don't have a body, so no sub-statements
pass
elif self.kind == "if_stmt":
for branch in self.lark_node.children:
if branch.data in ["if_branch", "elif_branch"]:
Expand Down Expand Up @@ -141,6 +144,10 @@ def _load_data_from_node_children(self, node: Tree) -> None:
function = Function(stmt)
self.functions.append(function)
self.all_functions.append(function)
if stmt.data == "abstract_func_def":
function = Function(stmt)
self.functions.append(function)
self.all_functions.append(function)

def _load_data_from_class_def(self, class_def: Tree) -> None:
name_token = find_name_token_among_children(class_def)
Expand Down
33 changes: 30 additions & 3 deletions gdtoolkit/formatter/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .expression_to_str import expression_to_str

_STANDALONE_ANNOTATIONS = [
"abstract",
"export_category",
"export_group",
"export_subgroup",
Expand Down Expand Up @@ -110,6 +111,16 @@
]


def is_abstract_annotation_for_statement(statement: Tree, next_statement: Tree) -> bool:
Copy link
Contributor Author

@TranquilMarmot TranquilMarmot Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to do some extra checks in order to get the @abstract annotation on the same line as func/class_name/class.

Based on the reference to @abstract here:
https://godotengine.org/article/dev-snapshot-godot-4-5-beta-2/
and looking at the GDScript style guide, it does seem like the @ annotations are supposed to be on the same line as what they're modifying.

"""Check if this is an @abstract annotation that should be combined with the next statement."""
if statement.data != "annotation":
return False
name = statement.children[0].value
if name != "abstract":
return False
return next_statement.data in ["abstract_func_def", "classname_stmt", "class_def"]


def is_non_standalone_annotation(statement: Tree) -> bool:
if statement.data != "annotation":
return False
Expand All @@ -135,9 +146,25 @@ def prepend_annotations_to_formatted_line(
single_line_length = (
context.indent + len(annotations_string) + len(whitelineless_line)
)
standalone_formatting_enforced = whitelineless_line.startswith(
"func"
) or whitelineless_line.startswith("static func")
# Check if this is an abstract function or class_name annotation
is_abstract_func = (
len(context.annotations) == 1
and context.annotations[0].children[0].value == "abstract"
and whitelineless_line.startswith("func")
)
is_abstract_class_name = (
len(context.annotations) == 1
and context.annotations[0].children[0].value == "abstract"
and whitelineless_line.startswith("class_name")
)
standalone_formatting_enforced = (
(
whitelineless_line.startswith("func")
or whitelineless_line.startswith("static func")
)
and not is_abstract_func
and not is_abstract_class_name
)
if (
not _annotations_have_standalone_comments(
context.annotations, context.standalone_comments, line_to_prepend_to[0]
Expand Down
39 changes: 27 additions & 12 deletions gdtoolkit/formatter/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .annotation import (
is_non_standalone_annotation,
prepend_annotations_to_formatted_line,
is_abstract_annotation_for_statement,
)


Expand All @@ -26,12 +27,20 @@ def format_block(
previous_statement_name = None
formatted_lines = [] # type: FormattedLines
previously_processed_line_number = context.previously_processed_line_number
for statement in statements:
if is_non_standalone_annotation(statement):

for i, statement in enumerate(statements):
# Check if this is an abstract annotation followed by an abstract function or class_name
next_statement = statements[i + 1] if i + 1 < len(statements) else None
is_abstract_for_statement = (
next_statement is not None
and is_abstract_annotation_for_statement(statement, next_statement)
)

if is_non_standalone_annotation(statement) or is_abstract_for_statement:
context.annotations.append(statement)
is_first_annotation = len(context.annotations) == 1
if not is_first_annotation:
if len(context.annotations) > 1:
continue

blank_lines = reconstruct_blank_lines_in_range(
previously_processed_line_number, get_line(statement), context
)
Expand All @@ -46,29 +55,35 @@ def format_block(
blank_lines = _add_extra_blanks_due_to_next_statement(
blank_lines, statement.data, surrounding_empty_lines_table
)
is_first_annotation = len(context.annotations) == 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to refactor this function a bit to get around:

    lint: commands[0]> pylint -rn -j0 setup.py gdtoolkit/ tests/ --rcfile=pylintrc
************* Module gdtoolkit.formatter.block
gdtoolkit/formatter/block.py:21:0: R0914: Too many local variables (16/15) (too-many-locals)

if is_non_standalone_annotation(statement) and is_first_annotation:

# Handle first annotation case
if (
is_non_standalone_annotation(statement) or is_abstract_for_statement
) and len(context.annotations) == 1:
formatted_lines += blank_lines
continue

if len(context.annotations) == 0:
formatted_lines += blank_lines

lines, previously_processed_line_number = statement_formatter(
statement, context
)
if len(context.annotations) > 0:
lines = prepend_annotations_to_formatted_line(lines[0], context) + lines[1:]
formatted_lines += lines
previous_statement_name = statement.data

# Handle end of block
dedent_line_number = _find_dedent_line_number(
previously_processed_line_number, context
)
lines_at_the_end = reconstruct_blank_lines_in_range(
previously_processed_line_number, dedent_line_number, context
formatted_lines += _remove_empty_strings_from_end(
reconstruct_blank_lines_in_range(
previously_processed_line_number, dedent_line_number, context
)
)
lines_at_the_end = _remove_empty_strings_from_end(lines_at_the_end)
formatted_lines += lines_at_the_end
previously_processed_line_number = dedent_line_number - 1
return (formatted_lines, previously_processed_line_number)
return (formatted_lines, dedent_line_number - 1)


def reconstruct_blank_lines_in_range(
Expand Down
19 changes: 19 additions & 0 deletions gdtoolkit/formatter/class_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def format_class_statement(statement: Tree, context: Context) -> Outcome:
"static_func_def": lambda s, c: _format_func_statement(
s.children[0], c, "static "
),
"abstract_func_def": _format_abstract_func_statement,
"annotation": format_standalone_annotation,
"property_body_def": format_property_body,
} # type: Dict[str, Callable]
Expand Down Expand Up @@ -203,3 +204,21 @@ def _format_enum_statement(statement: Tree, context: Context) -> Outcome:
)
enum_body = actual_enum.children[-1]
return format_concrete_expression(enum_body, expression_context, context)


def _format_abstract_func_statement(statement: Tree, context: Context) -> Outcome:
abstract_func_header = statement.children[0]
return _format_abstract_func_header(abstract_func_header, context)


def _format_abstract_func_header(statement: Tree, context: Context) -> Outcome:
name = statement.children[0].value
has_return_type = len(statement.children) > 2
expression_context = ExpressionContext(
f"func {name}",
get_line(statement),
f" -> {statement.children[2].value}" if has_return_type else "",
get_end_line(statement),
)
func_args = statement.children[1]
return format_concrete_expression(func_args, expression_context, context)
11 changes: 11 additions & 0 deletions gdtoolkit/gd2py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _convert_statement(statement: Tree, context: Context) -> List[str]:
)
],
"static_func_def": _convert_first_child_as_statement,
"abstract_func_def": _convert_abstract_func_def,
"docstr_stmt": _pass,
# func statements:
"func_var_stmt": _convert_first_child_as_statement,
Expand Down Expand Up @@ -164,6 +165,16 @@ def _convert_func_def(statement: Tree, context: Context) -> List[str]:
] + _convert_block(statement.children[1:], context.create_child_context(-1))


def _convert_abstract_func_def(statement: Tree, context: Context) -> List[str]:
# Abstract functions don't have a body, so we create a function that raises NotImplementedError
func_header = statement.children[0]
func_name = func_header.children[0].value
return [
f"{context.indent_string}def {func_name}():",
f"{context.indent_string} raise NotImplementedError('Abstract method not implemented')",
]


def _convert_branch_with_expression(
prefix: str, statement: Tree, context: Context
) -> List[str]:
Expand Down
2 changes: 2 additions & 0 deletions gdtoolkit/linter/class_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def _map_statement_to_section(statement: Statement) -> str:
return "others"
if statement.kind == "static_func_def":
return "others"
if statement.kind == "abstract_func_def":
return "others"
if statement.kind == "docstr_stmt":
return "docstrings"
if statement.kind == "static_class_var_stmt":
Expand Down
3 changes: 3 additions & 0 deletions gdtoolkit/parser/gdscript.lark
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ _simple_class_stmt: annotation* single_class_stmt (";" annotation* single_class_
| property_body_def
| func_def
| "static" func_def -> static_func_def
| abstract_func_def
annotation: "@" NAME [annotation_args]
annotation_args: "(" [test_expr ("," test_expr)* [trailing_comma]] ")"

Expand Down Expand Up @@ -71,7 +72,9 @@ _property_delegates: property_delegate_set ["," [_NL] property_delegate_get]
property_custom_getter_args: "(" ")"

func_def: func_header _func_suite
abstract_func_def: abstract_func_header
func_header: "func" NAME func_args ["->" TYPE_HINT] ":"
abstract_func_header: "func" NAME func_args ["->" TYPE_HINT]
func_args: "(" [func_arg ("," func_arg)* [trailing_comma]] ")"
?func_arg: func_arg_regular
| func_arg_inf
Expand Down
22 changes: 22 additions & 0 deletions tests/formatter/input-output-pairs/abstract_functions.in.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@abstract
class_name BaseClass

@abstract
class TestClass:
@abstract
func test_func()

@abstract
func simple_abstract()

@abstract
func abstract_with_params(param1: String, param2: int)

@abstract
func abstract_with_return_type() -> String

@abstract
func abstract_with_params_and_return(input: String) -> int

func concrete_method():
pass
17 changes: 17 additions & 0 deletions tests/formatter/input-output-pairs/abstract_functions.out.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@abstract class_name BaseClass

@abstract class TestClass:
@abstract func test_func()


@abstract func simple_abstract()

@abstract func abstract_with_params(param1: String, param2: int)

@abstract func abstract_with_return_type() -> String

@abstract func abstract_with_params_and_return(input: String) -> int


func concrete_method():
pass