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.
Files changed (154) hide show
  1. {thailint-0.9.0 → thailint-0.11.0}/PKG-INFO +116 -3
  2. {thailint-0.9.0 → thailint-0.11.0}/README.md +115 -2
  3. {thailint-0.9.0 → thailint-0.11.0}/pyproject.toml +4 -3
  4. {thailint-0.9.0 → thailint-0.11.0}/src/__init__.py +1 -0
  5. thailint-0.11.0/src/cli/__init__.py +27 -0
  6. thailint-0.11.0/src/cli/__main__.py +22 -0
  7. thailint-0.11.0/src/cli/config.py +478 -0
  8. thailint-0.11.0/src/cli/linters/__init__.py +58 -0
  9. thailint-0.11.0/src/cli/linters/code_patterns.py +372 -0
  10. thailint-0.11.0/src/cli/linters/code_smells.py +343 -0
  11. thailint-0.11.0/src/cli/linters/documentation.py +155 -0
  12. thailint-0.11.0/src/cli/linters/shared.py +89 -0
  13. thailint-0.11.0/src/cli/linters/structure.py +313 -0
  14. thailint-0.11.0/src/cli/linters/structure_quality.py +316 -0
  15. thailint-0.11.0/src/cli/main.py +120 -0
  16. thailint-0.11.0/src/cli/utils.py +375 -0
  17. thailint-0.11.0/src/cli_main.py +34 -0
  18. {thailint-0.9.0 → thailint-0.11.0}/src/config.py +2 -3
  19. {thailint-0.9.0 → thailint-0.11.0}/src/core/rule_discovery.py +43 -10
  20. {thailint-0.9.0 → thailint-0.11.0}/src/core/types.py +13 -0
  21. thailint-0.11.0/src/core/violation_utils.py +69 -0
  22. {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/ignore.py +32 -16
  23. thailint-0.11.0/src/linters/collection_pipeline/__init__.py +90 -0
  24. thailint-0.11.0/src/linters/collection_pipeline/config.py +63 -0
  25. thailint-0.11.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
  26. thailint-0.11.0/src/linters/collection_pipeline/detector.py +130 -0
  27. thailint-0.11.0/src/linters/collection_pipeline/linter.py +437 -0
  28. thailint-0.11.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
  29. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/block_filter.py +99 -9
  30. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/cache.py +94 -6
  31. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/config.py +47 -10
  32. thailint-0.11.0/src/linters/dry/constant.py +92 -0
  33. thailint-0.11.0/src/linters/dry/constant_matcher.py +214 -0
  34. thailint-0.11.0/src/linters/dry/constant_violation_builder.py +98 -0
  35. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/linter.py +89 -48
  36. thailint-0.11.0/src/linters/dry/python_analyzer.py +281 -0
  37. thailint-0.11.0/src/linters/dry/python_constant_extractor.py +101 -0
  38. thailint-0.11.0/src/linters/dry/single_statement_detector.py +415 -0
  39. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/token_hasher.py +5 -5
  40. thailint-0.11.0/src/linters/dry/typescript_analyzer.py +273 -0
  41. thailint-0.11.0/src/linters/dry/typescript_constant_extractor.py +134 -0
  42. thailint-0.11.0/src/linters/dry/typescript_statement_detector.py +255 -0
  43. thailint-0.11.0/src/linters/dry/typescript_value_extractor.py +66 -0
  44. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/linter.py +9 -13
  45. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/linter.py +30 -10
  46. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/pattern_matcher.py +19 -5
  47. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/linter.py +8 -67
  48. thailint-0.11.0/src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  49. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/linter.py +12 -9
  50. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/linter.py +7 -24
  51. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/class_analyzer.py +9 -9
  52. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/heuristics.py +6 -5
  53. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/linter.py +4 -5
  54. {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/linter.py +2 -2
  55. thailint-0.11.0/src/linters/stringly_typed/__init__.py +23 -0
  56. thailint-0.11.0/src/linters/stringly_typed/config.py +165 -0
  57. thailint-0.11.0/src/linters/stringly_typed/python/__init__.py +29 -0
  58. thailint-0.11.0/src/linters/stringly_typed/python/analyzer.py +198 -0
  59. thailint-0.11.0/src/linters/stringly_typed/python/condition_extractor.py +131 -0
  60. thailint-0.11.0/src/linters/stringly_typed/python/conditional_detector.py +176 -0
  61. thailint-0.11.0/src/linters/stringly_typed/python/constants.py +21 -0
  62. thailint-0.11.0/src/linters/stringly_typed/python/match_analyzer.py +88 -0
  63. thailint-0.11.0/src/linters/stringly_typed/python/validation_detector.py +186 -0
  64. thailint-0.11.0/src/linters/stringly_typed/python/variable_extractor.py +96 -0
  65. thailint-0.11.0/src/orchestrator/core.py +462 -0
  66. thailint-0.9.0/src/cli.py +0 -2014
  67. thailint-0.9.0/src/linters/dry/python_analyzer.py +0 -668
  68. thailint-0.9.0/src/linters/dry/typescript_analyzer.py +0 -592
  69. thailint-0.9.0/src/orchestrator/core.py +0 -233
  70. {thailint-0.9.0 → thailint-0.11.0}/CHANGELOG.md +0 -0
  71. {thailint-0.9.0 → thailint-0.11.0}/LICENSE +0 -0
  72. {thailint-0.9.0 → thailint-0.11.0}/src/analyzers/__init__.py +0 -0
  73. {thailint-0.9.0 → thailint-0.11.0}/src/analyzers/typescript_base.py +0 -0
  74. {thailint-0.9.0 → thailint-0.11.0}/src/api.py +0 -0
  75. {thailint-0.9.0 → thailint-0.11.0}/src/core/__init__.py +0 -0
  76. {thailint-0.9.0 → thailint-0.11.0}/src/core/base.py +0 -0
  77. {thailint-0.9.0 → thailint-0.11.0}/src/core/cli_utils.py +0 -0
  78. {thailint-0.9.0 → thailint-0.11.0}/src/core/config_parser.py +0 -0
  79. {thailint-0.9.0 → thailint-0.11.0}/src/core/linter_utils.py +0 -0
  80. {thailint-0.9.0 → thailint-0.11.0}/src/core/registry.py +0 -0
  81. {thailint-0.9.0 → thailint-0.11.0}/src/core/violation_builder.py +0 -0
  82. {thailint-0.9.0 → thailint-0.11.0}/src/formatters/__init__.py +0 -0
  83. {thailint-0.9.0 → thailint-0.11.0}/src/formatters/sarif.py +0 -0
  84. {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/__init__.py +0 -0
  85. {thailint-0.9.0 → thailint-0.11.0}/src/linter_config/loader.py +0 -0
  86. {thailint-0.9.0 → thailint-0.11.0}/src/linters/__init__.py +0 -0
  87. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/__init__.py +0 -0
  88. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/base_token_analyzer.py +0 -0
  89. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/block_grouper.py +0 -0
  90. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/cache_query.py +0 -0
  91. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/config_loader.py +0 -0
  92. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/deduplicator.py +0 -0
  93. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/duplicate_storage.py +0 -0
  94. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/file_analyzer.py +0 -0
  95. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/inline_ignore.py +0 -0
  96. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/storage_initializer.py +0 -0
  97. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_builder.py +0 -0
  98. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_filter.py +0 -0
  99. {thailint-0.9.0 → thailint-0.11.0}/src/linters/dry/violation_generator.py +0 -0
  100. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/__init__.py +0 -0
  101. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/atemporal_detector.py +0 -0
  102. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/base_parser.py +0 -0
  103. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/bash_parser.py +0 -0
  104. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/config.py +0 -0
  105. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/css_parser.py +0 -0
  106. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/field_validator.py +0 -0
  107. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/markdown_parser.py +0 -0
  108. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/python_parser.py +0 -0
  109. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/typescript_parser.py +0 -0
  110. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_header/violation_builder.py +0 -0
  111. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/__init__.py +0 -0
  112. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/config_loader.py +0 -0
  113. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/directory_matcher.py +0 -0
  114. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/path_resolver.py +0 -0
  115. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/pattern_validator.py +0 -0
  116. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/rule_checker.py +0 -0
  117. {thailint-0.9.0 → thailint-0.11.0}/src/linters/file_placement/violation_factory.py +0 -0
  118. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/__init__.py +0 -0
  119. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/config.py +0 -0
  120. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
  121. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
  122. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  123. {thailint-0.9.0 → thailint-0.11.0}/src/linters/magic_numbers/violation_builder.py +0 -0
  124. {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/__init__.py +0 -0
  125. {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/config.py +0 -0
  126. {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/linter.py +0 -0
  127. {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/python_analyzer.py +0 -0
  128. {thailint-0.9.0 → thailint-0.11.0}/src/linters/method_property/violation_builder.py +0 -0
  129. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/__init__.py +0 -0
  130. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/config.py +0 -0
  131. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/python_analyzer.py +0 -0
  132. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/typescript_analyzer.py +0 -0
  133. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
  134. {thailint-0.9.0 → thailint-0.11.0}/src/linters/nesting/violation_builder.py +0 -0
  135. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/__init__.py +0 -0
  136. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/config.py +0 -0
  137. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/python_analyzer.py +0 -0
  138. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
  139. {thailint-0.9.0 → thailint-0.11.0}/src/linters/print_statements/violation_builder.py +0 -0
  140. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/__init__.py +0 -0
  141. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/config.py +0 -0
  142. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/metrics_evaluator.py +0 -0
  143. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/python_analyzer.py +0 -0
  144. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/typescript_analyzer.py +0 -0
  145. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  146. {thailint-0.9.0 → thailint-0.11.0}/src/linters/srp/violation_builder.py +0 -0
  147. {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/__init__.py +0 -0
  148. {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/config.py +0 -0
  149. {thailint-0.9.0 → thailint-0.11.0}/src/linters/stateless_class/python_analyzer.py +0 -0
  150. {thailint-0.9.0 → thailint-0.11.0}/src/orchestrator/__init__.py +0 -0
  151. {thailint-0.9.0 → thailint-0.11.0}/src/orchestrator/language_detector.py +0 -0
  152. {thailint-0.9.0 → thailint-0.11.0}/src/templates/thailint_config_template.yaml +0 -0
  153. {thailint-0.9.0 → thailint-0.11.0}/src/utils/__init__.py +0 -0
  154. {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.9.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
  [![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-728%2F728%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-884%2F884%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,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
  [![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-728%2F728%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-884%2F884%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,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.9.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.cli:cli"
108
- thai-lint = "src.cli:cli"
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()