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,36 @@
|
|
|
1
|
+
"""Flake8 plugin for Elegant Objects violations.
|
|
2
|
+
|
|
3
|
+
This plugin detects violations of the Elegant Objects principles including
|
|
4
|
+
"-er" entities, null usage, mutable objects, static methods, and more.
|
|
5
|
+
|
|
6
|
+
Based on Yegor Bugayenko's principles from elegantobjects.org
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import ElegantObjectsCore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ElegantObjectsPlugin:
|
|
17
|
+
"""Flake8 plugin to check for Elegant Objects violations."""
|
|
18
|
+
|
|
19
|
+
name = "flake8-elegant-objects"
|
|
20
|
+
version = "1.0.0"
|
|
21
|
+
|
|
22
|
+
def __init__(self, tree: ast.AST) -> None:
|
|
23
|
+
self.tree = tree
|
|
24
|
+
self._core = ElegantObjectsCore(tree)
|
|
25
|
+
|
|
26
|
+
def run(self) -> Iterator[tuple[int, int, str, type["ElegantObjectsPlugin"]]]:
|
|
27
|
+
"""Run the checker and yield errors."""
|
|
28
|
+
violations = self._core.check_violations()
|
|
29
|
+
for violation in violations:
|
|
30
|
+
yield (violation.line, violation.column, violation.message, type(self))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Entry point for flake8 plugin registration
|
|
34
|
+
def factory(_app: Any) -> type[ElegantObjectsPlugin]:
|
|
35
|
+
"""Factory function for flake8 plugin."""
|
|
36
|
+
return ElegantObjectsPlugin
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Standalone command-line interface for flake8-elegant-objects."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ast
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .base import ElegantObjectsCore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
"""Standalone command-line interface."""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description="Check Python files for Elegant Objects violations"
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("files", nargs="+", help="Python files to check")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--show-source",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Show source code context for violations",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
total_errors = 0
|
|
25
|
+
|
|
26
|
+
for file_path in args.files:
|
|
27
|
+
if not file_path.endswith(".py"):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with open(file_path, encoding="utf-8") as f:
|
|
32
|
+
source = f.read()
|
|
33
|
+
|
|
34
|
+
tree = ast.parse(source, filename=file_path)
|
|
35
|
+
core = ElegantObjectsCore(tree)
|
|
36
|
+
|
|
37
|
+
file_errors = 0
|
|
38
|
+
violations = core.check_violations()
|
|
39
|
+
|
|
40
|
+
for violation in violations:
|
|
41
|
+
print(
|
|
42
|
+
f"{file_path}:{violation.line}:{violation.column}: {violation.message}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if args.show_source:
|
|
46
|
+
lines = source.split("\n")
|
|
47
|
+
if 0 <= violation.line - 1 < len(lines):
|
|
48
|
+
print(f" {lines[violation.line - 1].strip()}")
|
|
49
|
+
print()
|
|
50
|
+
|
|
51
|
+
file_errors += 1
|
|
52
|
+
total_errors += 1
|
|
53
|
+
|
|
54
|
+
if file_errors == 0:
|
|
55
|
+
print(f"{file_path}: No violations found ✓")
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Error processing {file_path}: {e}", file=sys.stderr)
|
|
59
|
+
|
|
60
|
+
if total_errors > 0:
|
|
61
|
+
print(f"\nTotal violations found: {total_errors}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
else:
|
|
64
|
+
print("\nAll files comply with Elegant Objects principles! ✓")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Base classes and protocols for Elegant Objects checkers."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ErrorCodes:
|
|
8
|
+
"""Centralized error message definitions."""
|
|
9
|
+
|
|
10
|
+
# Naming violations (EO001-EO004)
|
|
11
|
+
EO001 = "EO001 Class name '{name}' violates -er principle (describes what it does, not what it is)"
|
|
12
|
+
EO002 = (
|
|
13
|
+
"EO002 Method name '{name}' violates -er principle (should be noun, not verb)"
|
|
14
|
+
)
|
|
15
|
+
EO003 = (
|
|
16
|
+
"EO003 Variable name '{name}' violates -er principle (should be noun, not verb)"
|
|
17
|
+
)
|
|
18
|
+
EO004 = (
|
|
19
|
+
"EO004 Function name '{name}' violates -er principle (should be noun, not verb)"
|
|
20
|
+
)
|
|
21
|
+
EO005 = "EO005 Null (None) usage violates EO principle (avoid None)"
|
|
22
|
+
EO006 = "EO006 Code in constructor violates EO principle (constructors should only assign parameters)"
|
|
23
|
+
EO007 = "EO007 Getter/setter method '{name}' violates EO principle (avoid getters/setters)"
|
|
24
|
+
EO008 = "EO008 Mutable object violation: '{name}' should be immutable"
|
|
25
|
+
EO009 = (
|
|
26
|
+
"EO009 Static method '{name}' violates EO principle (no static methods allowed)"
|
|
27
|
+
)
|
|
28
|
+
EO010 = "EO010 isinstance/type casting violates EO principle (avoid type discrimination)"
|
|
29
|
+
EO011 = "EO011 Public method '{name}' without contract (Protocol/ABC) violates EO principle"
|
|
30
|
+
EO012 = "EO012 Test method '{name}' contains non-assertThat statements (only assertThat allowed)"
|
|
31
|
+
EO013 = "EO013 ORM/ActiveRecord pattern '{name}' violates EO principle"
|
|
32
|
+
EO014 = "EO014 Implementation inheritance violates EO principle (class '{name}' inherits from non-abstract class)"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Violation:
|
|
36
|
+
"""Represents a detected violation."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, line: int, column: int, message: str) -> None:
|
|
39
|
+
self._line = line
|
|
40
|
+
self._column = column
|
|
41
|
+
self._message = message
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def line(self) -> int:
|
|
45
|
+
return self._line
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def column(self) -> int:
|
|
49
|
+
return self._column
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def message(self) -> str:
|
|
53
|
+
return self._message
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Violations = list[Violation]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Source:
|
|
60
|
+
"""Aggregation of AST node and current class context."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
node: ast.AST,
|
|
65
|
+
current_class: ast.ClassDef | None = None,
|
|
66
|
+
tree: ast.AST | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self._node = node
|
|
69
|
+
self._current_class = current_class
|
|
70
|
+
self._tree = tree
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def node(self) -> ast.AST:
|
|
74
|
+
return self._node
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def current_class(self) -> ast.ClassDef | None:
|
|
78
|
+
return self._current_class
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def tree(self) -> ast.AST | None:
|
|
82
|
+
return self._tree
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Principle(Protocol):
|
|
86
|
+
"""Protocol for Elegant Objects principles analysis."""
|
|
87
|
+
|
|
88
|
+
def check(self, source: Source) -> Violations:
|
|
89
|
+
"""Check source for violations and return list of detected violations."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def violation(node: ast.AST, message: str) -> Violations:
|
|
94
|
+
"""Create a violation if node has location information."""
|
|
95
|
+
if hasattr(node, "lineno") and hasattr(node, "col_offset"):
|
|
96
|
+
return [Violation(node.lineno, node.col_offset, message)]
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
101
|
+
"""Check if function is a method (has self parameter)."""
|
|
102
|
+
if not node.args.args:
|
|
103
|
+
return False
|
|
104
|
+
return node.args.args[0].arg in ("self", "cls")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_all_principles() -> list[Principle]:
|
|
108
|
+
"""Get all available Elegant Objects principle checkers."""
|
|
109
|
+
# Import here to avoid circular imports
|
|
110
|
+
from .no_constructor_code import NoConstructorCode
|
|
111
|
+
from .no_er_name import NoErName
|
|
112
|
+
from .no_getters_setters import NoGettersSetters
|
|
113
|
+
from .no_implementation_inheritance import NoImplementationInheritance
|
|
114
|
+
from .no_impure_tests import NoImpureTests
|
|
115
|
+
from .no_mutable_objects import NoMutableObjects
|
|
116
|
+
from .no_null import NoNull
|
|
117
|
+
from .no_orm import NoOrm
|
|
118
|
+
from .no_public_methods_without_contracts import NoPublicMethodsWithoutContracts
|
|
119
|
+
from .no_static import NoStatic
|
|
120
|
+
from .no_type_discrimination import NoTypeDiscrimination
|
|
121
|
+
|
|
122
|
+
return [
|
|
123
|
+
NoErName(),
|
|
124
|
+
NoNull(),
|
|
125
|
+
NoConstructorCode(),
|
|
126
|
+
NoGettersSetters(),
|
|
127
|
+
NoMutableObjects(),
|
|
128
|
+
NoStatic(),
|
|
129
|
+
NoTypeDiscrimination(),
|
|
130
|
+
NoPublicMethodsWithoutContracts(),
|
|
131
|
+
NoImpureTests(),
|
|
132
|
+
NoOrm(),
|
|
133
|
+
NoImplementationInheritance(),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ElegantObjectsCore:
|
|
138
|
+
"""Core analyzer for Elegant Objects violations."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, tree: ast.AST) -> None:
|
|
141
|
+
self.tree = tree
|
|
142
|
+
|
|
143
|
+
def check_violations(self) -> list[Violation]:
|
|
144
|
+
"""Check for all violations in the AST tree."""
|
|
145
|
+
violations = []
|
|
146
|
+
for violation in self._visit(self.tree, None):
|
|
147
|
+
violations.append(violation)
|
|
148
|
+
return violations
|
|
149
|
+
|
|
150
|
+
def _visit(
|
|
151
|
+
self, node: ast.AST, current_class: ast.ClassDef | None = None
|
|
152
|
+
) -> list[Violation]:
|
|
153
|
+
"""Visit AST nodes and check for violations."""
|
|
154
|
+
violations = []
|
|
155
|
+
|
|
156
|
+
if isinstance(node, ast.ClassDef):
|
|
157
|
+
current_class = node
|
|
158
|
+
|
|
159
|
+
# Check principles on current node
|
|
160
|
+
violations.extend(self._check_principles(node, current_class))
|
|
161
|
+
|
|
162
|
+
# Visit child nodes
|
|
163
|
+
for child in ast.iter_child_nodes(node):
|
|
164
|
+
violations.extend(self._visit(child, current_class))
|
|
165
|
+
|
|
166
|
+
return violations
|
|
167
|
+
|
|
168
|
+
def _check_principles(
|
|
169
|
+
self, node: ast.AST, current_class: ast.ClassDef | None
|
|
170
|
+
) -> list[Violation]:
|
|
171
|
+
"""Check all principles against the given node."""
|
|
172
|
+
violations = []
|
|
173
|
+
source = Source(node, current_class, self.tree)
|
|
174
|
+
principles = get_all_principles()
|
|
175
|
+
|
|
176
|
+
for principle in principles:
|
|
177
|
+
principle_violations = principle.check(source)
|
|
178
|
+
violations.extend(principle_violations)
|
|
179
|
+
|
|
180
|
+
return violations
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""No constructor code principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, is_method, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoConstructorCode:
|
|
9
|
+
"""Checks for code in constructors beyond parameter assignments (EO006)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for constructor code violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
15
|
+
return []
|
|
16
|
+
return self._check_constructor_code(node)
|
|
17
|
+
|
|
18
|
+
def _check_constructor_code(
|
|
19
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
20
|
+
) -> Violations:
|
|
21
|
+
"""Check for code in constructors beyond parameter assignments."""
|
|
22
|
+
if node.name != "__init__" or not is_method(node):
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
violations = []
|
|
26
|
+
# Constructors should only contain assignments to self.attribute = parameter
|
|
27
|
+
for stmt in node.body:
|
|
28
|
+
if isinstance(stmt, ast.Assign):
|
|
29
|
+
# Check if it's a simple self.attr = param assignment
|
|
30
|
+
if len(stmt.targets) == 1 and isinstance(
|
|
31
|
+
stmt.targets[0], ast.Attribute
|
|
32
|
+
):
|
|
33
|
+
target = stmt.targets[0]
|
|
34
|
+
if isinstance(target.value, ast.Name) and target.value.id == "self":
|
|
35
|
+
# This is a self.attr assignment, check if value is a simple name
|
|
36
|
+
if not isinstance(stmt.value, ast.Name):
|
|
37
|
+
violations.extend(violation(stmt, ErrorCodes.EO006))
|
|
38
|
+
else:
|
|
39
|
+
violations.extend(violation(stmt, ErrorCodes.EO006))
|
|
40
|
+
else:
|
|
41
|
+
violations.extend(violation(stmt, ErrorCodes.EO006))
|
|
42
|
+
elif not isinstance(stmt, ast.Pass): # Allow pass statements
|
|
43
|
+
violations.extend(violation(stmt, ErrorCodes.EO006))
|
|
44
|
+
|
|
45
|
+
return violations
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Naming violations checker for Elegant Objects principles."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from .base import ErrorCodes, Source, Violations, is_method, violation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NoErName:
|
|
11
|
+
"""Checks for naming violations in classes, methods, variables, and functions."""
|
|
12
|
+
|
|
13
|
+
# Hall of shame: common -er suffixes (from elegantobjects.org)
|
|
14
|
+
ER_SUFFIXES: ClassVar[set[str]] = {
|
|
15
|
+
"accumulator",
|
|
16
|
+
"adapter",
|
|
17
|
+
"aggregator",
|
|
18
|
+
"analyzer",
|
|
19
|
+
"broker",
|
|
20
|
+
"builder",
|
|
21
|
+
"calculator",
|
|
22
|
+
"checker",
|
|
23
|
+
"collector",
|
|
24
|
+
"compiler",
|
|
25
|
+
"compressor",
|
|
26
|
+
"consumer",
|
|
27
|
+
"controller",
|
|
28
|
+
"converter",
|
|
29
|
+
"coordinator",
|
|
30
|
+
"creator",
|
|
31
|
+
"decoder",
|
|
32
|
+
"decompressor",
|
|
33
|
+
"deserializer",
|
|
34
|
+
"dispatcher",
|
|
35
|
+
"displayer",
|
|
36
|
+
"encoder",
|
|
37
|
+
"evaluator",
|
|
38
|
+
"executor",
|
|
39
|
+
"exporter",
|
|
40
|
+
"factory",
|
|
41
|
+
"fetcher",
|
|
42
|
+
"filter",
|
|
43
|
+
"finder",
|
|
44
|
+
"formatter",
|
|
45
|
+
"generator",
|
|
46
|
+
"handler",
|
|
47
|
+
"helper",
|
|
48
|
+
"importer",
|
|
49
|
+
"interpreter",
|
|
50
|
+
"joiner",
|
|
51
|
+
"listener",
|
|
52
|
+
"loader",
|
|
53
|
+
"manager",
|
|
54
|
+
"mediator",
|
|
55
|
+
"merger",
|
|
56
|
+
"monitor",
|
|
57
|
+
"observer",
|
|
58
|
+
"orchestrator",
|
|
59
|
+
"organizer",
|
|
60
|
+
"parser",
|
|
61
|
+
"printer",
|
|
62
|
+
"processor",
|
|
63
|
+
"producer",
|
|
64
|
+
"provider",
|
|
65
|
+
"reader",
|
|
66
|
+
"renderer",
|
|
67
|
+
"reporter",
|
|
68
|
+
"router",
|
|
69
|
+
"runner",
|
|
70
|
+
"saver",
|
|
71
|
+
"scanner",
|
|
72
|
+
"scheduler",
|
|
73
|
+
"serializer",
|
|
74
|
+
"sorter",
|
|
75
|
+
"splitter",
|
|
76
|
+
"supplier",
|
|
77
|
+
"synchronizer",
|
|
78
|
+
"tracker",
|
|
79
|
+
"transformer",
|
|
80
|
+
"validator",
|
|
81
|
+
"worker",
|
|
82
|
+
"wrapper",
|
|
83
|
+
"writer",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Common procedural verbs that should be nouns
|
|
87
|
+
PROCEDURAL_VERBS: ClassVar[set[str]] = {
|
|
88
|
+
"accumulate",
|
|
89
|
+
"add",
|
|
90
|
+
"aggregate",
|
|
91
|
+
"analyze",
|
|
92
|
+
"append",
|
|
93
|
+
"build",
|
|
94
|
+
"calculate",
|
|
95
|
+
"change",
|
|
96
|
+
"check",
|
|
97
|
+
"clean",
|
|
98
|
+
"clear",
|
|
99
|
+
"close",
|
|
100
|
+
"collect",
|
|
101
|
+
"compile",
|
|
102
|
+
"compress",
|
|
103
|
+
"control",
|
|
104
|
+
"convert",
|
|
105
|
+
"create",
|
|
106
|
+
"decode",
|
|
107
|
+
"decompress",
|
|
108
|
+
"delete",
|
|
109
|
+
"deserialize",
|
|
110
|
+
"dispatch",
|
|
111
|
+
"display",
|
|
112
|
+
"do",
|
|
113
|
+
"encode",
|
|
114
|
+
"evaluate",
|
|
115
|
+
"execute",
|
|
116
|
+
"export",
|
|
117
|
+
"fetch",
|
|
118
|
+
"filter",
|
|
119
|
+
"find",
|
|
120
|
+
"format",
|
|
121
|
+
"generate",
|
|
122
|
+
"get",
|
|
123
|
+
"handle",
|
|
124
|
+
"hide",
|
|
125
|
+
"import",
|
|
126
|
+
"insert",
|
|
127
|
+
"interpret",
|
|
128
|
+
"join",
|
|
129
|
+
"load",
|
|
130
|
+
"manage",
|
|
131
|
+
"merge",
|
|
132
|
+
"modify",
|
|
133
|
+
"open",
|
|
134
|
+
"organize",
|
|
135
|
+
"parse",
|
|
136
|
+
"pause",
|
|
137
|
+
"prepend",
|
|
138
|
+
"print",
|
|
139
|
+
"process",
|
|
140
|
+
"put",
|
|
141
|
+
"read",
|
|
142
|
+
"receive",
|
|
143
|
+
"refresh",
|
|
144
|
+
"remove",
|
|
145
|
+
"render",
|
|
146
|
+
"reset",
|
|
147
|
+
"resume",
|
|
148
|
+
"retrieve",
|
|
149
|
+
"route",
|
|
150
|
+
"run",
|
|
151
|
+
"save",
|
|
152
|
+
"schedule",
|
|
153
|
+
"search",
|
|
154
|
+
"send",
|
|
155
|
+
"serialize",
|
|
156
|
+
"set",
|
|
157
|
+
"show",
|
|
158
|
+
"sort",
|
|
159
|
+
"split",
|
|
160
|
+
"start",
|
|
161
|
+
"stop",
|
|
162
|
+
"store",
|
|
163
|
+
"transform",
|
|
164
|
+
"transmit",
|
|
165
|
+
"update",
|
|
166
|
+
"validate",
|
|
167
|
+
"write",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Allowed exceptions (common patterns that are OK)
|
|
171
|
+
ALLOWED_EXCEPTIONS: ClassVar[set[str]] = {
|
|
172
|
+
"buffer",
|
|
173
|
+
"character",
|
|
174
|
+
"cluster",
|
|
175
|
+
"container",
|
|
176
|
+
"counter",
|
|
177
|
+
"error",
|
|
178
|
+
"footer",
|
|
179
|
+
"header",
|
|
180
|
+
"identifier",
|
|
181
|
+
"number",
|
|
182
|
+
"order",
|
|
183
|
+
"owner",
|
|
184
|
+
"parameter",
|
|
185
|
+
"pointer",
|
|
186
|
+
"register",
|
|
187
|
+
"server",
|
|
188
|
+
"timer",
|
|
189
|
+
"user",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def check(self, source: Source) -> Violations:
|
|
193
|
+
"""Check source for naming violations."""
|
|
194
|
+
violations = []
|
|
195
|
+
node = source.node
|
|
196
|
+
|
|
197
|
+
if isinstance(node, ast.ClassDef):
|
|
198
|
+
violations.extend(self._check_class_name(node))
|
|
199
|
+
elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
200
|
+
violations.extend(self._check_function_name(node))
|
|
201
|
+
elif isinstance(node, ast.Assign):
|
|
202
|
+
violations.extend(self._check_variable_assignment(node))
|
|
203
|
+
elif isinstance(node, ast.AnnAssign):
|
|
204
|
+
violations.extend(self._check_annotated_assignment(node))
|
|
205
|
+
|
|
206
|
+
return violations
|
|
207
|
+
|
|
208
|
+
def _check_class_name(self, node: ast.ClassDef) -> Violations:
|
|
209
|
+
"""Check if class name violates -er principle."""
|
|
210
|
+
name = node.name.lower()
|
|
211
|
+
|
|
212
|
+
# Skip if it's an allowed exception
|
|
213
|
+
if name in self.ALLOWED_EXCEPTIONS:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
# Check for -er suffixes (the hall of shame)
|
|
217
|
+
for suffix in self.ER_SUFFIXES:
|
|
218
|
+
if name.endswith(suffix):
|
|
219
|
+
return violation(node, ErrorCodes.EO001.format(name=node.name))
|
|
220
|
+
|
|
221
|
+
# Check for procedural patterns in compound names
|
|
222
|
+
if self._contains_procedural_pattern(name):
|
|
223
|
+
return violation(node, ErrorCodes.EO001.format(name=node.name))
|
|
224
|
+
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
def _check_function_name(
|
|
228
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
229
|
+
) -> Violations:
|
|
230
|
+
"""Check if function/method name violates -er principle."""
|
|
231
|
+
# Skip special methods (__init__, __str__, etc.)
|
|
232
|
+
if node.name.startswith("_"):
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
# Skip common property patterns
|
|
236
|
+
if node.name in ("property", "getter", "setter"):
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
# Check for procedural verbs
|
|
240
|
+
if self._starts_with_procedural_verb(node.name):
|
|
241
|
+
# Determine if it's a method or standalone function
|
|
242
|
+
error_code = ErrorCodes.EO002 if is_method(node) else ErrorCodes.EO004
|
|
243
|
+
return violation(node, error_code.format(name=node.name))
|
|
244
|
+
|
|
245
|
+
return []
|
|
246
|
+
|
|
247
|
+
def _check_variable_assignment(self, node: ast.Assign) -> Violations:
|
|
248
|
+
"""Check variable names in assignments."""
|
|
249
|
+
violations = []
|
|
250
|
+
for target in node.targets:
|
|
251
|
+
if isinstance(target, ast.Name):
|
|
252
|
+
violations.extend(self._check_variable_name(target))
|
|
253
|
+
return violations
|
|
254
|
+
|
|
255
|
+
def _check_annotated_assignment(self, node: ast.AnnAssign) -> Violations:
|
|
256
|
+
"""Check variable names in annotated assignments."""
|
|
257
|
+
if isinstance(node.target, ast.Name):
|
|
258
|
+
return self._check_variable_name(node.target)
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
def _check_variable_name(self, node: ast.Name) -> Violations:
|
|
262
|
+
"""Check if variable name violates -er principle."""
|
|
263
|
+
# Skip private variables and common patterns
|
|
264
|
+
if node.id.startswith("_") or node.id.isupper():
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
name = node.id.lower()
|
|
268
|
+
|
|
269
|
+
# Skip if it's an allowed exception
|
|
270
|
+
if name in self.ALLOWED_EXCEPTIONS:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
# Check for -er suffixes
|
|
274
|
+
for suffix in self.ER_SUFFIXES:
|
|
275
|
+
if name.endswith(suffix):
|
|
276
|
+
return violation(node, ErrorCodes.EO003.format(name=node.id))
|
|
277
|
+
|
|
278
|
+
# Check for procedural verbs as variable names
|
|
279
|
+
if self._starts_with_procedural_verb(name):
|
|
280
|
+
return violation(node, ErrorCodes.EO003.format(name=node.id))
|
|
281
|
+
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
def _contains_procedural_pattern(self, name: str) -> bool:
|
|
285
|
+
"""Check if name contains procedural patterns."""
|
|
286
|
+
# Split camelCase/snake_case into words
|
|
287
|
+
words = re.findall(r"[a-z]+", name)
|
|
288
|
+
|
|
289
|
+
# Check if any word is a procedural verb
|
|
290
|
+
return any(word in self.PROCEDURAL_VERBS for word in words)
|
|
291
|
+
|
|
292
|
+
def _starts_with_procedural_verb(self, name: str) -> bool:
|
|
293
|
+
"""Check if name starts with a procedural verb."""
|
|
294
|
+
# Split camelCase/snake_case and check first word
|
|
295
|
+
# First split on underscores, then on camelCase boundaries
|
|
296
|
+
words: list[str] = []
|
|
297
|
+
for part in name.split("_"):
|
|
298
|
+
# Split camelCase: insert space before uppercase letters
|
|
299
|
+
camel_split = re.sub(r"([a-z])([A-Z])", r"\1 \2", part)
|
|
300
|
+
words.extend(word.lower() for word in camel_split.split() if word)
|
|
301
|
+
|
|
302
|
+
if not words:
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
first_word = words[0]
|
|
306
|
+
return first_word in self.PROCEDURAL_VERBS
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""No getters/setters principle checker for Elegant Objects violations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .base import ErrorCodes, Source, Violations, is_method, violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoGettersSetters:
|
|
9
|
+
"""Checks for getter/setter methods (EO007)."""
|
|
10
|
+
|
|
11
|
+
def check(self, source: Source) -> Violations:
|
|
12
|
+
"""Check source for getter/setter violations."""
|
|
13
|
+
node = source.node
|
|
14
|
+
if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
15
|
+
return []
|
|
16
|
+
return self._check_getters_setters(node)
|
|
17
|
+
|
|
18
|
+
def _check_getters_setters(
|
|
19
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
20
|
+
) -> Violations:
|
|
21
|
+
"""Check for getter/setter methods."""
|
|
22
|
+
if not is_method(node) or node.name.startswith("_"):
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
# Skip methods with @property decorator
|
|
26
|
+
for decorator in node.decorator_list:
|
|
27
|
+
if isinstance(decorator, ast.Name) and decorator.id == "property":
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
name = node.name.lower()
|
|
31
|
+
original_name = node.name
|
|
32
|
+
|
|
33
|
+
# Check for getter patterns
|
|
34
|
+
if (
|
|
35
|
+
name.startswith("get_")
|
|
36
|
+
or (
|
|
37
|
+
name.startswith("get")
|
|
38
|
+
and len(original_name) > 3
|
|
39
|
+
and original_name[3].isupper()
|
|
40
|
+
)
|
|
41
|
+
or name == "get"
|
|
42
|
+
):
|
|
43
|
+
return violation(node, ErrorCodes.EO007.format(name=node.name))
|
|
44
|
+
|
|
45
|
+
# Check for setter patterns
|
|
46
|
+
if (
|
|
47
|
+
name.startswith("set_")
|
|
48
|
+
or (
|
|
49
|
+
name.startswith("set")
|
|
50
|
+
and len(original_name) > 3
|
|
51
|
+
and original_name[3].isupper()
|
|
52
|
+
)
|
|
53
|
+
or name == "set"
|
|
54
|
+
):
|
|
55
|
+
return violation(node, ErrorCodes.EO007.format(name=node.name))
|
|
56
|
+
|
|
57
|
+
return []
|