Skip to content

Commit 78496d7

Browse files
committed
Fix 'class_definitions_order' linter check
1 parent 4a40cac commit 78496d7

File tree

3 files changed

+137
-62
lines changed

3 files changed

+137
-62
lines changed

gdtoolkit/common/ast.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
1+
# flake8: noqa
12
from typing import List
23

34
from lark import Tree
45

6+
from ..formatter.annotation import STANDALONE_ANNOTATIONS
7+
58
from .utils import find_name_token_among_children, find_tree_among_children
69

710

8-
# pylint: disable=too-few-public-methods
11+
# pylint: disable-next=too-few-public-methods
12+
class Annotation:
13+
def __init__(self, node: Tree):
14+
self.lark_node = node
15+
self.name = node.children[0].value
16+
17+
18+
# pylint: disable-next=too-few-public-methods
919
class Statement:
1020
"""Abstract representation of statement"""
1121

12-
def __init__(self, node: Tree):
22+
# TODO: remove default = [] and fix Statements w/o annotations passed
23+
# pylint: disable-next=dangerous-default-value
24+
def __init__(self, node: Tree, annotations: List[Annotation] = []):
1325
self.lark_node = node
1426
self.kind = node.data
27+
self.annotations = annotations
1528
self.sub_statements = [] # type: List[Statement]
1629
self.all_sub_statements = [] # type: List[Statement]
1730

1831
self._load_sub_statements()
1932

2033
def _load_sub_statements(self):
2134
if self.kind == "class_def":
22-
raise NotImplementedError
35+
pass # TODO: implement
2336
if self.kind == "property_body_def":
2437
raise NotImplementedError
2538
if self.kind in ["func_def", "static_func_def"]:
@@ -48,15 +61,15 @@ def __repr__(self):
4861
)
4962

5063

51-
# pylint: disable=too-few-public-methods
64+
# pylint: disable-next=too-few-public-methods
5265
class Parameter:
5366
"""Abstract representation of function parameter"""
5467

5568
def __init__(self, node: Tree):
5669
self.name = node.children[0].value
5770

5871

59-
# pylint: disable=too-few-public-methods
72+
# pylint: disable-next=too-few-public-methods
6073
class Function(Statement):
6174
"""Abstract representation of function"""
6275

@@ -80,7 +93,7 @@ def _load_data_from_func_def(self, func_def: Tree) -> None:
8093
]
8194

8295

83-
# pylint: disable=too-few-public-methods
96+
# pylint: disable-next=too-few-public-methods
8497
class Class:
8598
"""Abstract representation of class.
8699
Since it contains sub-classes, it forms a tree"""
@@ -92,6 +105,7 @@ def __init__(self, parse_tree: Tree):
92105
self.all_sub_classes = [] # type: List[Class]
93106
self.functions = [] # type: List[Function]
94107
self.all_functions = [] # type: List[Function]
108+
self.statements = [] # type: List[Statement]
95109

96110
if parse_tree.data == "start":
97111
start = parse_tree
@@ -102,9 +116,19 @@ def __init__(self, parse_tree: Tree):
102116
raise Exception("Cannot load class from that node")
103117

104118
def _load_data_from_node_children(self, node: Tree) -> None:
105-
for stmt in node.children:
106-
if not isinstance(stmt, Tree):
119+
offset = 1 if node.data == "class_def" else 0
120+
annotations = []
121+
for stmt in node.children[offset:]:
122+
if stmt.data == "annotation" and not _is_annotation_standalone(
123+
Annotation(stmt)
124+
):
125+
annotations.append(Annotation(stmt))
126+
continue
127+
if stmt.data == "property_body_def":
107128
continue
129+
assert isinstance(stmt, Tree), stmt
130+
self.statements.append(Statement(stmt, annotations))
131+
annotations = []
108132
if stmt.data == "class_def":
109133
sub_class = Class(stmt)
110134
self.sub_classes.append(sub_class)
@@ -121,7 +145,7 @@ def _load_data_from_class_def(self, class_def: Tree) -> None:
121145
self._load_data_from_node_children(class_def)
122146

123147

124-
# pylint: disable=too-few-public-methods
148+
# pylint: disable-next=too-few-public-methods
125149
class AbstractSyntaxTree:
126150
"""Post-processed version of parse tree - more convenient representation
127151
for further processing"""
@@ -130,3 +154,7 @@ def __init__(self, parse_tree: Tree):
130154
self.root_class = Class(parse_tree)
131155
self.all_classes = [self.root_class] + self.root_class.all_sub_classes
132156
self.all_functions = self.root_class.all_functions
157+
158+
159+
def _is_annotation_standalone(annotation: Annotation) -> bool:
160+
return annotation.name in STANDALONE_ANNOTATIONS

