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.
Files changed (46) hide show
  1. src/cli.py +242 -0
  2. src/config.py +2 -3
  3. src/core/base.py +4 -0
  4. src/core/rule_discovery.py +143 -84
  5. src/core/violation_builder.py +75 -15
  6. src/linter_config/loader.py +43 -11
  7. src/linters/collection_pipeline/__init__.py +90 -0
  8. src/linters/collection_pipeline/config.py +63 -0
  9. src/linters/collection_pipeline/continue_analyzer.py +100 -0
  10. src/linters/collection_pipeline/detector.py +130 -0
  11. src/linters/collection_pipeline/linter.py +437 -0
  12. src/linters/collection_pipeline/suggestion_builder.py +63 -0
  13. src/linters/dry/block_filter.py +6 -8
  14. src/linters/dry/block_grouper.py +4 -0
  15. src/linters/dry/cache_query.py +4 -0
  16. src/linters/dry/python_analyzer.py +34 -18
  17. src/linters/dry/token_hasher.py +5 -1
  18. src/linters/dry/typescript_analyzer.py +61 -31
  19. src/linters/dry/violation_builder.py +4 -0
  20. src/linters/dry/violation_filter.py +4 -0
  21. src/linters/file_header/bash_parser.py +4 -0
  22. src/linters/file_header/linter.py +7 -11
  23. src/linters/file_placement/directory_matcher.py +4 -0
  24. src/linters/file_placement/linter.py +28 -8
  25. src/linters/file_placement/pattern_matcher.py +4 -0
  26. src/linters/file_placement/pattern_validator.py +4 -0
  27. src/linters/magic_numbers/context_analyzer.py +4 -0
  28. src/linters/magic_numbers/typescript_analyzer.py +4 -0
  29. src/linters/nesting/python_analyzer.py +4 -0
  30. src/linters/nesting/typescript_function_extractor.py +4 -0
  31. src/linters/print_statements/typescript_analyzer.py +4 -0
  32. src/linters/srp/class_analyzer.py +4 -0
  33. src/linters/srp/heuristics.py +4 -3
  34. src/linters/srp/linter.py +2 -3
  35. src/linters/srp/python_analyzer.py +55 -20
  36. src/linters/srp/typescript_metrics_calculator.py +83 -47
  37. src/linters/srp/violation_builder.py +4 -0
  38. src/linters/stateless_class/__init__.py +25 -0
  39. src/linters/stateless_class/config.py +58 -0
  40. src/linters/stateless_class/linter.py +355 -0
  41. src/linters/stateless_class/python_analyzer.py +299 -0
  42. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/METADATA +226 -3
  43. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/RECORD +46 -36
  44. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/WHEEL +0 -0
  45. {thailint-0.8.0.dist-info → thailint-0.10.0.dist-info}/entry_points.txt +0 -0
  46. {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.8.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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
39
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
40
- [![Tests](https://img.shields.io/badge/tests-682%2F682%20passing-brightgreen.svg)](tests/)
41
- [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
40
+ [![Tests](https://img.shields.io/badge/tests-795%2F795%20passing-brightgreen.svg)](tests/)
41
+ [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
42
42
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
43
43
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](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