thailint 0.9.0__tar.gz → 0.11.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.9.0 → thailint-0.11.0}/PKG-INFO +116 -3
- {thailint-0.9.0 → thailint-0.11.0}/README.md +115 -2
- {thailint-0.9.0 → thailint-0.11.0}/pyproject.toml +4 -3
- {thailint-0.9.0 → thailint-0.11.0}/src/__init__.py +1 -0
- thailint-0.11.0/src/cli/__init__.py +27 -0
- thailint-0.11.0/src/cli/__main__.py +22 -0
- thailint-0.11.0/src/cli/config.py +478 -0
- thailint-0.11.0/src/cli/linters/__init__.py +58 -0
- thailint-0.11.0/src/cli/linters/code_patterns.py +372 -0
- thailint-0.11.0/src/cli/linters/code_smells.py +343 -0
- thailint-0.11.0/src/cli/linters/documentation.py +155 -0
- thailint-0.11.0/src/cli/linters/shared.py +89 -0
- thailint-0.11.0/src/cli/linters/structure.py +313 -0
- thailint-0.11.0/src/cli/linters/structure_quality.py +316 -0
- thailint-0.11.0/src/cli/main.py +120 -0
- thailint-0.11.0/src/cli/utils.py +375 -0
- thailint-0.11.0/src/cli_main.py +34 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/config.py +2 -3
- {thailint-0.9.0 → thailint-0.11.0}/src/core/rule_discovery.py +43 -10
- {thailint-0.9.0 → thailint-0.11.0}/src/core/types.py +13 -0
- thailint-0.11.0/src/core/violation_utils.py +69 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/ignore.py +32 -16
- thailint-0.11.0/src/linters/collection_pipeline/__init__.py +90 -0
- thailint-0.11.0/src/linters/collection_pipeline/config.py +63 -0
- thailint-0.11.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
- thailint-0.11.0/src/linters/collection_pipeline/detector.py +130 -0
- thailint-0.11.0/src/linters/collection_pipeline/linter.py +437 -0
- thailint-0.11.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/block_filter.py +99 -9
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/cache.py +94 -6
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/config.py +47 -10
- thailint-0.11.0/src/linters/dry/constant.py +92 -0
- thailint-0.11.0/src/linters/dry/constant_matcher.py +214 -0
- thailint-0.11.0/src/linters/dry/constant_violation_builder.py +98 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/linter.py +89 -48
- thailint-0.11.0/src/linters/dry/python_analyzer.py +281 -0
- thailint-0.11.0/src/linters/dry/python_constant_extractor.py +101 -0
- thailint-0.11.0/src/linters/dry/single_statement_detector.py +415 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/token_hasher.py +5 -5
- thailint-0.11.0/src/linters/dry/typescript_analyzer.py +273 -0
- thailint-0.11.0/src/linters/dry/typescript_constant_extractor.py +134 -0
- thailint-0.11.0/src/linters/dry/typescript_statement_detector.py +255 -0
- thailint-0.11.0/src/linters/dry/typescript_value_extractor.py +66 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/linter.py +9 -13
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/linter.py +30 -10
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/pattern_matcher.py +19 -5
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/linter.py +8 -67
- thailint-0.11.0/src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/linter.py +12 -9
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/linter.py +7 -24
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/class_analyzer.py +9 -9
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/heuristics.py +6 -5
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/linter.py +4 -5
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/linter.py +2 -2
- thailint-0.11.0/src/linters/stringly_typed/__init__.py +23 -0
- thailint-0.11.0/src/linters/stringly_typed/config.py +165 -0
- thailint-0.11.0/src/linters/stringly_typed/python/__init__.py +29 -0
- thailint-0.11.0/src/linters/stringly_typed/python/analyzer.py +198 -0
- thailint-0.11.0/src/linters/stringly_typed/python/condition_extractor.py +131 -0
- thailint-0.11.0/src/linters/stringly_typed/python/conditional_detector.py +176 -0
- thailint-0.11.0/src/linters/stringly_typed/python/constants.py +21 -0
- thailint-0.11.0/src/linters/stringly_typed/python/match_analyzer.py +88 -0
- thailint-0.11.0/src/linters/stringly_typed/python/validation_detector.py +186 -0
- thailint-0.11.0/src/linters/stringly_typed/python/variable_extractor.py +96 -0
- thailint-0.11.0/src/orchestrator/core.py +462 -0
- thailint-0.9.0/src/cli.py +0 -2014
- thailint-0.9.0/src/linters/dry/python_analyzer.py +0 -668
- thailint-0.9.0/src/linters/dry/typescript_analyzer.py +0 -592
- thailint-0.9.0/src/orchestrator/core.py +0 -233
- {thailint-0.9.0 → thailint-0.11.0}/CHANGELOG.md +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/LICENSE +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/analyzers/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/api.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/base.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/cli_utils.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/config_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/linter_utils.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/registry.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/core/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/formatters/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/formatters/sarif.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/loader.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/block_grouper.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/cache_query.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_filter.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/bash_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/directory_matcher.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/pattern_validator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/config.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/orchestrator/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/utils/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.11.0}/src/utils/project_root.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thailint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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,6 +98,12 @@ 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)
|
|
@@ -741,6 +747,109 @@ Built-in filters automatically exclude common non-duplication patterns:
|
|
|
741
747
|
|
|
742
748
|
See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
|
|
743
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
|
+
|
|
744
853
|
## Magic Numbers Linter
|
|
745
854
|
|
|
746
855
|
### Overview
|
|
@@ -1351,6 +1460,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
|
|
|
1351
1460
|
# Lint specific subdirectory
|
|
1352
1461
|
docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
|
|
1353
1462
|
|
|
1463
|
+
# Collection pipeline linter
|
|
1464
|
+
docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
|
|
1465
|
+
|
|
1354
1466
|
# With custom config
|
|
1355
1467
|
docker run --rm -v $(pwd):/data \
|
|
1356
1468
|
washad/thailint:latest nesting --config /data/.thailint.yaml /data
|
|
@@ -1414,6 +1526,7 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1414
1526
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1415
1527
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1416
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
|
|
1417
1530
|
- **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
|
|
1418
1531
|
- **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
|
|
1419
1532
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
@@ -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,6 +63,12 @@ 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)
|
|
@@ -706,6 +712,109 @@ Built-in filters automatically exclude common non-duplication patterns:
|
|
|
706
712
|
|
|
707
713
|
See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
|
|
708
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
|
+
|
|
709
818
|
## Magic Numbers Linter
|
|
710
819
|
|
|
711
820
|
### Overview
|
|
@@ -1316,6 +1425,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
|
|
|
1316
1425
|
# Lint specific subdirectory
|
|
1317
1426
|
docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
|
|
1318
1427
|
|
|
1428
|
+
# Collection pipeline linter
|
|
1429
|
+
docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
|
|
1430
|
+
|
|
1319
1431
|
# With custom config
|
|
1320
1432
|
docker run --rm -v $(pwd):/data \
|
|
1321
1433
|
washad/thailint:latest nesting --config /data/.thailint.yaml /data
|
|
@@ -1379,6 +1491,7 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1379
1491
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1380
1492
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1381
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
|
|
1382
1495
|
- **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
|
|
1383
1496
|
- **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
|
|
1384
1497
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
@@ -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.11.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"
|
|
@@ -104,8 +104,8 @@ loguru = "^0.7.3"
|
|
|
104
104
|
pytest-xdist = "^3.8.0"
|
|
105
105
|
|
|
106
106
|
[tool.poetry.scripts]
|
|
107
|
-
thailint = "src.
|
|
108
|
-
thai-lint = "src.
|
|
107
|
+
thailint = "src.cli_main:cli"
|
|
108
|
+
thai-lint = "src.cli_main:cli"
|
|
109
109
|
|
|
110
110
|
# Ruff configuration
|
|
111
111
|
[tool.ruff]
|
|
@@ -228,6 +228,7 @@ disable = [
|
|
|
228
228
|
|
|
229
229
|
[tool.pylint.format]
|
|
230
230
|
max-line-length = 120
|
|
231
|
+
max-module-lines = 500
|
|
231
232
|
|
|
232
233
|
# Flake8 configuration (in .flake8 file, not pyproject.toml)
|
|
233
234
|
# Note: Flake8 doesn't support pyproject.toml natively
|
|
@@ -19,6 +19,7 @@ Exports: __version__, Linter (high-level API), cli (CLI entry point), load_confi
|
|
|
19
19
|
Interfaces: Package version string, Linter class API, CLI command group, configuration functions
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
__version__: str
|
|
22
23
|
try:
|
|
23
24
|
from importlib.metadata import version
|
|
24
25
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI package entry point and public API for thai-lint command-line interface
|
|
3
|
+
|
|
4
|
+
Scope: Re-export fully configured CLI with all commands registered
|
|
5
|
+
|
|
6
|
+
Overview: Provides the public API for the modular CLI package by re-exporting the CLI group from
|
|
7
|
+
src.cli.main and triggering command registration by importing submodules. Importing from this
|
|
8
|
+
module (src.cli) gives access to the complete CLI with all commands. Maintains backward
|
|
9
|
+
compatibility with code that imports from src.cli while enabling modular organization.
|
|
10
|
+
|
|
11
|
+
Dependencies: src.cli.main for CLI group, src.cli.config for config commands, src.cli.linters
|
|
12
|
+
for linter commands
|
|
13
|
+
|
|
14
|
+
Exports: cli (main Click command group with all commands registered)
|
|
15
|
+
|
|
16
|
+
Interfaces: Single import point for CLI access via 'from src.cli import cli'
|
|
17
|
+
|
|
18
|
+
Implementation: Imports submodules to trigger command registration via Click decorators
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Import the CLI group from main module
|
|
22
|
+
# Import config and linters to register their commands with the CLI group
|
|
23
|
+
from src.cli import config as _config_module # noqa: F401
|
|
24
|
+
from src.cli import linters as _linters_module # noqa: F401
|
|
25
|
+
from src.cli.main import cli # noqa: F401
|
|
26
|
+
|
|
27
|
+
__all__ = ["cli"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Entry point for running thai-lint CLI as a module (python -m src.cli)
|
|
3
|
+
|
|
4
|
+
Scope: Module execution support for direct CLI invocation
|
|
5
|
+
|
|
6
|
+
Overview: Enables running the CLI via 'python -m src.cli' by invoking the main cli group.
|
|
7
|
+
This file is executed when the package is run as a module, providing an alternative
|
|
8
|
+
entry point to the installed 'thailint' command.
|
|
9
|
+
|
|
10
|
+
Dependencies: src.cli for fully configured CLI
|
|
11
|
+
|
|
12
|
+
Exports: None (execution entry point only)
|
|
13
|
+
|
|
14
|
+
Interfaces: Command-line invocation via 'python -m src.cli [command] [args]'
|
|
15
|
+
|
|
16
|
+
Implementation: Imports and invokes cli() from the package
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from src.cli import cli
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
cli()
|