gdtoolkit/linter/class_checks.py

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from lark import Token, Tree
66

7+
from ..common.ast import AbstractSyntaxTree, Class, Statement, Annotation
78
from ..common.utils import find_name_token_among_children
89

910
from .problem import Problem
@@ -17,15 +18,22 @@ def lint(parse_tree: Tree, config: MappingProxyType) -> List[Problem]:
1718
"private-method-call",
1819
_private_method_call_check,
1920
),
21+
] # type: List[Tuple[str, Callable]]
22+
problem_clusters = (
23+
x[1](parse_tree) if x[0] not in disable else [] for x in checks_to_run_w_tree
24+
)
25+
problems = [problem for cluster in problem_clusters for problem in cluster]
26+
checks_to_run_w_ast = [
2027
(
2128
"class-definitions-order",
2229
partial(_class_definitions_order_check, config["class-definitions-order"]),
2330
),
24-
] # type: List[Tuple[str, Callable]]
31+
]
32+
ast = AbstractSyntaxTree(parse_tree)
2533
problem_clusters = (
26-
x[1](parse_tree) if x[0] not in disable else [] for x in checks_to_run_w_tree
34+
x[1](ast) if x[0] not in disable else [] for x in checks_to_run_w_ast
2735
)
28-
problems = [problem for cluster in problem_clusters for problem in cluster]
36+
problems += [problem for cluster in problem_clusters for problem in cluster]
2937
return problems
3038

3139

@@ -55,71 +63,89 @@ def _private_method_call_check(parse_tree: Tree) -> List[Problem]:
5563
return problems
5664

5765

66+
# TODO: drop
5867
def _is_method_private(method_name: str) -> bool:
5968
return method_name.startswith("_") # TODO: consider making configurable
6069

6170

62-
def _class_definitions_order_check(order, parse_tree: Tree) -> List[Problem]:
63-
problems = _class_definitions_order_check_for_class(
64-
"global scope", parse_tree.children, order
65-
)
66-
for class_def in parse_tree.find_data("class_def"):
67-
class_name = class_def.children[0].value
68-
problems += _class_definitions_order_check_for_class(
69-
"class {}".format(class_name), class_def.children, order
70-
)
71-
return problems
71+
def _class_definitions_order_check(
72+
order: List[str], ast: AbstractSyntaxTree
73+
) -> List[Problem]:
74+
return [
75+
problem
76+
for a_class in ast.all_classes
77+
for problem in _class_definitions_order_check_for_class(a_class, order)
78+
]
7279

7380

7481
def _class_definitions_order_check_for_class(
75-
class_name: str, class_children, order
82+
a_class: Class, order: List[str]
7683
) -> List[Problem]:
77-
stmt_to_section_mapping = {
78-
"tool_stmt": "tools",
79-
"signal_stmt": "signals",
80-
"extends_stmt": "extends",
81-
"classname_stmt": "classnames",
82-
"const_stmt": "consts",
83-
"export_stmt": "exports",
84-
"enum_def": "enums",
85-
}
86-
visibility_dependent_stmt_to_section_mapping = {
87-
"class_var_stmt": {"pub": "pubvars", "prv": "prvvars"},
88-
"onready_stmt": {"pub": "onreadypubvars", "prv": "onreadyprvvars"},
89-
}
9084
problems = []
9185
current_section = order[0]
92-
for class_child in class_children:
93-
if not isinstance(class_child, Tree):
94-
continue
95-
if class_child.data in ["annotation"]:
86+
for statement in a_class.statements:
87+
if _is_statement_irrelevant(statement):
9688
continue
97-
stmt = class_child.data
98-
if stmt == "class_var_stmt":
99-
visibility = _class_var_stmt_visibility(class_child)
100-
section = visibility_dependent_stmt_to_section_mapping[stmt][visibility]
101-
elif stmt == "onready_stmt":
102-
class_var_stmt = class_child.children[0]
103-
visibility = _class_var_stmt_visibility(class_var_stmt)
104-
section = visibility_dependent_stmt_to_section_mapping[stmt][visibility]
105-
else:
106-
section = stmt_to_section_mapping.get(stmt, "others")
107-
section_rank = order.index(section)
108-
if section_rank >= order.index(current_section):
109-
current_section = section
89+
current_section_rank = order.index(current_section)
90+
statement_section = _map_statement_to_section(statement)
91+
section_rank = order.index(statement_section)
92+
if section_rank >= current_section_rank:
93+
current_section = statement_section
11094
else:
11195
problems.append(
11296
Problem(
11397
name="class-definitions-order",
114-
description="Definition out of order in {}".format(class_name),
115-
line=class_child.line,
116-
column=class_child.column,
98+
description="Definition out of order in {}".format(a_class.name),
99+
line=statement.lark_node.line,
100+
column=statement.lark_node.column,
117101
)
118102
)
119103
return problems
120104

