flake8-elegant-objects 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flake8_elegant_objects/__init__.py +36 -0
- flake8_elegant_objects/__main__.py +68 -0
- flake8_elegant_objects/base.py +180 -0
- flake8_elegant_objects/no_constructor_code.py +45 -0
- flake8_elegant_objects/no_er_name.py +306 -0
- flake8_elegant_objects/no_getters_setters.py +57 -0
- flake8_elegant_objects/no_implementation_inheritance.py +75 -0
- flake8_elegant_objects/no_impure_tests.py +127 -0
- flake8_elegant_objects/no_mutable_objects.py +69 -0
- flake8_elegant_objects/no_null.py +50 -0
- flake8_elegant_objects/no_orm.py +97 -0
- flake8_elegant_objects/no_public_methods_without_contracts.py +125 -0
- flake8_elegant_objects/no_static.py +31 -0
- flake8_elegant_objects/no_type_discrimination.py +34 -0
- flake8_elegant_objects-1.0.0.dist-info/METADATA +216 -0
- flake8_elegant_objects-1.0.0.dist-info/RECORD +19 -0
- flake8_elegant_objects-1.0.0.dist-info/WHEEL +4 -0
- flake8_elegant_objects-1.0.0.dist-info/entry_points.txt +5 -0
- flake8_elegant_objects-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""No implementation inheritance principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoImplementationInheritance:
|
|
9
|
+
"""Checks for implementation inheritance violations (EO014)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for implementation inheritance violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
|
|
15
|
+
if isinstance(node, ast.ClassDef):
|
|
16
|
+
return self._check_implementation_inheritance(node)
|
|
17
|
+
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def _check_implementation_inheritance(self, node: ast.ClassDef) -> Violations:
|
|
21
|
+
"""Check for implementation inheritance violations."""
|
|
22
|
+
for base in node.bases:
|
|
23
|
+
is_abstract_base = False
|
|
24
|
+
|
|
25
|
+
if isinstance(base, ast.Name):
|
|
26
|
+
# Allow inheritance from abstract base classes and common patterns
|
|
27
|
+
allowed_bases = {
|
|
28
|
+
# Abstract bases
|
|
29
|
+
"ABC",
|
|
30
|
+
"Protocol",
|
|
31
|
+
# Exception hierarchy (standard pattern)
|
|
32
|
+
"Exception",
|
|
33
|
+
"BaseException",
|
|
34
|
+
"ValueError",
|
|
35
|
+
"TypeError",
|
|
36
|
+
"RuntimeError",
|
|
37
|
+
"AttributeError",
|
|
38
|
+
"KeyError",
|
|
39
|
+
"IndexError",
|
|
40
|
+
"ImportError",
|
|
41
|
+
"OSError",
|
|
42
|
+
# Standard library abstract bases
|
|
43
|
+
"Enum",
|
|
44
|
+
"IntEnum",
|
|
45
|
+
"Flag",
|
|
46
|
+
"IntFlag",
|
|
47
|
+
# Generic object (unavoidable in Python)
|
|
48
|
+
"object",
|
|
49
|
+
}
|
|
50
|
+
is_abstract_base = base.id in allowed_bases
|
|
51
|
+
|
|
52
|
+
elif isinstance(base, ast.Attribute):
|
|
53
|
+
# Check for module.AbstractBase patterns
|
|
54
|
+
if base.attr in {"Protocol", "ABC"}:
|
|
55
|
+
is_abstract_base = True
|
|
56
|
+
elif isinstance(base.value, ast.Name) and base.value.id in {
|
|
57
|
+
"abc",
|
|
58
|
+
"typing",
|
|
59
|
+
"collections",
|
|
60
|
+
"enum",
|
|
61
|
+
}:
|
|
62
|
+
is_abstract_base = True
|
|
63
|
+
# Check for imported ABC/Protocol
|
|
64
|
+
elif isinstance(base.value, ast.Name) and base.attr in {
|
|
65
|
+
"ABC",
|
|
66
|
+
"abstractmethod",
|
|
67
|
+
"Protocol",
|
|
68
|
+
}:
|
|
69
|
+
is_abstract_base = True
|
|
70
|
+
|
|
71
|
+
# If not an abstract base, it's implementation inheritance
|
|
72
|
+
if not is_abstract_base:
|
|
73
|
+
return violation(node, ErrorCodes.EO014.format(name=node.name))
|
|
74
|
+
|
|
75
|
+
return []
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""No impure tests principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoImpureTests:
|
|
9
|
+
"""Checks for impure test methods violations (EO012)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for impure test method violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
|
|
15
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
16
|
+
return self._check_test_methods(node)
|
|
17
|
+
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def _check_test_methods(
|
|
21
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
22
|
+
) -> Violations:
|
|
23
|
+
"""Check that test methods only contain single assertion statements."""
|
|
24
|
+
if not node.name.startswith("test_"):
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
violations = []
|
|
28
|
+
assertion_count = 0
|
|
29
|
+
|
|
30
|
+
for stmt in node.body:
|
|
31
|
+
if isinstance(stmt, ast.Pass):
|
|
32
|
+
continue # Allow pass statements
|
|
33
|
+
|
|
34
|
+
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
35
|
+
# Check if it's an assertion
|
|
36
|
+
if self._is_assertion_call(stmt.value):
|
|
37
|
+
assertion_count += 1
|
|
38
|
+
continue
|
|
39
|
+
else:
|
|
40
|
+
# Non-assertion expression call
|
|
41
|
+
violations.extend(
|
|
42
|
+
violation(stmt, ErrorCodes.EO012.format(name=node.name))
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
elif isinstance(stmt, ast.Assert):
|
|
46
|
+
# Direct assert statement
|
|
47
|
+
assertion_count += 1
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
elif isinstance(stmt, ast.With):
|
|
51
|
+
# Check for pytest.raises or similar context managers
|
|
52
|
+
if self._is_assertion_context_manager(stmt):
|
|
53
|
+
assertion_count += 1
|
|
54
|
+
continue
|
|
55
|
+
else:
|
|
56
|
+
violations.extend(
|
|
57
|
+
violation(stmt, ErrorCodes.EO012.format(name=node.name))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
# Any other statement (assignments, etc.) is a violation
|
|
62
|
+
violations.extend(
|
|
63
|
+
violation(stmt, ErrorCodes.EO012.format(name=node.name))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Test must have exactly one assertion
|
|
67
|
+
if assertion_count == 0:
|
|
68
|
+
violations.extend(violation(node, ErrorCodes.EO012.format(name=node.name)))
|
|
69
|
+
elif assertion_count > 1:
|
|
70
|
+
violations.extend(violation(node, ErrorCodes.EO012.format(name=node.name)))
|
|
71
|
+
|
|
72
|
+
return violations
|
|
73
|
+
|
|
74
|
+
def _is_assertion_call(self, call: ast.Call) -> bool:
|
|
75
|
+
"""Check if a call is an assertion."""
|
|
76
|
+
# Check for unittest style assertions (self.assertEqual, self.assertTrue, etc.)
|
|
77
|
+
if isinstance(call.func, ast.Attribute):
|
|
78
|
+
if call.func.attr.startswith("assert"):
|
|
79
|
+
return True
|
|
80
|
+
# Check for chained assertions like assertThat(...).isEqualTo(...)
|
|
81
|
+
if self._contains_assertion_in_chain(call):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Check for standalone assertion functions
|
|
85
|
+
if isinstance(call.func, ast.Name):
|
|
86
|
+
if call.func.id.startswith("assert") or call.func.id == "assertThat":
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _contains_assertion_in_chain(self, call: ast.Call) -> bool:
|
|
92
|
+
"""Check if assertion exists anywhere in the call chain."""
|
|
93
|
+
current = call
|
|
94
|
+
while isinstance(current, ast.Call):
|
|
95
|
+
if isinstance(current.func, ast.Name):
|
|
96
|
+
if (
|
|
97
|
+
current.func.id.startswith("assert")
|
|
98
|
+
or current.func.id == "assertThat"
|
|
99
|
+
):
|
|
100
|
+
return True
|
|
101
|
+
elif isinstance(current.func, ast.Attribute):
|
|
102
|
+
if (
|
|
103
|
+
current.func.attr.startswith("assert")
|
|
104
|
+
or current.func.attr == "assertThat"
|
|
105
|
+
):
|
|
106
|
+
return True
|
|
107
|
+
# Move to the next level in the chain
|
|
108
|
+
if isinstance(current.func.value, ast.Call):
|
|
109
|
+
current = current.func.value
|
|
110
|
+
else:
|
|
111
|
+
break
|
|
112
|
+
else:
|
|
113
|
+
break
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _is_assertion_context_manager(self, with_stmt: ast.With) -> bool:
|
|
117
|
+
"""Check if with statement is for assertions like pytest.raises."""
|
|
118
|
+
for item in with_stmt.items:
|
|
119
|
+
if isinstance(item.context_expr, ast.Call):
|
|
120
|
+
if isinstance(item.context_expr.func, ast.Attribute):
|
|
121
|
+
# Check for pytest.raises, unittest.assertRaises, etc.
|
|
122
|
+
if item.context_expr.func.attr in {"raises", "assertRaises"}:
|
|
123
|
+
return True
|
|
124
|
+
elif isinstance(item.context_expr.func, ast.Name):
|
|
125
|
+
if item.context_expr.func.id in {"raises", "assertRaises"}:
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""No mutable objects principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoMutableObjects:
|
|
9
|
+
"""Checks for mutable object violations (EO008)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for mutable object violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
if not isinstance(node, ast.ClassDef):
|
|
15
|
+
return []
|
|
16
|
+
return self._check_mutable_class(node)
|
|
17
|
+
|
|
18
|
+
def _check_mutable_class(self, node: ast.ClassDef) -> Violations:
|
|
19
|
+
"""Check for mutable class violations."""
|
|
20
|
+
violations = []
|
|
21
|
+
|
|
22
|
+
# Look for @dataclass decorator without frozen=True
|
|
23
|
+
has_dataclass = False
|
|
24
|
+
has_frozen = False
|
|
25
|
+
|
|
26
|
+
for decorator in node.decorator_list:
|
|
27
|
+
if isinstance(decorator, ast.Name) and decorator.id == "dataclass":
|
|
28
|
+
has_dataclass = True
|
|
29
|
+
elif isinstance(decorator, ast.Call):
|
|
30
|
+
if (
|
|
31
|
+
isinstance(decorator.func, ast.Name)
|
|
32
|
+
and decorator.func.id == "dataclass"
|
|
33
|
+
):
|
|
34
|
+
has_dataclass = True
|
|
35
|
+
# Check for frozen=True
|
|
36
|
+
for keyword in decorator.keywords:
|
|
37
|
+
if keyword.arg == "frozen" and isinstance(
|
|
38
|
+
keyword.value, ast.Constant
|
|
39
|
+
):
|
|
40
|
+
if keyword.value.value is True:
|
|
41
|
+
has_frozen = True
|
|
42
|
+
|
|
43
|
+
# If it's a dataclass without frozen=True, it's mutable
|
|
44
|
+
if has_dataclass and not has_frozen:
|
|
45
|
+
violations.extend(violation(node, ErrorCodes.EO008.format(name=node.name)))
|
|
46
|
+
|
|
47
|
+
# Check for mutable instance attributes in class body
|
|
48
|
+
for stmt in node.body:
|
|
49
|
+
if isinstance(stmt, ast.Assign):
|
|
50
|
+
for target in stmt.targets:
|
|
51
|
+
if isinstance(target, ast.Name):
|
|
52
|
+
# This is a class attribute, check if it's mutable
|
|
53
|
+
if self._is_mutable_type(stmt.value):
|
|
54
|
+
violations.extend(
|
|
55
|
+
violation(stmt, ErrorCodes.EO008.format(name=target.id))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return violations
|
|
59
|
+
|
|
60
|
+
def _is_mutable_type(self, node: ast.AST) -> bool:
|
|
61
|
+
"""Check if a node represents a mutable type."""
|
|
62
|
+
if isinstance(node, ast.List | ast.Dict | ast.Set):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
66
|
+
mutable_types = {"list", "dict", "set", "bytearray"}
|
|
67
|
+
return node.func.id in mutable_types
|
|
68
|
+
|
|
69
|
+
return False
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""No null principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoNull:
|
|
9
|
+
"""Checks for None usage violations (EO005)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for None usage violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
if isinstance(node, ast.Constant) and node.value is None:
|
|
15
|
+
# Skip None in type annotations
|
|
16
|
+
if self._is_in_type_annotation(node, source.tree):
|
|
17
|
+
return []
|
|
18
|
+
return violation(node, ErrorCodes.EO005)
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
def _is_in_type_annotation(
|
|
22
|
+
self, target_node: ast.AST, tree: ast.AST | None
|
|
23
|
+
) -> bool:
|
|
24
|
+
"""Check if the target node is within a type annotation context."""
|
|
25
|
+
if not tree:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
# Find all annotation contexts in the tree
|
|
29
|
+
for node in ast.walk(tree):
|
|
30
|
+
# Function return annotations
|
|
31
|
+
if (
|
|
32
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
33
|
+
and node.returns
|
|
34
|
+
):
|
|
35
|
+
if self._node_in_subtree(target_node, node.returns):
|
|
36
|
+
return True
|
|
37
|
+
# Parameter annotations
|
|
38
|
+
elif isinstance(node, ast.arg) and node.annotation:
|
|
39
|
+
if self._node_in_subtree(target_node, node.annotation):
|
|
40
|
+
return True
|
|
41
|
+
# Variable annotations
|
|
42
|
+
elif isinstance(node, ast.AnnAssign) and node.annotation:
|
|
43
|
+
if self._node_in_subtree(target_node, node.annotation):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def _node_in_subtree(self, target: ast.AST, tree: ast.AST) -> bool:
|
|
49
|
+
"""Check if target node is within the tree."""
|
|
50
|
+
return any(child is target for child in ast.walk(tree))
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""No ORM principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoOrm:
|
|
9
|
+
"""Checks for ORM/ActiveRecord pattern violations (EO013)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for ORM pattern violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
|
|
15
|
+
if isinstance(node, ast.Call):
|
|
16
|
+
return self._check_orm_patterns(node)
|
|
17
|
+
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def _check_orm_patterns(self, node: ast.Call) -> Violations:
|
|
21
|
+
"""Check for ORM/ActiveRecord patterns."""
|
|
22
|
+
if not isinstance(node.func, ast.Attribute):
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
orm_methods = {
|
|
26
|
+
"save",
|
|
27
|
+
"delete",
|
|
28
|
+
"destroy",
|
|
29
|
+
"update",
|
|
30
|
+
"create",
|
|
31
|
+
"reload",
|
|
32
|
+
"find",
|
|
33
|
+
"find_by",
|
|
34
|
+
"where",
|
|
35
|
+
"filter",
|
|
36
|
+
"filter_by",
|
|
37
|
+
"get",
|
|
38
|
+
"get_or_create",
|
|
39
|
+
"select",
|
|
40
|
+
"insert",
|
|
41
|
+
"update_all",
|
|
42
|
+
"delete_all",
|
|
43
|
+
"execute",
|
|
44
|
+
"query",
|
|
45
|
+
"order_by",
|
|
46
|
+
"group_by",
|
|
47
|
+
"having",
|
|
48
|
+
"limit",
|
|
49
|
+
"offset",
|
|
50
|
+
"join",
|
|
51
|
+
"includes",
|
|
52
|
+
"eager_load",
|
|
53
|
+
"preload",
|
|
54
|
+
"create_table",
|
|
55
|
+
"drop_table",
|
|
56
|
+
"add_column",
|
|
57
|
+
"remove_column",
|
|
58
|
+
}
|
|
59
|
+
if node.func.attr not in orm_methods:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
# Check if this is a valid non-ORM usage
|
|
63
|
+
if self._is_allowed_method_usage(node.func.value):
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
return violation(node, ErrorCodes.EO013.format(name=node.func.attr))
|
|
67
|
+
|
|
68
|
+
def _is_allowed_method_usage(self, value: ast.AST) -> bool:
|
|
69
|
+
"""Check if the method usage is allowed (not ORM)."""
|
|
70
|
+
# Built-in types
|
|
71
|
+
if isinstance(value, ast.Name) and value.id in {
|
|
72
|
+
"list",
|
|
73
|
+
"dict",
|
|
74
|
+
"set",
|
|
75
|
+
"tuple",
|
|
76
|
+
"str",
|
|
77
|
+
"int",
|
|
78
|
+
"float",
|
|
79
|
+
"bool",
|
|
80
|
+
}:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
# Allow methods on list/dict variables
|
|
84
|
+
if isinstance(value, ast.Name) and value.id.endswith("_list"):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Literal values
|
|
88
|
+
if isinstance(value, ast.Constant | ast.List | ast.Dict | ast.Tuple | ast.Set):
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# Constructor calls
|
|
92
|
+
return (
|
|
93
|
+
isinstance(value, ast.Call)
|
|
94
|
+
and isinstance(value.func, ast.Name)
|
|
95
|
+
and value.func.id
|
|
96
|
+
in {"open", "int", "str", "list", "dict", "set", "tuple", "bool", "float"}
|
|
97
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""No public methods without contracts principle checker for Python."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Principle, Source, Violations, is_method, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoPublicMethodsWithoutContracts(Principle):
|
|
9
|
+
"""Check that public methods are defined by contracts (Protocol/ABC)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check for public methods without contracts."""
|
|
13
|
+
violations: Violations = []
|
|
14
|
+
|
|
15
|
+
if not isinstance(source.node, ast.FunctionDef):
|
|
16
|
+
return violations
|
|
17
|
+
|
|
18
|
+
if not source.current_class or not is_method(source.node):
|
|
19
|
+
return violations
|
|
20
|
+
|
|
21
|
+
if source.node.name.startswith("_"):
|
|
22
|
+
return violations
|
|
23
|
+
|
|
24
|
+
if source.node.name.startswith("__") and source.node.name.endswith("__"):
|
|
25
|
+
return violations
|
|
26
|
+
if self._class_has_contract(source.current_class, source.tree):
|
|
27
|
+
if not self._method_from_contract(
|
|
28
|
+
source.node.name, source.current_class, source.tree
|
|
29
|
+
):
|
|
30
|
+
violations.extend(
|
|
31
|
+
violation(
|
|
32
|
+
source.node, ErrorCodes.EO011.format(name=source.node.name)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
violations.extend(
|
|
37
|
+
violation(source.node, ErrorCodes.EO011.format(name=source.node.name))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return violations
|
|
41
|
+
|
|
42
|
+
def _class_has_contract(
|
|
43
|
+
self, class_node: ast.ClassDef, tree: ast.AST | None
|
|
44
|
+
) -> bool:
|
|
45
|
+
"""Check if class implements any Protocol or ABC."""
|
|
46
|
+
if not class_node.bases:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
for base in class_node.bases:
|
|
50
|
+
base_name = self._get_base_name(base)
|
|
51
|
+
if not base_name:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if self._is_protocol_or_abc(base_name, tree):
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def _method_from_contract(
|
|
60
|
+
self, method_name: str, class_node: ast.ClassDef, tree: ast.AST | None
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""Check if method is defined in any of the class's contracts."""
|
|
63
|
+
for base in class_node.bases:
|
|
64
|
+
base_name = self._get_base_name(base)
|
|
65
|
+
if not base_name:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
base_class = self._find_class_def(base_name, tree)
|
|
69
|
+
if not base_class:
|
|
70
|
+
if self._is_protocol_or_abc(base_name, tree):
|
|
71
|
+
return True
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if self._has_method(base_class, method_name):
|
|
75
|
+
if self._is_protocol_or_abc(base_name, tree):
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def _get_base_name(self, base: ast.expr) -> str | None:
|
|
81
|
+
"""Extract base class name from AST node."""
|
|
82
|
+
if isinstance(base, ast.Name):
|
|
83
|
+
return base.id
|
|
84
|
+
elif isinstance(base, ast.Attribute):
|
|
85
|
+
return base.attr
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def _is_protocol_or_abc(self, class_name: str, tree: ast.AST | None) -> bool:
|
|
89
|
+
"""Check if a class is a Protocol or ABC."""
|
|
90
|
+
if class_name in ("Protocol", "ABC", "ABCMeta"):
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
if class_name.endswith("Protocol") or class_name.endswith("ABC"):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
if tree:
|
|
97
|
+
class_def = self._find_class_def(class_name, tree)
|
|
98
|
+
if class_def:
|
|
99
|
+
for base in class_def.bases:
|
|
100
|
+
base_name = self._get_base_name(base)
|
|
101
|
+
if base_name and self._is_protocol_or_abc(base_name, None):
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def _find_class_def(
|
|
107
|
+
self, class_name: str, tree: ast.AST | None
|
|
108
|
+
) -> ast.ClassDef | None:
|
|
109
|
+
"""Find class definition in the AST tree."""
|
|
110
|
+
if not tree:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
for node in ast.walk(tree):
|
|
114
|
+
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
115
|
+
return node
|
|
116
|
+
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def _has_method(self, class_node: ast.ClassDef, method_name: str) -> bool:
|
|
120
|
+
"""Check if class has a method with given name."""
|
|
121
|
+
for node in class_node.body:
|
|
122
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
123
|
+
if node.name == method_name:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""No static methods principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoStatic:
|
|
9
|
+
"""Checks for static method violations (EO009)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for static method violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
|
|
15
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
16
|
+
return self._check_static_methods(node)
|
|
17
|
+
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def _check_static_methods(
|
|
21
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
22
|
+
) -> Violations:
|
|
23
|
+
"""Check for static methods violations."""
|
|
24
|
+
# Check for @staticmethod decorator
|
|
25
|
+
for decorator in node.decorator_list:
|
|
26
|
+
if isinstance(decorator, ast.Name) and decorator.id in {
|
|
27
|
+
"staticmethod",
|
|
28
|
+
"classmethod",
|
|
29
|
+
}:
|
|
30
|
+
return violation(node, ErrorCodes.EO009.format(name=node.name))
|
|
31
|
+
return []
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""No type discrimination principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoTypeDiscrimination:
|
|
9
|
+
"""Checks for type discrimination violations (EO010)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for type discrimination violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
|
|
15
|
+
if isinstance(node, ast.Call):
|
|
16
|
+
return self._check_isinstance_usage(node)
|
|
17
|
+
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
def _check_isinstance_usage(self, node: ast.Call) -> Violations:
|
|
21
|
+
"""Check for isinstance, type casting, or reflection usage."""
|
|
22
|
+
if isinstance(node.func, ast.Name):
|
|
23
|
+
forbidden_funcs = {
|
|
24
|
+
"isinstance",
|
|
25
|
+
"type",
|
|
26
|
+
"hasattr",
|
|
27
|
+
"getattr",
|
|
28
|
+
"setattr",
|
|
29
|
+
"delattr",
|
|
30
|
+
"callable",
|
|
31
|
+
}
|
|
32
|
+
if node.func.id in forbidden_funcs:
|
|
33
|
+
return violation(node, ErrorCodes.EO010)
|
|
34
|
+
return []
|