-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcode_free_ctor_visitor.py
More file actions
147 lines (133 loc) · 5.96 KB
/
code_free_ctor_visitor.py
File metadata and controls
147 lines (133 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# SPDX-FileCopyrightText: Copyright (c) 2023-2026 Almaz Ilaletdinov <a.ilaletdinov@yandex.ru>
# SPDX-License-Identifier: MIT
"""AssignmentOnlyCtorVisitor."""
import argparse
import ast
from typing import final
@final
class CodeFreeCtorVisitor(ast.NodeVisitor):
"""CodeFreeCtorVisitor."""
def __init__(self, options: argparse.Namespace) -> None:
"""Ctor."""
self.problems: list[tuple[int, int, str]] = []
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802, WPS231, C901
"""Visit by classes.
:param node: ast.ClassDef
"""
if self._is_enum_class(node):
self.generic_visit(node)
return
for elem in node.body:
if not isinstance(elem, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if elem.name == '__init__':
self._check_constructor_body(elem, 'PEO101 __init__ method should contain only assignments')
elif self._is_classmethod(elem):
self._check_constructor_body(elem, 'PEO102 @classmethod should contain only cls() call')
self.generic_visit(node)
def _is_enum_class(self, node: ast.ClassDef) -> bool:
for base in node.bases:
if (
(isinstance(base, ast.Name) and base.id.endswith('Enum'))
or (isinstance(base, ast.Attribute) and base.attr.endswith('Enum'))
):
return True
return False
def _is_classmethod(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
for decorator in node.decorator_list:
if (
(isinstance(decorator, ast.Name) and decorator.id == 'classmethod')
or (isinstance(decorator, ast.Attribute) and decorator.attr == 'classmethod')
):
return True
return False
def _check_constructor_body(self, node: ast.FunctionDef | ast.AsyncFunctionDef, error_message: str) -> None:
for body_elem in node.body:
if isinstance(body_elem, (ast.Assign, ast.AnnAssign)):
if node.name == '__init__' and not self._is_valid_assignment(body_elem, node):
self.problems.append((body_elem.lineno, body_elem.col_offset, error_message))
continue
elif isinstance(body_elem, ast.Return):
if body_elem.value is None:
if node.name == '__init__':
continue
else:
self.problems.append((body_elem.lineno, body_elem.col_offset, error_message))
else:
if self._is_classmethod(node) and isinstance(body_elem.value, ast.Call):
if self._is_valid_cls_call(body_elem.value, node) or self._is_constructor_call(body_elem.value):
continue
else:
self.problems.append((body_elem.lineno, body_elem.col_offset, error_message))
else:
self.problems.append((body_elem.lineno, body_elem.col_offset, error_message))
elif (
isinstance(body_elem, ast.Expr)
and isinstance(body_elem.value, ast.Constant)
and isinstance(body_elem.value.value, str)
):
continue
else:
self.problems.append((body_elem.lineno, body_elem.col_offset, error_message))
def _is_valid_cls_call(self, node: ast.Call, func_node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
if not isinstance(node.func, ast.Name) or node.func.id != 'cls':
return False
arg_names = {arg.arg for arg in func_node.args.args}
if func_node.args.vararg:
arg_names.add(func_node.args.vararg.arg)
if func_node.args.kwarg:
arg_names.add(func_node.args.kwarg.arg)
if func_node.args.kwonlyargs:
for kwarg in func_node.args.kwonlyargs:
arg_names.add(kwarg.arg)
for arg in node.args:
if isinstance(arg, ast.Name) and arg.id in arg_names:
continue
elif isinstance(arg, ast.Constant):
continue
elif isinstance(arg, ast.Call):
if self._is_constructor_call(arg):
continue
else:
return False
else:
return False
return True
def _is_constructor_call(self, node: ast.Call) -> bool:
if isinstance(node.func, ast.Name):
return node.func.id[0].isupper() if node.func.id else False
elif isinstance(node.func, ast.Attribute):
return self._is_class_reference(node.func.value)
return False
def _is_class_reference(self, node: ast.expr) -> bool:
if isinstance(node, ast.Name):
return node.id[0].isupper() if node.id else False
elif isinstance(node, ast.Attribute):
return self._is_class_reference(node.value)
return False
def _is_valid_assignment(
self,
node: ast.Assign | ast.AnnAssign,
func_node: ast.FunctionDef | ast.AsyncFunctionDef,
) -> bool:
arg_names = {arg.arg for arg in func_node.args.args}
if isinstance(node, ast.Assign):
for target in node.targets:
if (
(
isinstance(target, ast.Attribute)
and (isinstance(node.value, ast.Name) and node.value.id in arg_names)
)
or isinstance(node.value, ast.Constant)
):
return True
return False
elif isinstance(node, ast.AnnAssign):
return (
(
isinstance(node.target, ast.Attribute)
and (isinstance(node.value, ast.Name) and node.value.id in arg_names)
)
or isinstance(node.value, ast.Constant)
)
return False