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.
- {thailint-0.8.0 → thailint-0.10.0}/CHANGELOG.md +13 -0
- {thailint-0.8.0 → thailint-0.10.0}/PKG-INFO +226 -3
- {thailint-0.8.0 → thailint-0.10.0}/README.md +225 -2
- {thailint-0.8.0 → thailint-0.10.0}/pyproject.toml +1 -1
- {thailint-0.8.0 → thailint-0.10.0}/src/cli.py +242 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/config.py +2 -3
- {thailint-0.8.0 → thailint-0.10.0}/src/core/base.py +4 -0
- thailint-0.10.0/src/core/rule_discovery.py +191 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/violation_builder.py +75 -15
- {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/loader.py +43 -11
- thailint-0.10.0/src/linters/collection_pipeline/__init__.py +90 -0
- thailint-0.10.0/src/linters/collection_pipeline/config.py +63 -0
- thailint-0.10.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
- thailint-0.10.0/src/linters/collection_pipeline/detector.py +130 -0
- thailint-0.10.0/src/linters/collection_pipeline/linter.py +437 -0
- thailint-0.10.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/block_filter.py +6 -8
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/block_grouper.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/cache_query.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/python_analyzer.py +34 -18
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/token_hasher.py +5 -1
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/typescript_analyzer.py +61 -31
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_builder.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_filter.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/bash_parser.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/linter.py +7 -11
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/directory_matcher.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/linter.py +28 -8
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/pattern_matcher.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/pattern_validator.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/context_analyzer.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/typescript_analyzer.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/python_analyzer.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/typescript_function_extractor.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/typescript_analyzer.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/class_analyzer.py +4 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/heuristics.py +4 -3
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/linter.py +2 -3
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/python_analyzer.py +55 -20
- thailint-0.10.0/src/linters/srp/typescript_metrics_calculator.py +126 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/violation_builder.py +4 -0
- thailint-0.10.0/src/linters/stateless_class/__init__.py +25 -0
- thailint-0.10.0/src/linters/stateless_class/config.py +58 -0
- thailint-0.10.0/src/linters/stateless_class/linter.py +355 -0
- thailint-0.10.0/src/linters/stateless_class/python_analyzer.py +299 -0
- thailint-0.8.0/src/core/rule_discovery.py +0 -132
- thailint-0.8.0/src/linters/srp/typescript_metrics_calculator.py +0 -90
- {thailint-0.8.0 → thailint-0.10.0}/LICENSE +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/analyzers/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/api.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/cli_utils.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/config_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/linter_utils.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/registry.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/core/types.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/formatters/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/formatters/sarif.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linter_config/ignore.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/cache.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/linter.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/linter.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/linter.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/python_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/method_property/violation_builder.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/linter.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/linter.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/config.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/__init__.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/core.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.8.0 → thailint-0.10.0}/src/utils/__init__.py +0 -0
- {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.
|
|
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
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](tests/)
|
|
6
|
+
[](htmlcov/)
|
|
7
7
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
8
8
|
[](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.
|
|
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"
|