thailint 0.8.0__tar.gz → 0.10.0__tar.gz

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 (120) hide show
  1. {thailint-0.8.0 → thailint-0.10.0}/CHANGELOG.md +13 -0
  2. {thailint-0.8.0 → thailint-0.10.0}/PKG-INFO +226 -3
  3. {thailint-0.8.0 → thailint-0.10.0}/README.md +225 -2
  4. {thailint-0.8.0 → thailint-0.10.0}/pyproject.toml +1 -1
  5. {thailint-0.8.0 → thailint-0.10.0}/src/cli.py +242 -0
  6. {thailint-0.8.0 → thailint-0.10.0}/src/config.py +2 -3
  7. {thailint-0.8.0 → thailint-0.10.0}/src/core/base.py +4 -0
  8. thailint-0.10.0/src/core/rule_discovery.py +191 -0
  9. {thailint-0.8.0 → thailint-0.10.0}/src/core/violation_builder.py +75 -15
  10. {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/loader.py +43 -11
  11. thailint-0.10.0/src/linters/collection_pipeline/__init__.py +90 -0
  12. thailint-0.10.0/src/linters/collection_pipeline/config.py +63 -0
  13. thailint-0.10.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
  14. thailint-0.10.0/src/linters/collection_pipeline/detector.py +130 -0
  15. thailint-0.10.0/src/linters/collection_pipeline/linter.py +437 -0
  16. thailint-0.10.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
  17. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/block_filter.py +6 -8
  18. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/block_grouper.py +4 -0
  19. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/cache_query.py +4 -0
  20. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/python_analyzer.py +34 -18
  21. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/token_hasher.py +5 -1
  22. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/typescript_analyzer.py +61 -31
  23. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_builder.py +4 -0
  24. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_filter.py +4 -0
  25. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/bash_parser.py +4 -0
  26. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/linter.py +7 -11
  27. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/directory_matcher.py +4 -0
  28. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/linter.py +28 -8
  29. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/pattern_matcher.py +4 -0
  30. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/pattern_validator.py +4 -0
  31. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/context_analyzer.py +4 -0
  32. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/typescript_analyzer.py +4 -0
  33. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/python_analyzer.py +4 -0
  34. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/typescript_function_extractor.py +4 -0
  35. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/typescript_analyzer.py +4 -0
  36. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/class_analyzer.py +4 -0
  37. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/heuristics.py +4 -3
  38. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/linter.py +2 -3
  39. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/python_analyzer.py +55 -20
  40. thailint-0.10.0/src/linters/srp/typescript_metrics_calculator.py +126 -0
  41. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/violation_builder.py +4 -0
  42. thailint-0.10.0/src/linters/stateless_class/__init__.py +25 -0
  43. thailint-0.10.0/src/linters/stateless_class/config.py +58 -0
  44. thailint-0.10.0/src/linters/stateless_class/linter.py +355 -0
  45. thailint-0.10.0/src/linters/stateless_class/python_analyzer.py +299 -0
  46. thailint-0.8.0/src/core/rule_discovery.py +0 -132
  47. thailint-0.8.0/src/linters/srp/typescript_metrics_calculator.py +0 -90
  48. {thailint-0.8.0 → thailint-0.10.0}/LICENSE +0 -0
  49. {thailint-0.8.0 → thailint-0.10.0}/src/__init__.py +0 -0
  50. {thailint-0.8.0 → thailint-0.10.0}/src/analyzers/__init__.py +0 -0
  51. {thailint-0.8.0 → thailint-0.10.0}/src/analyzers/typescript_base.py +0 -0
  52. {thailint-0.8.0 → thailint-0.10.0}/src/api.py +0 -0
  53. {thailint-0.8.0 → thailint-0.10.0}/src/core/__init__.py +0 -0
  54. {thailint-0.8.0 → thailint-0.10.0}/src/core/cli_utils.py +0 -0
  55. {thailint-0.8.0 → thailint-0.10.0}/src/core/config_parser.py +0 -0
  56. {thailint-0.8.0 → thailint-0.10.0}/src/core/linter_utils.py +0 -0
  57. {thailint-0.8.0 → thailint-0.10.0}/src/core/registry.py +0 -0
  58. {thailint-0.8.0 → thailint-0.10.0}/src/core/types.py +0 -0
  59. {thailint-0.8.0 → thailint-0.10.0}/src/formatters/__init__.py +0 -0
  60. {thailint-0.8.0 → thailint-0.10.0}/src/formatters/sarif.py +0 -0
  61. {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/__init__.py +0 -0
  62. {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/ignore.py +0 -0
  63. {thailint-0.8.0 → thailint-0.10.0}/src/linters/__init__.py +0 -0
  64. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/__init__.py +0 -0
  65. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/base_token_analyzer.py +0 -0
  66. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/cache.py +0 -0
  67. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/config.py +0 -0
  68. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/config_loader.py +0 -0
  69. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/deduplicator.py +0 -0
  70. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/duplicate_storage.py +0 -0
  71. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/file_analyzer.py +0 -0
  72. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/inline_ignore.py +0 -0
  73. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/linter.py +0 -0
  74. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/storage_initializer.py +0 -0
  75. {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_generator.py +0 -0
  76. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/__init__.py +0 -0
  77. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/atemporal_detector.py +0 -0
  78. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/base_parser.py +0 -0
  79. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/config.py +0 -0
  80. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/css_parser.py +0 -0
  81. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/field_validator.py +0 -0
  82. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/markdown_parser.py +0 -0
  83. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/python_parser.py +0 -0
  84. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/typescript_parser.py +0 -0
  85. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/violation_builder.py +0 -0
  86. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/__init__.py +0 -0
  87. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/config_loader.py +0 -0
  88. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/path_resolver.py +0 -0
  89. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/rule_checker.py +0 -0
  90. {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/violation_factory.py +0 -0
  91. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/__init__.py +0 -0
  92. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/config.py +0 -0
  93. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/linter.py +0 -0
  94. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  95. {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  96. {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/__init__.py +0 -0
  97. {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/config.py +0 -0
  98. {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/linter.py +0 -0
  99. {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/python_analyzer.py +0 -0
  100. {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/violation_builder.py +0 -0
  101. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/__init__.py +0 -0
  102. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/config.py +0 -0
  103. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/linter.py +0 -0
  104. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  105. {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/violation_builder.py +0 -0
  106. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/__init__.py +0 -0
  107. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/config.py +0 -0
  108. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/linter.py +0 -0
  109. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/python_analyzer.py +0 -0
  110. {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/violation_builder.py +0 -0
  111. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/__init__.py +0 -0
  112. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/config.py +0 -0
  113. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/metrics_evaluator.py +0 -0
  114. {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/typescript_analyzer.py +0 -0
  115. {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/__init__.py +0 -0
  116. {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/core.py +0 -0
  117. {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/language_detector.py +0 -0
  118. {thailint-0.8.0 → thailint-0.10.0}/src/templates/thailint_config_template.yaml +0 -0
  119. {thailint-0.8.0 → thailint-0.10.0}/src/utils/__init__.py +0 -0
  120. {thailint-0.8.0 → thailint-0.10.0}/src/utils/project_root.py +0 -0
@@ -26,6 +26,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
26
26
 
27
27
  ### Added
28
28
 
29
+ - **Stateless Class Linter** - Detect Python classes without state that should be module-level functions
30
+ - AST-based detection of classes without `__init__`/`__new__` constructors
31
+ - Detects classes without instance state (`self.attr` assignments)
32
+ - Excludes ABC, Protocol, and decorated classes (legitimate patterns)
33
+ - Excludes classes with class-level attributes
34
+ - Minimum 2 methods required to flag (avoids false positives on simple wrappers)
35
+ - CLI command: `thailint stateless-class src/`
36
+ - JSON and SARIF output formats
37
+ - Configuration via `.thailint.yaml` with `min_methods` and `ignore` options
38
+ - Self-dogfooded: 23 violations in thai-lint codebase were fixed
39
+ - 28 tests (15 detector + 13 CLI) with 100% pass rate
40
+ - Documentation: `docs/stateless-class-linter.md`
41
+
29
42
  - **Project Root Detection System** - Three-level precedence system for accurate configuration and ignore pattern resolution
30
43
  - `--project-root` CLI option for explicit project root specification (highest priority)
31
44
  - Automatic project root inference from `--config` path (config's parent directory becomes project root)
@@ -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
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
5
- [![Tests](https://img.shields.io/badge/tests-682%2F682%20passing-brightgreen.svg)](tests/)
6
- [![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen.svg)](htmlcov/)
5
+ [![Tests](https://img.shields.io/badge/tests-795%2F795%20passing-brightgreen.svg)](tests/)
6
+ [![Coverage](https://img.shields.io/badge/coverage-88%25-brightgreen.svg)](htmlcov/)
7
7
  [![Documentation Status](https://readthedocs.org/projects/thai-lint/badge/?version=latest)](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
8
8
  [![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-orange.svg)](docs/sarif-output.md)
9
9
 
@@ -63,12 +63,24 @@ thailint complements your existing linting stack by catching the patterns AI too
63
63
  - Configurable thresholds (lines, tokens, occurrences)
64
64
  - Language-specific detection (Python, TypeScript, JavaScript)
65
65
  - False positive filtering (keyword args, imports)
66
+ - **Collection Pipeline Linting** - Detect for loops with embedded filtering
67
+ - Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
68
+ - Detects if/continue patterns that should use generator expressions
69
+ - Generates refactoring suggestions with generator syntax
70
+ - Configurable threshold (min_continues)
71
+ - Python support with AST analysis
66
72
  - **Method Property Linting** - Detect methods that should be @property decorators
67
73
  - Python AST-based detection
68
74
  - get_* prefix detection (Java-style getters)
69
75
  - Simple computed value detection
70
76
  - Action verb exclusion (to_*, finalize, serialize)
71
77
  - Test file detection
78
+ - **Stateless Class Linting** - Detect classes that should be module-level functions
79
+ - Python AST-based detection
80
+ - No constructor (__init__ or __new__) detection
81
+ - No instance state (self.attr) detection
82
+ - Excludes ABC, Protocol, and decorated classes
83
+ - Helpful refactoring suggestions
72
84
  - **Pluggable Architecture** - Easy to extend with custom linters
73
85
  - **Multi-Language Support** - Python, TypeScript, JavaScript, and more
74
86
  - **Flexible Configuration** - YAML/JSON configs with pattern matching
@@ -700,6 +712,109 @@ Built-in filters automatically exclude common non-duplication patterns:
700
712
 
701
713
  See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
702
714
 
715
+ ## Collection Pipeline Linter
716
+
717
+ ### Overview
718
+
719
+ 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.
720
+
721
+ ### The Anti-Pattern
722
+
723
+ ```python
724
+ # Anti-pattern: Embedded filtering in loop body
725
+ for file_path in dir_path.glob(pattern):
726
+ if not file_path.is_file():
727
+ continue
728
+ if ignore_parser.is_ignored(file_path):
729
+ continue
730
+ violations.extend(lint_file(file_path))
731
+ ```
732
+
733
+ ### The Solution
734
+
735
+ ```python
736
+ # Collection pipeline: Filtering separated from processing
737
+ valid_files = (
738
+ f for f in dir_path.glob(pattern)
739
+ if f.is_file() and not ignore_parser.is_ignored(f)
740
+ )
741
+ for file_path in valid_files:
742
+ violations.extend(lint_file(file_path))
743
+ ```
744
+
745
+ ### Quick Start
746
+
747
+ ```bash
748
+ # Check current directory
749
+ thailint pipeline .
750
+
751
+ # Check specific directory
752
+ thailint pipeline src/
753
+
754
+ # Only flag patterns with 2+ filter conditions
755
+ thailint pipeline --min-continues 2 src/
756
+
757
+ # JSON output
758
+ thailint pipeline --format json src/
759
+ ```
760
+
761
+ ### Configuration
762
+
763
+ ```yaml
764
+ # .thailint.yaml
765
+ collection-pipeline:
766
+ enabled: true
767
+ min_continues: 1 # Minimum if/continue patterns to flag
768
+ ignore:
769
+ - "tests/**"
770
+ - "**/legacy/**"
771
+ ```
772
+
773
+ ### Example Violation
774
+
775
+ **Detected Pattern:**
776
+ ```python
777
+ def process_files(paths):
778
+ for path in paths:
779
+ if not path.is_file():
780
+ continue
781
+ analyze(path)
782
+ ```
783
+
784
+ **Violation Message:**
785
+ ```
786
+ src/processor.py:3 - For loop over 'paths' has embedded filtering.
787
+ Consider using a generator expression:
788
+ for path in (path for path in paths if path.is_file()):
789
+ ```
790
+
791
+ **Refactored Code:**
792
+ ```python
793
+ def process_files(paths):
794
+ valid_paths = (p for p in paths if p.is_file())
795
+ for path in valid_paths:
796
+ analyze(path)
797
+ ```
798
+
799
+ ### Why This Matters
800
+
801
+ - **Separation of concerns**: Filtering logic is separate from processing logic
802
+ - **Readability**: Intent is clear at a glance
803
+ - **Testability**: Filtering can be tested independently
804
+ - **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
805
+
806
+ ### Ignoring Violations
807
+
808
+ ```python
809
+ # Line-level ignore
810
+ for item in items: # thailint: ignore[collection-pipeline]
811
+ if not item.valid:
812
+ continue
813
+ process(item)
814
+ ```
815
+
816
+ See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
817
+
703
818
  ## Magic Numbers Linter
704
819
 
705
820
  ### Overview
@@ -982,6 +1097,108 @@ Overview: Created 2024-01-15. # thailint: ignore[file-header]
982
1097
 
983
1098
  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.
984
1099
 
1100
+ ## Stateless Class Linter
1101
+
1102
+ ### Overview
1103
+
1104
+ 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.
1105
+
1106
+ ### What Are Stateless Classes?
1107
+
1108
+ Stateless classes are classes that:
1109
+ - Have no `__init__` or `__new__` method
1110
+ - Have no instance attributes (`self.attr` assignments)
1111
+ - Have 2+ methods (grouped functionality without state)
1112
+
1113
+ ```python
1114
+ # Bad - Stateless class (no state, just grouped functions)
1115
+ class TokenHasher:
1116
+ def hash_token(self, token: str) -> str:
1117
+ return hashlib.sha256(token.encode()).hexdigest()
1118
+
1119
+ def verify_token(self, token: str, hash_value: str) -> bool:
1120
+ return self.hash_token(token) == hash_value
1121
+
1122
+ # Good - Module-level functions
1123
+ def hash_token(token: str) -> str:
1124
+ return hashlib.sha256(token.encode()).hexdigest()
1125
+
1126
+ def verify_token(token: str, hash_value: str) -> bool:
1127
+ return hash_token(token) == hash_value
1128
+ ```
1129
+
1130
+ ### Quick Start
1131
+
1132
+ ```bash
1133
+ # Check stateless classes in current directory
1134
+ thailint stateless-class .
1135
+
1136
+ # Check specific directory
1137
+ thailint stateless-class src/
1138
+
1139
+ # Get JSON output
1140
+ thailint stateless-class --format json src/
1141
+ ```
1142
+
1143
+ ### Configuration
1144
+
1145
+ Add to `.thailint.yaml`:
1146
+
1147
+ ```yaml
1148
+ stateless-class:
1149
+ enabled: true
1150
+ min_methods: 2 # Minimum methods to flag
1151
+ ```
1152
+
1153
+ ### Example Violation
1154
+
1155
+ **Code with stateless class:**
1156
+ ```python
1157
+ class StringUtils:
1158
+ def capitalize_words(self, text: str) -> str:
1159
+ return ' '.join(w.capitalize() for w in text.split())
1160
+
1161
+ def reverse_words(self, text: str) -> str:
1162
+ return ' '.join(reversed(text.split()))
1163
+ ```
1164
+
1165
+ **Violation message:**
1166
+ ```
1167
+ src/utils.py:1 - Class 'StringUtils' has no state and should be refactored to module-level functions
1168
+ ```
1169
+
1170
+ **Refactored code:**
1171
+ ```python
1172
+ def capitalize_words(text: str) -> str:
1173
+ return ' '.join(w.capitalize() for w in text.split())
1174
+
1175
+ def reverse_words(text: str) -> str:
1176
+ return ' '.join(reversed(text.split()))
1177
+ ```
1178
+
1179
+ ### Exclusion Rules
1180
+
1181
+ The linter does NOT flag classes that:
1182
+ - Have `__init__` or `__new__` constructors
1183
+ - Have instance attributes (`self.attr = value`)
1184
+ - Have class-level attributes
1185
+ - Inherit from ABC or Protocol
1186
+ - Have any decorator (`@dataclass`, `@register`, etc.)
1187
+ - Have 0-1 methods
1188
+
1189
+ ### Ignoring Violations
1190
+
1191
+ ```python
1192
+ # Line-level ignore
1193
+ class TokenHasher: # thailint: ignore[stateless-class] - Legacy API
1194
+ def hash(self, token): ...
1195
+
1196
+ # File-level ignore
1197
+ # thailint: ignore-file[stateless-class]
1198
+ ```
1199
+
1200
+ 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.
1201
+
985
1202
  ## Pre-commit Hooks
986
1203
 
987
1204
  Automate code quality checks before every commit and push with pre-commit hooks.
@@ -1208,6 +1425,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
1208
1425
  # Lint specific subdirectory
1209
1426
  docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
1210
1427
 
1428
+ # Collection pipeline linter
1429
+ docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
1430
+
1211
1431
  # With custom config
1212
1432
  docker run --rm -v $(pwd):/data \
1213
1433
  washad/thailint:latest nesting --config /data/.thailint.yaml /data
@@ -1271,6 +1491,9 @@ docker run --rm -v /path/to/workspace:/workspace \
1271
1491
  - **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
1272
1492
  - **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
1273
1493
  - **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
1494
+ - **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
1495
+ - **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
1496
+ - **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
1274
1497
  - **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
1275
1498
  - **[SARIF Output Guide](docs/sarif-output.md)** - SARIF format for GitHub Code Scanning and CI/CD
1276
1499
  - **[Publishing Guide](https://thai-lint.readthedocs.io/en/latest/releasing/)** - Release and publishing workflow
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.8.0"
20
+ version = "0.10.0"
21
21
  description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
22
22
  authors = ["Steve Jackson"]
23
23
  license = "MIT"