Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
34 changes: 31 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,26 @@ 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")
)
is_abstract_class = (
len(context.annotations) == 1 and
context.annotations[0].children[0].value == "abstract" and
whitelineless_line.startswith("class ")
)
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
14 changes: 11 additions & 3 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,8 +27,15 @@ 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:
Expand All @@ -47,7 +55,7 @@ def format_block(
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:
if (is_non_standalone_annotation(statement) or is_abstract_for_statement) and is_first_annotation:
formatted_lines += blank_lines
continue
if len(context.annotations) == 0:
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
Loading