thailint 0.8.0__py3-none-any.whl → 0.10.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.
- src/cli.py +242 -0
- src/config.py +2 -3
- src/core/base.py +4 -0
- src/core/rule_discovery.py +143 -84
- src/core/violation_builder.py +75 -15
- src/linter_config/loader.py +43 -11
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/config.py +63 -0
- src/linters/collection_pipeline/continue_analyzer.py +100 -0
- src/linters/collection_pipeline/detector.py +130 -0
- src/linters/collection_pipeline/linter.py +437 -0
- src/linters/collection_pipeline/suggestion_builder.py +63 -0
- src/linters/dry/block_filter.py +6 -8
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/python_analyzer.py +34 -18
- src/linters/dry/token_hasher.py +5 -1
- src/linters/dry/typescript_analyzer.py +61 -31
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +4 -0
- src/linters/file_header/bash_parser.py +4 -0
- src/linters/file_header/linter.py +7 -11
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +28 -8
- src/linters/file_placement/pattern_matcher.py +4 -0
- src/linters/file_placement/pattern_validator.py +4 -0
- src/linters/magic_numbers/context_analyzer.py +4 -0
- src/linters/magic_numbers/typescript_analyzer.py +4 -0
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_function_extractor.py +4 -0
- src/linters/print_statements/typescript_analyzer.py +4 -0
- src/linters/srp/class_analyzer.py +4 -0
- src/linters/srp/heuristics.py +4 -3
- src/linters/srp/linter.py +2 -3
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +83 -47
- src/linters/srp/violation_builder.py +4 -0
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +355 -0
- src/linters/stateless_class/python_analyzer.py +299 -0
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/METADATA +226 -3
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/RECORD +46 -36
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/WHEEL +0 -0
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analyzer for detecting stateless classes
|
|
3
|
+
|
|
4
|
+
Scope: AST-based analysis of Python class definitions for stateless patterns
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Python source code using AST to detect classes that have no
|
|
7
|
+
constructor (__init__ or __new__), no instance state (self.attr assignments),
|
|
8
|
+
and 2+ methods - indicating they should be refactored to module-level functions.
|
|
9
|
+
Excludes legitimate patterns like ABC, Protocol, decorated classes, and classes
|
|
10
|
+
with class-level attributes.
|
|
11
|
+
|
|
12
|
+
Dependencies: Python AST module
|
|
13
|
+
|
|
14
|
+
Exports: analyze_code function, ClassInfo dataclass
|
|
15
|
+
|
|
16
|
+
Interfaces: analyze_code(code) -> list[ClassInfo] returning detected stateless classes
|
|
17
|
+
|
|
18
|
+
Implementation: AST visitor pattern with focused helper functions for different checks
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ClassInfo:
|
|
27
|
+
"""Information about a detected stateless class."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
line: int
|
|
31
|
+
column: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def analyze_code(code: str, min_methods: int = 2) -> list[ClassInfo]:
|
|
35
|
+
"""Analyze Python code for stateless classes.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
code: Python source code
|
|
39
|
+
min_methods: Minimum methods required to flag class
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of detected stateless class info
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
tree = ast.parse(code)
|
|
46
|
+
except SyntaxError:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
return _find_stateless_classes(tree, min_methods)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_stateless_classes(tree: ast.Module, min_methods: int = 2) -> list[ClassInfo]:
|
|
53
|
+
"""Find all stateless classes in AST.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
tree: Parsed AST module
|
|
57
|
+
min_methods: Minimum methods required to flag class
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of stateless class info
|
|
61
|
+
"""
|
|
62
|
+
results = []
|
|
63
|
+
for node in ast.walk(tree):
|
|
64
|
+
if isinstance(node, ast.ClassDef) and _is_stateless(node, min_methods):
|
|
65
|
+
results.append(ClassInfo(node.name, node.lineno, node.col_offset))
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_stateless(class_node: ast.ClassDef, min_methods: int = 2) -> bool:
|
|
70
|
+
"""Check if class is stateless and should be functions.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
class_node: AST ClassDef node
|
|
74
|
+
min_methods: Minimum methods required to flag class
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if class is stateless violation
|
|
78
|
+
"""
|
|
79
|
+
if _should_skip_class(class_node):
|
|
80
|
+
return False
|
|
81
|
+
return _count_methods(class_node) >= min_methods
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _should_skip_class(class_node: ast.ClassDef) -> bool:
|
|
85
|
+
"""Check if class should be skipped from analysis.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
class_node: AST ClassDef node
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if class should be skipped
|
|
92
|
+
"""
|
|
93
|
+
return (
|
|
94
|
+
_has_constructor(class_node)
|
|
95
|
+
or _is_exception_case(class_node)
|
|
96
|
+
or _has_class_attributes(class_node)
|
|
97
|
+
or _has_instance_attributes(class_node)
|
|
98
|
+
or _has_base_classes(class_node)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _has_base_classes(class_node: ast.ClassDef) -> bool:
|
|
103
|
+
"""Check if class inherits from non-trivial base classes.
|
|
104
|
+
|
|
105
|
+
Classes that inherit from other classes are using polymorphism/inheritance
|
|
106
|
+
and should not be flagged as stateless.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
class_node: AST ClassDef node
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if class has non-trivial base classes
|
|
113
|
+
"""
|
|
114
|
+
if not class_node.bases:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
for base in class_node.bases:
|
|
118
|
+
base_name = _get_base_name(base)
|
|
119
|
+
# Skip trivial bases like object
|
|
120
|
+
if base_name and base_name not in ("object",):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _count_methods(class_node: ast.ClassDef) -> int:
|
|
127
|
+
"""Count methods in class.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
class_node: AST ClassDef node
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Number of methods
|
|
134
|
+
"""
|
|
135
|
+
return sum(1 for item in class_node.body if isinstance(item, ast.FunctionDef))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _has_constructor(class_node: ast.ClassDef) -> bool:
|
|
139
|
+
"""Check if class has __init__ or __new__ method.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
class_node: AST ClassDef node
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if class has constructor
|
|
146
|
+
"""
|
|
147
|
+
constructor_names = ("__init__", "__new__")
|
|
148
|
+
for item in class_node.body:
|
|
149
|
+
if isinstance(item, ast.FunctionDef) and item.name in constructor_names:
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _is_exception_case(class_node: ast.ClassDef) -> bool:
|
|
155
|
+
"""Check if class is an exception case (ABC, Protocol, or decorated).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
class_node: AST ClassDef node
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if class is ABC, Protocol, or decorated
|
|
162
|
+
"""
|
|
163
|
+
if class_node.decorator_list:
|
|
164
|
+
return True
|
|
165
|
+
return _inherits_from_abc_or_protocol(class_node)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _inherits_from_abc_or_protocol(class_node: ast.ClassDef) -> bool:
|
|
169
|
+
"""Check if class inherits from ABC or Protocol.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
class_node: AST ClassDef node
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if inherits from ABC or Protocol
|
|
176
|
+
"""
|
|
177
|
+
for base in class_node.bases:
|
|
178
|
+
if _get_base_name(base) in ("ABC", "Protocol"):
|
|
179
|
+
return True
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _get_base_name(base: ast.expr) -> str:
|
|
184
|
+
"""Extract name from base class expression.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
base: AST expression for base class
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Base class name or empty string
|
|
191
|
+
"""
|
|
192
|
+
if isinstance(base, ast.Name):
|
|
193
|
+
return base.id
|
|
194
|
+
if isinstance(base, ast.Attribute):
|
|
195
|
+
return base.attr
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _has_class_attributes(class_node: ast.ClassDef) -> bool:
|
|
200
|
+
"""Check if class has class-level attributes.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
class_node: AST ClassDef node
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if class has class attributes
|
|
207
|
+
"""
|
|
208
|
+
for item in class_node.body:
|
|
209
|
+
if isinstance(item, (ast.Assign, ast.AnnAssign)):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _has_instance_attributes(class_node: ast.ClassDef) -> bool:
|
|
215
|
+
"""Check if methods assign to self.attr.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
class_node: AST ClassDef node
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if any method assigns to self
|
|
222
|
+
"""
|
|
223
|
+
for item in class_node.body:
|
|
224
|
+
if isinstance(item, ast.FunctionDef) and _method_has_self_assignment(item):
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _method_has_self_assignment(method: ast.FunctionDef) -> bool:
|
|
230
|
+
"""Check if method assigns to self.attr.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
method: AST FunctionDef node
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if method assigns to self
|
|
237
|
+
"""
|
|
238
|
+
for node in ast.walk(method):
|
|
239
|
+
if _is_self_attribute_assignment(node):
|
|
240
|
+
return True
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_self_attribute_assignment(node: ast.AST) -> bool:
|
|
245
|
+
"""Check if node is a self.attr assignment.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
node: AST node to check
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
True if node is self attribute assignment
|
|
252
|
+
"""
|
|
253
|
+
if not isinstance(node, ast.Assign):
|
|
254
|
+
return False
|
|
255
|
+
return any(_is_self_attribute(t) for t in node.targets)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _is_self_attribute(node: ast.expr) -> bool:
|
|
259
|
+
"""Check if node is a self.attr reference.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
node: AST expression node
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
True if node is self.attr
|
|
266
|
+
"""
|
|
267
|
+
if not isinstance(node, ast.Attribute):
|
|
268
|
+
return False
|
|
269
|
+
if not isinstance(node.value, ast.Name):
|
|
270
|
+
return False
|
|
271
|
+
return node.value.id == "self"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Legacy class wrapper for backward compatibility with linter.py
|
|
275
|
+
class StatelessClassAnalyzer:
|
|
276
|
+
"""Analyzes Python code for stateless classes.
|
|
277
|
+
|
|
278
|
+
Note: This class is a thin wrapper around module-level functions
|
|
279
|
+
to maintain backward compatibility with existing code.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, min_methods: int = 2) -> None:
|
|
283
|
+
"""Initialize the analyzer.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
min_methods: Minimum methods required to flag class
|
|
287
|
+
"""
|
|
288
|
+
self._min_methods = min_methods
|
|
289
|
+
|
|
290
|
+
def analyze(self, code: str) -> list[ClassInfo]:
|
|
291
|
+
"""Analyze Python code for stateless classes.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
code: Python source code
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of detected stateless class info
|
|
298
|
+
"""
|
|
299
|
+
return analyze_code(code, self._min_methods)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thailint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -37,8 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
[](https://opensource.org/licenses/MIT)
|
|
39
39
|
[](https://www.python.org/downloads/)
|
|
40
|
-
[](tests/)
|
|
41
|
+
[](htmlcov/)
|
|
42
42
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](docs/sarif-output.md)
|
|
44
44
|
|
|
@@ -98,12 +98,24 @@ thailint complements your existing linting stack by catching the patterns AI too
|
|
|
98
98
|
- Configurable thresholds (lines, tokens, occurrences)
|
|
99
99
|
- Language-specific detection (Python, TypeScript, JavaScript)
|
|
100
100
|
- False positive filtering (keyword args, imports)
|
|
101
|
+
- **Collection Pipeline Linting** - Detect for loops with embedded filtering
|
|
102
|
+
- Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
103
|
+
- Detects if/continue patterns that should use generator expressions
|
|
104
|
+
- Generates refactoring suggestions with generator syntax
|
|
105
|
+
- Configurable threshold (min_continues)
|
|
106
|
+
- Python support with AST analysis
|
|
101
107
|
- **Method Property Linting** - Detect methods that should be @property decorators
|
|
102
108
|
- Python AST-based detection
|
|
103
109
|
- get_* prefix detection (Java-style getters)
|
|
104
110
|
- Simple computed value detection
|
|
105
111
|
- Action verb exclusion (to_*, finalize, serialize)
|
|
106
112
|
- Test file detection
|
|
113
|
+
- **Stateless Class Linting** - Detect classes that should be module-level functions
|
|
114
|
+
- Python AST-based detection
|
|
115
|
+
- No constructor (__init__ or __new__) detection
|
|
116
|
+
- No instance state (self.attr) detection
|
|
117
|
+
- Excludes ABC, Protocol, and decorated classes
|
|
118
|
+
- Helpful refactoring suggestions
|
|
107
119
|
- **Pluggable Architecture** - Easy to extend with custom linters
|
|
108
120
|
- **Multi-Language Support** - Python, TypeScript, JavaScript, and more
|
|
109
121
|
- **Flexible Configuration** - YAML/JSON configs with pattern matching
|
|
@@ -735,6 +747,109 @@ Built-in filters automatically exclude common non-duplication patterns:
|
|
|
735
747
|
|
|
736
748
|
See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
|
|
737
749
|
|
|
750
|
+
## Collection Pipeline Linter
|
|
751
|
+
|
|
752
|
+
### Overview
|
|
753
|
+
|
|
754
|
+
The collection-pipeline linter detects for loops with embedded filtering (if/continue patterns) that should be refactored to use generator expressions or other collection pipelines. Based on Martin Fowler's "Replace Loop with Pipeline" refactoring pattern.
|
|
755
|
+
|
|
756
|
+
### The Anti-Pattern
|
|
757
|
+
|
|
758
|
+
```python
|
|
759
|
+
# Anti-pattern: Embedded filtering in loop body
|
|
760
|
+
for file_path in dir_path.glob(pattern):
|
|
761
|
+
if not file_path.is_file():
|
|
762
|
+
continue
|
|
763
|
+
if ignore_parser.is_ignored(file_path):
|
|
764
|
+
continue
|
|
765
|
+
violations.extend(lint_file(file_path))
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### The Solution
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
# Collection pipeline: Filtering separated from processing
|
|
772
|
+
valid_files = (
|
|
773
|
+
f for f in dir_path.glob(pattern)
|
|
774
|
+
if f.is_file() and not ignore_parser.is_ignored(f)
|
|
775
|
+
)
|
|
776
|
+
for file_path in valid_files:
|
|
777
|
+
violations.extend(lint_file(file_path))
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Quick Start
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
# Check current directory
|
|
784
|
+
thailint pipeline .
|
|
785
|
+
|
|
786
|
+
# Check specific directory
|
|
787
|
+
thailint pipeline src/
|
|
788
|
+
|
|
789
|
+
# Only flag patterns with 2+ filter conditions
|
|
790
|
+
thailint pipeline --min-continues 2 src/
|
|
791
|
+
|
|
792
|
+
# JSON output
|
|
793
|
+
thailint pipeline --format json src/
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Configuration
|
|
797
|
+
|
|
798
|
+
```yaml
|
|
799
|
+
# .thailint.yaml
|
|
800
|
+
collection-pipeline:
|
|
801
|
+
enabled: true
|
|
802
|
+
min_continues: 1 # Minimum if/continue patterns to flag
|
|
803
|
+
ignore:
|
|
804
|
+
- "tests/**"
|
|
805
|
+
- "**/legacy/**"
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Example Violation
|
|
809
|
+
|
|
810
|
+
**Detected Pattern:**
|
|
811
|
+
```python
|
|
812
|
+
def process_files(paths):
|
|
813
|
+
for path in paths:
|
|
814
|
+
if not path.is_file():
|
|
815
|
+
continue
|
|
816
|
+
analyze(path)
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
**Violation Message:**
|
|
820
|
+
```
|
|
821
|
+
src/processor.py:3 - For loop over 'paths' has embedded filtering.
|
|
822
|
+
Consider using a generator expression:
|
|
823
|
+
for path in (path for path in paths if path.is_file()):
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
**Refactored Code:**
|
|
827
|
+
```python
|
|
828
|
+
def process_files(paths):
|
|
829
|
+
valid_paths = (p for p in paths if p.is_file())
|
|
830
|
+
for path in valid_paths:
|
|
831
|
+
analyze(path)
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Why This Matters
|
|
835
|
+
|
|
836
|
+
- **Separation of concerns**: Filtering logic is separate from processing logic
|
|
837
|
+
- **Readability**: Intent is clear at a glance
|
|
838
|
+
- **Testability**: Filtering can be tested independently
|
|
839
|
+
- **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
840
|
+
|
|
841
|
+
### Ignoring Violations
|
|
842
|
+
|
|
843
|
+
```python
|
|
844
|
+
# Line-level ignore
|
|
845
|
+
for item in items: # thailint: ignore[collection-pipeline]
|
|
846
|
+
if not item.valid:
|
|
847
|
+
continue
|
|
848
|
+
process(item)
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
|
|
852
|
+
|
|
738
853
|
## Magic Numbers Linter
|
|
739
854
|
|
|
740
855
|
### Overview
|
|
@@ -1017,6 +1132,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
|
|
|
1017
1132
|
|
|
1018
1133
|
See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[File Header Linter Guide](https://thai-lint.readthedocs.io/en/latest/file-header-linter/)** for complete documentation.
|
|
1019
1134
|
|
|
1135
|
+
## Stateless Class Linter
|
|
1136
|
+
|
|
1137
|
+
### Overview
|
|
1138
|
+
|
|
1139
|
+
The stateless class linter detects Python classes that have no state (no constructor, no instance attributes) and should be refactored to module-level functions. This is a common anti-pattern in AI-generated code.
|
|
1140
|
+
|
|
1141
|
+
### What Are Stateless Classes?
|
|
1142
|
+
|
|
1143
|
+
Stateless classes are classes that:
|
|
1144
|
+
- Have no `__init__` or `__new__` method
|
|
1145
|
+
- Have no instance attributes (`self.attr` assignments)
|
|
1146
|
+
- Have 2+ methods (grouped functionality without state)
|
|
1147
|
+
|
|
1148
|
+
```python
|
|
1149
|
+
# Bad - Stateless class (no state, just grouped functions)
|
|
1150
|
+
class TokenHasher:
|
|
1151
|
+
def hash_token(self, token: str) -> str:
|
|
1152
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
1153
|
+
|
|
1154
|
+
def verify_token(self, token: str, hash_value: str) -> bool:
|
|
1155
|
+
return self.hash_token(token) == hash_value
|
|
1156
|
+
|
|
1157
|
+
# Good - Module-level functions
|
|
1158
|
+
def hash_token(token: str) -> str:
|
|
1159
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
1160
|
+
|
|
1161
|
+
def verify_token(token: str, hash_value: str) -> bool:
|
|
1162
|
+
return hash_token(token) == hash_value
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### Quick Start
|
|
1166
|
+
|
|
1167
|
+
```bash
|
|
1168
|
+
# Check stateless classes in current directory
|
|
1169
|
+
thailint stateless-class .
|
|
1170
|
+
|
|
1171
|
+
# Check specific directory
|
|
1172
|
+
thailint stateless-class src/
|
|
1173
|
+
|
|
1174
|
+
# Get JSON output
|
|
1175
|
+
thailint stateless-class --format json src/
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Configuration
|
|
1179
|
+
|
|
1180
|
+
Add to `.thailint.yaml`:
|
|
1181
|
+
|
|
1182
|
+
```yaml
|
|
1183
|
+
stateless-class:
|
|
1184
|
+
enabled: true
|
|
1185
|
+
min_methods: 2 # Minimum methods to flag
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
### Example Violation
|
|
1189
|
+
|
|
1190
|
+
**Code with stateless class:**
|
|
1191
|
+
```python
|
|
1192
|
+
class StringUtils:
|
|
1193
|
+
def capitalize_words(self, text: str) -> str:
|
|
1194
|
+
return ' '.join(w.capitalize() for w in text.split())
|
|
1195
|
+
|
|
1196
|
+
def reverse_words(self, text: str) -> str:
|
|
1197
|
+
return ' '.join(reversed(text.split()))
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
**Violation message:**
|
|
1201
|
+
```
|
|
1202
|
+
src/utils.py:1 - Class 'StringUtils' has no state and should be refactored to module-level functions
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
**Refactored code:**
|
|
1206
|
+
```python
|
|
1207
|
+
def capitalize_words(text: str) -> str:
|
|
1208
|
+
return ' '.join(w.capitalize() for w in text.split())
|
|
1209
|
+
|
|
1210
|
+
def reverse_words(text: str) -> str:
|
|
1211
|
+
return ' '.join(reversed(text.split()))
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### Exclusion Rules
|
|
1215
|
+
|
|
1216
|
+
The linter does NOT flag classes that:
|
|
1217
|
+
- Have `__init__` or `__new__` constructors
|
|
1218
|
+
- Have instance attributes (`self.attr = value`)
|
|
1219
|
+
- Have class-level attributes
|
|
1220
|
+
- Inherit from ABC or Protocol
|
|
1221
|
+
- Have any decorator (`@dataclass`, `@register`, etc.)
|
|
1222
|
+
- Have 0-1 methods
|
|
1223
|
+
|
|
1224
|
+
### Ignoring Violations
|
|
1225
|
+
|
|
1226
|
+
```python
|
|
1227
|
+
# Line-level ignore
|
|
1228
|
+
class TokenHasher: # thailint: ignore[stateless-class] - Legacy API
|
|
1229
|
+
def hash(self, token): ...
|
|
1230
|
+
|
|
1231
|
+
# File-level ignore
|
|
1232
|
+
# thailint: ignore-file[stateless-class]
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
See **[How to Ignore Violations](https://thai-lint.readthedocs.io/en/latest/how-to-ignore-violations/)** and **[Stateless Class Linter Guide](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** for complete documentation.
|
|
1236
|
+
|
|
1020
1237
|
## Pre-commit Hooks
|
|
1021
1238
|
|
|
1022
1239
|
Automate code quality checks before every commit and push with pre-commit hooks.
|
|
@@ -1243,6 +1460,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
|
|
|
1243
1460
|
# Lint specific subdirectory
|
|
1244
1461
|
docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
|
|
1245
1462
|
|
|
1463
|
+
# Collection pipeline linter
|
|
1464
|
+
docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
|
|
1465
|
+
|
|
1246
1466
|
# With custom config
|
|
1247
1467
|
docker run --rm -v $(pwd):/data \
|
|
1248
1468
|
washad/thailint:latest nesting --config /data/.thailint.yaml /data
|
|
@@ -1306,6 +1526,9 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1306
1526
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1307
1527
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1308
1528
|
- **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
|
|
1529
|
+
- **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
|
|
1530
|
+
- **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
|
|
1531
|
+
- **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
|
|
1309
1532
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
1310
1533
|
- **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
|
|
1311
1534
|
- **[Publishing Guide](https://thai-lint.readthedocs.io/en/latest/releasing/)** - Release and publishing workflow
|