121105

122-
def _class_var_stmt_visibility(class_var_stmt) -> str:
106+
def _is_statement_irrelevant(statement: Statement) -> bool:
107+
if statement.kind == "pass_stmt":
108+
return True
109+
if statement.kind == "annotation":
110+
return Annotation(statement.lark_node).name != "tool"
111+
return False
112+
113+
114+
# pylint: disable-next=too-many-return-statements
115+
def _map_statement_to_section(statement: Statement) -> str:
116+
if statement.kind == "class_var_stmt":
117+
if any(
118+
annotation.name.startswith("export") for annotation in statement.annotations
119+
):
120+
return "exports"
121+
if any(annotation.name == "onready" for annotation in statement.annotations):
122+
return "onready{}vars".format(
123+
_class_var_stmt_visibility(statement.lark_node)
124+
)
125+
return "{}vars".format(_class_var_stmt_visibility(statement.lark_node))
126+
if statement.kind == "signal_stmt":
127+
return "signals"
128+
if statement.kind == "extends_stmt":
129+
return "extends"
130+
if statement.kind == "enum_stmt":
131+
return "enums"
132+
if statement.kind == "classname_stmt":
133+
return "classnames"
134+
if statement.kind == "const_stmt":
135+
return "consts"
136+
if (
137+
statement.kind == "annotation"
138+
and Annotation(statement.lark_node).name == "tool"
139+
):
140+
return "tools"
141+
if statement.kind == "class_def":
142+
return "others"
143+
if statement.kind == "func_def":
144+
return "others"
145+
raise NotImplementedError
146+
147+
148+
def _class_var_stmt_visibility(class_var_stmt: Tree) -> str:
123149
some_var_stmt = class_var_stmt.children[0]
124150
name_token = find_name_token_among_children(some_var_stmt)
125151
return "pub" if is_function_public(name_token.value) else "prv" # type: ignore

tests/linter/test_class_checks.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ def test_private_method_call_nok(code):
3131
simple_nok_check(code, 'private-method-call')
3232

3333

34-
@pytest.mark.skip(reason='to be fixed in a bundle')
3534
@pytest.mark.parametrize('code', [
3635
"""
3736
pass
@@ -40,11 +39,12 @@ def test_private_method_call_nok(code):
4039
signal s
4140
enum { A, B, C }
4241
const X = 1
43-
export var k = 1
42+
@export_group("Foo")
43+
@export var k = 1
4444
var x = 1
4545
var _x = 1
46-
onready var y = null
47-
onready var _y = null
46+
@onready var y = null
47+
@onready var _y = null
4848
class Z:
4949
pass
5050
extends Node
@@ -53,7 +53,7 @@ class Z:
5353
""",
5454
])
5555
def test_class_definitions_order_ok(code):
56-
simple_ok_check(code)
56+
simple_ok_check(code, disable="unnecessary-pass")
5757

5858

5959
@pytest.mark.parametrize('code', [
@@ -66,6 +66,27 @@ def test_class_definitions_order_ok(code):
6666
"""
6767
class X: var x;extends Node
6868
""",
69+
"""var _x
70+
var x
71+
""",
72+
"""@onready var x
73+
var y
74+
""",
75+
"""@onready var _x
76+
@onready var x
77+
""",
78+
"""var x
79+
enum X { A, B }
80+
""",
81+
"""var x
82+
class_name Asdf
83+
""",
84+
"""var x
85+
const X = 1
86+
""",
87+
"""var x
88+
@tool
89+
""",
6990
])
7091
def test_class_definitions_order_nok(code):
7192
simple_nok_check(code, 'class-definitions-order')

0 commit comments

Comments
 (0)