diff --git a/gdtoolkit/common/ast.py b/gdtoolkit/common/ast.py index f3ca564..938e940 100644 --- a/gdtoolkit/common/ast.py +++ b/gdtoolkit/common/ast.py @@ -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"]: @@ -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) diff --git a/gdtoolkit/formatter/annotation.py b/gdtoolkit/formatter/annotation.py index f0a9348..3975565 100644 --- a/gdtoolkit/formatter/annotation.py +++ b/gdtoolkit/formatter/annotation.py @@ -9,6 +9,7 @@ from .expression_to_str import expression_to_str _STANDALONE_ANNOTATIONS = [ + "abstract", "export_category", "export_group", "export_subgroup", @@ -110,6 +111,16 @@ ] +def is_abstract_annotation_for_statement(statement: Tree, next_statement: Tree) -> bool: + """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 @@ -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] diff --git a/gdtoolkit/formatter/block.py b/gdtoolkit/formatter/block.py index 38b7518..e23a6c8 100644 --- a/gdtoolkit/formatter/block.py +++ b/gdtoolkit/formatter/block.py @@ -14,6 +14,7 @@ from .annotation import ( is_non_standalone_annotation, prepend_annotations_to_formatted_line, + is_abstract_annotation_for_statement, ) @@ -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 ) @@ -46,12 +55,17 @@ 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 - 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 ) @@ -59,16 +73,17 @@ def format_block( 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( diff --git a/gdtoolkit/formatter/class_statement.py b/gdtoolkit/formatter/class_statement.py index b9ed7b9..cd4cca5 100644 --- a/gdtoolkit/formatter/class_statement.py +++ b/gdtoolkit/formatter/class_statement.py @@ -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] @@ -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) diff --git a/gdtoolkit/gd2py/__init__.py b/gdtoolkit/gd2py/__init__.py index e2e1954..3294a89 100644 --- a/gdtoolkit/gd2py/__init__.py +++ b/gdtoolkit/gd2py/__init__.py @@ -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, @@ -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]: diff --git a/gdtoolkit/linter/class_checks.py b/gdtoolkit/linter/class_checks.py index 6c6fc82..3da2021 100644 --- a/gdtoolkit/linter/class_checks.py +++ b/gdtoolkit/linter/class_checks.py @@ -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": diff --git a/gdtoolkit/parser/gdscript.lark b/gdtoolkit/parser/gdscript.lark index 8f9cd76..28be929 100644 --- a/gdtoolkit/parser/gdscript.lark +++ b/gdtoolkit/parser/gdscript.lark @@ -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]] ")" @@ -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 diff --git a/tests/formatter/input-output-pairs/abstract_functions.in.gd b/tests/formatter/input-output-pairs/abstract_functions.in.gd new file mode 100644 index 0000000..2c2b9b6 --- /dev/null +++ b/tests/formatter/input-output-pairs/abstract_functions.in.gd @@ -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 diff --git a/tests/formatter/input-output-pairs/abstract_functions.out.gd b/tests/formatter/input-output-pairs/abstract_functions.out.gd new file mode 100644 index 0000000..0995cd6 --- /dev/null +++ b/tests/formatter/input-output-pairs/abstract_functions.out.gd @@ -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