thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CollectionPipelineRule implementation for detecting loop filtering anti-patterns
|
|
3
|
+
|
|
4
|
+
Scope: Main rule class implementing BaseLintRule interface for collection-pipeline detection
|
|
5
|
+
|
|
6
|
+
Overview: Implements the BaseLintRule interface to detect for loops with embedded
|
|
7
|
+
filtering logic that could be refactored to collection pipelines. Detects patterns
|
|
8
|
+
like 'for x in iter: if not cond: continue; action(x)' which can be refactored to
|
|
9
|
+
use generator expressions or filter(). Based on Martin Fowler's refactoring pattern.
|
|
10
|
+
Integrates with thai-lint CLI and supports text, JSON, and SARIF output formats.
|
|
11
|
+
Supports comprehensive 5-level ignore system including project-level patterns,
|
|
12
|
+
linter-specific ignore patterns, file-level directives, line-level directives,
|
|
13
|
+
and block-level directives via IgnoreDirectiveParser.
|
|
14
|
+
|
|
15
|
+
Dependencies: BaseLintRule, BaseLintContext, Violation, PipelinePatternDetector,
|
|
16
|
+
CollectionPipelineConfig, IgnoreDirectiveParser
|
|
17
|
+
|
|
18
|
+
Exports: CollectionPipelineRule class
|
|
19
|
+
|
|
20
|
+
Interfaces: CollectionPipelineRule.check(context) -> list[Violation], rule metadata properties
|
|
21
|
+
|
|
22
|
+
Implementation: Uses PipelinePatternDetector for AST analysis, composition pattern with
|
|
23
|
+
config loading and comprehensive ignore checking via IgnoreDirectiveParser
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- srp,dry: Rule class coordinates detector, config, and comprehensive ignore system.
|
|
27
|
+
Method count exceeds limit due to 5-level ignore pattern support.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
33
|
+
from src.core.constants import HEADER_SCAN_LINES, IgnoreDirective, Language
|
|
34
|
+
from src.core.types import Severity, Violation
|
|
35
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
36
|
+
from src.linter_config.rule_matcher import rule_matches
|
|
37
|
+
|
|
38
|
+
from .config import CollectionPipelineConfig
|
|
39
|
+
from .detector import PatternMatch, PipelinePatternDetector
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CollectionPipelineRule(BaseLintRule): # thailint: ignore[srp,dry]
|
|
43
|
+
"""Detects for loops with embedded filtering that could use collection pipelines."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
"""Initialize the rule with ignore parser."""
|
|
47
|
+
self._ignore_parser = get_ignore_parser()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def rule_id(self) -> str:
|
|
51
|
+
"""Unique identifier for this rule."""
|
|
52
|
+
return "collection-pipeline.embedded-filter"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def rule_name(self) -> str:
|
|
56
|
+
"""Human-readable name for this rule."""
|
|
57
|
+
return "Embedded Loop Filtering"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def description(self) -> str:
|
|
61
|
+
"""Description of what this rule checks."""
|
|
62
|
+
return (
|
|
63
|
+
"For loops with embedded if/continue filtering patterns should be "
|
|
64
|
+
"refactored to use collection pipelines (generator expressions, filter())"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
68
|
+
"""Check for collection pipeline anti-patterns.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
context: Lint context with file information
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of violations found
|
|
75
|
+
"""
|
|
76
|
+
if not self._should_analyze(context):
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
config = self._load_config(context)
|
|
80
|
+
if not config.enabled:
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
if self._is_file_ignored(context, config):
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
if self._has_file_level_ignore(context):
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
return self._analyze_python(context, config)
|
|
90
|
+
|
|
91
|
+
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
92
|
+
"""Check if context should be analyzed.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
context: Lint context
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if should analyze
|
|
99
|
+
"""
|
|
100
|
+
return context.language == Language.PYTHON and context.file_content is not None
|
|
101
|
+
|
|
102
|
+
def _get_config_dict(self, context: BaseLintContext) -> dict | None:
|
|
103
|
+
"""Get configuration dictionary from context.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
context: Lint context
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Config dict or None
|
|
110
|
+
"""
|
|
111
|
+
if hasattr(context, "config") and context.config is not None:
|
|
112
|
+
return context.config
|
|
113
|
+
if hasattr(context, "metadata") and context.metadata is not None:
|
|
114
|
+
return context.metadata
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def _load_config(self, context: BaseLintContext) -> CollectionPipelineConfig:
|
|
118
|
+
"""Load configuration from context.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
context: Lint context
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
CollectionPipelineConfig instance
|
|
125
|
+
"""
|
|
126
|
+
config_dict = self._get_config_dict(context)
|
|
127
|
+
if config_dict is None or not isinstance(config_dict, dict):
|
|
128
|
+
return CollectionPipelineConfig()
|
|
129
|
+
|
|
130
|
+
# Check for collection_pipeline or collection-pipeline specific config
|
|
131
|
+
linter_config = config_dict.get(
|
|
132
|
+
"collection_pipeline", config_dict.get("collection-pipeline", config_dict)
|
|
133
|
+
)
|
|
134
|
+
return CollectionPipelineConfig.from_dict(linter_config)
|
|
135
|
+
|
|
136
|
+
def _is_file_ignored(self, context: BaseLintContext, config: CollectionPipelineConfig) -> bool:
|
|
137
|
+
"""Check if file matches ignore patterns.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
context: Lint context
|
|
141
|
+
config: Configuration
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if file should be ignored
|
|
145
|
+
"""
|
|
146
|
+
if not config.ignore:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
if not context.file_path:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
file_path = Path(context.file_path)
|
|
153
|
+
return any(self._matches_pattern(file_path, pattern) for pattern in config.ignore)
|
|
154
|
+
|
|
155
|
+
def _matches_pattern(self, file_path: Path, pattern: str) -> bool:
|
|
156
|
+
"""Check if file path matches a glob pattern.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
file_path: Path to check
|
|
160
|
+
pattern: Glob pattern
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if path matches pattern
|
|
164
|
+
"""
|
|
165
|
+
if file_path.match(pattern):
|
|
166
|
+
return True
|
|
167
|
+
if pattern in str(file_path):
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def _has_file_level_ignore(self, context: BaseLintContext) -> bool:
|
|
172
|
+
"""Check if file has file-level ignore directive.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
context: Lint context
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if file should be ignored at file level
|
|
179
|
+
"""
|
|
180
|
+
if not context.file_content:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
# Check first lines for ignore-file directive
|
|
184
|
+
lines = context.file_content.splitlines()[:HEADER_SCAN_LINES]
|
|
185
|
+
return any(self._is_file_ignore_directive(line) for line in lines)
|
|
186
|
+
|
|
187
|
+
def _is_file_ignore_directive(self, line: str) -> bool:
|
|
188
|
+
"""Check if line is a file-level ignore directive.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
line: Line to check
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if line has file-level ignore for this rule
|
|
195
|
+
"""
|
|
196
|
+
line_lower = line.lower()
|
|
197
|
+
if "thailint: ignore-file" not in line_lower:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Check for general ignore-file (no rule specified)
|
|
201
|
+
if "ignore-file[" not in line_lower:
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
# Check for rule-specific ignore
|
|
205
|
+
return self._matches_rule_ignore(line_lower, "ignore-file")
|
|
206
|
+
|
|
207
|
+
def _matches_rule_ignore(self, line: str, directive: str) -> bool:
|
|
208
|
+
"""Check if line matches rule-specific ignore.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
line: Line to check (lowercase)
|
|
212
|
+
directive: Directive name (ignore-file or ignore)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if ignore applies to this rule
|
|
216
|
+
"""
|
|
217
|
+
import re
|
|
218
|
+
|
|
219
|
+
pattern = rf"{directive}\[([^\]]+)\]"
|
|
220
|
+
match = re.search(pattern, line)
|
|
221
|
+
if not match:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
rules = [r.strip().lower() for r in match.group(1).split(",")]
|
|
225
|
+
return any(self._rule_matches(r) for r in rules)
|
|
226
|
+
|
|
227
|
+
def _rule_matches(self, rule_pattern: str) -> bool:
|
|
228
|
+
"""Check if rule pattern matches this rule.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
rule_pattern: Rule pattern to check
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if pattern matches this rule
|
|
235
|
+
"""
|
|
236
|
+
return rule_matches(self.rule_id, rule_pattern)
|
|
237
|
+
|
|
238
|
+
def _analyze_python(
|
|
239
|
+
self, context: BaseLintContext, config: CollectionPipelineConfig
|
|
240
|
+
) -> list[Violation]:
|
|
241
|
+
"""Analyze Python code for collection pipeline patterns.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
context: Lint context with Python file information
|
|
245
|
+
config: Collection pipeline configuration
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of violations found
|
|
249
|
+
"""
|
|
250
|
+
detector = PipelinePatternDetector(context.file_content or "")
|
|
251
|
+
matches = detector.detect_patterns()
|
|
252
|
+
|
|
253
|
+
return self._filter_matches_to_violations(matches, config, context)
|
|
254
|
+
|
|
255
|
+
def _filter_matches_to_violations(
|
|
256
|
+
self,
|
|
257
|
+
matches: list[PatternMatch],
|
|
258
|
+
config: CollectionPipelineConfig,
|
|
259
|
+
context: BaseLintContext,
|
|
260
|
+
) -> list[Violation]:
|
|
261
|
+
"""Filter matches by threshold and ignore rules.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
matches: Detected pattern matches
|
|
265
|
+
config: Configuration with thresholds
|
|
266
|
+
context: Lint context
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of violations after filtering
|
|
270
|
+
"""
|
|
271
|
+
return [
|
|
272
|
+
violation
|
|
273
|
+
for match in matches
|
|
274
|
+
if (violation := self._process_match(match, config, context))
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
def _process_match(
|
|
278
|
+
self,
|
|
279
|
+
match: PatternMatch,
|
|
280
|
+
config: CollectionPipelineConfig,
|
|
281
|
+
context: BaseLintContext,
|
|
282
|
+
) -> Violation | None:
|
|
283
|
+
"""Process a single match into a violation if applicable.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
match: Pattern match to process
|
|
287
|
+
config: Configuration with thresholds
|
|
288
|
+
context: Lint context
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Violation if match should be reported, None otherwise
|
|
292
|
+
"""
|
|
293
|
+
if len(match.conditions) < config.min_continues:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
violation = self._create_violation(match, context)
|
|
297
|
+
if self._should_ignore_violation(violation, match.line_number, context):
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
return violation
|
|
301
|
+
|
|
302
|
+
def _should_ignore_violation(
|
|
303
|
+
self, violation: Violation, line_num: int, context: BaseLintContext
|
|
304
|
+
) -> bool:
|
|
305
|
+
"""Check if violation should be ignored.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
violation: Violation to check
|
|
309
|
+
line_num: Line number of the violation
|
|
310
|
+
context: Lint context
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
True if violation should be ignored
|
|
314
|
+
"""
|
|
315
|
+
if not context.file_content:
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
# Check using IgnoreDirectiveParser for comprehensive ignore checking
|
|
319
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content):
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
# Also check inline ignore on loop line
|
|
323
|
+
return self._has_inline_ignore(line_num, context)
|
|
324
|
+
|
|
325
|
+
def _has_inline_ignore(self, line_num: int, context: BaseLintContext) -> bool:
|
|
326
|
+
"""Check for inline ignore directive on loop line.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
line_num: Line number to check
|
|
330
|
+
context: Lint context
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
True if line has ignore directive
|
|
334
|
+
"""
|
|
335
|
+
line = self._get_line_text(line_num, context)
|
|
336
|
+
if not line:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
return self._is_ignore_directive(line.lower())
|
|
340
|
+
|
|
341
|
+
def _get_line_text(self, line_num: int, context: BaseLintContext) -> str | None:
|
|
342
|
+
"""Get text of a specific line.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
line_num: Line number (1-indexed)
|
|
346
|
+
context: Lint context
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Line text or None if invalid
|
|
350
|
+
"""
|
|
351
|
+
if not context.file_content:
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
lines = context.file_content.splitlines()
|
|
355
|
+
if line_num <= 0 or line_num > len(lines):
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
return lines[line_num - 1]
|
|
359
|
+
|
|
360
|
+
def _is_ignore_directive(self, line: str) -> bool:
|
|
361
|
+
"""Check if line contains ignore directive for this rule.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
line: Line text (lowercase)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if line has applicable ignore directive
|
|
368
|
+
"""
|
|
369
|
+
if "thailint:" not in line or "ignore" not in line:
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
# General ignore (no rule specified)
|
|
373
|
+
if "ignore[" not in line:
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
# Rule-specific ignore
|
|
377
|
+
return self._matches_rule_ignore(line, IgnoreDirective.IGNORE)
|
|
378
|
+
|
|
379
|
+
def _create_violation(self, match: PatternMatch, context: BaseLintContext) -> Violation:
|
|
380
|
+
"""Create a Violation from a PatternMatch.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
match: Detected pattern match
|
|
384
|
+
context: Lint context
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Violation object for the detected pattern
|
|
388
|
+
"""
|
|
389
|
+
message = self._build_message(match)
|
|
390
|
+
file_path = str(context.file_path) if context.file_path else "unknown"
|
|
391
|
+
|
|
392
|
+
return Violation(
|
|
393
|
+
rule_id=self.rule_id,
|
|
394
|
+
file_path=file_path,
|
|
395
|
+
line=match.line_number,
|
|
396
|
+
column=0,
|
|
397
|
+
message=message,
|
|
398
|
+
severity=Severity.ERROR,
|
|
399
|
+
suggestion=match.suggestion,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _build_message(self, match: PatternMatch) -> str:
|
|
403
|
+
"""Build violation message.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
match: Detected pattern match
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Human-readable message describing the violation
|
|
410
|
+
"""
|
|
411
|
+
num_conditions = len(match.conditions)
|
|
412
|
+
if num_conditions == 1:
|
|
413
|
+
return (
|
|
414
|
+
f"For loop over '{match.iterable}' has embedded filtering. "
|
|
415
|
+
f"Consider using a generator expression."
|
|
416
|
+
)
|
|
417
|
+
return (
|
|
418
|
+
f"For loop over '{match.iterable}' has {num_conditions} filter conditions. "
|
|
419
|
+
f"Consider combining into a collection pipeline."
|
|
420
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build refactoring suggestions for collection pipeline patterns
|
|
3
|
+
|
|
4
|
+
Scope: Generate code suggestions for converting embedded filtering to collection pipelines
|
|
5
|
+
|
|
6
|
+
Overview: Provides helper functions for generating refactoring suggestions when embedded
|
|
7
|
+
filtering patterns are detected. Handles condition inversion (converting continue guard
|
|
8
|
+
conditions to filter conditions), target name extraction, and suggestion string generation.
|
|
9
|
+
Separates suggestion logic from pattern detection logic for better maintainability.
|
|
10
|
+
|
|
11
|
+
Dependencies: ast module for Python AST processing
|
|
12
|
+
|
|
13
|
+
Exports: build_suggestion, invert_condition, get_target_name, build_any_suggestion,
|
|
14
|
+
build_all_suggestion, build_filter_map_suggestion, build_takewhile_suggestion
|
|
15
|
+
|
|
16
|
+
Interfaces: Functions for suggestion generation and condition transformation
|
|
17
|
+
|
|
18
|
+
Implementation: AST-based condition inversion and string formatting for suggestions
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_target_name(target: ast.expr) -> str:
|
|
25
|
+
"""Get the loop variable name from AST target.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
target: AST expression for loop target
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
String representation of the loop variable
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(target, ast.Name):
|
|
34
|
+
return target.id
|
|
35
|
+
return ast.unparse(target)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def invert_condition(condition: ast.expr) -> str:
|
|
39
|
+
"""Invert a condition (for if not x: continue -> if x).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
condition: AST expression for the condition
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
String representation of the inverted condition
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(condition, ast.UnaryOp) and isinstance(condition.op, ast.Not):
|
|
48
|
+
return ast.unparse(condition.operand)
|
|
49
|
+
return f"not ({ast.unparse(condition)})"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_suggestion(loop_var: str, iterable: str, conditions: list[str]) -> str:
|
|
53
|
+
"""Generate refactoring suggestion code snippet.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
loop_var: Name of the loop variable
|
|
57
|
+
iterable: Source representation of the iterable
|
|
58
|
+
conditions: List of filter conditions (already inverted)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Code suggestion for refactoring to generator expression
|
|
62
|
+
"""
|
|
63
|
+
combined = " and ".join(conditions)
|
|
64
|
+
return f"for {loop_var} in ({loop_var} for {loop_var} in {iterable} if {combined}):"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_any_suggestion(loop_var: str, iterable: str, condition: str) -> str:
|
|
68
|
+
"""Generate any() refactoring suggestion.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
loop_var: Name of the loop variable
|
|
72
|
+
iterable: Source representation of the iterable
|
|
73
|
+
condition: The filter condition
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Code suggestion for refactoring to any()
|
|
77
|
+
"""
|
|
78
|
+
return f"return any({condition} for {loop_var} in {iterable})"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_all_suggestion(loop_var: str, iterable: str, condition: str) -> str:
|
|
82
|
+
"""Generate all() refactoring suggestion.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
loop_var: Name of the loop variable
|
|
86
|
+
iterable: Source representation of the iterable
|
|
87
|
+
condition: The filter condition (already inverted to positive form)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Code suggestion for refactoring to all()
|
|
91
|
+
"""
|
|
92
|
+
return f"return all({condition} for {loop_var} in {iterable})"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_filter_map_suggestion(
|
|
96
|
+
loop_var: str,
|
|
97
|
+
iterable: str,
|
|
98
|
+
transform_var: str,
|
|
99
|
+
transform_expr: str,
|
|
100
|
+
use_walrus: bool = True,
|
|
101
|
+
) -> str:
|
|
102
|
+
"""Generate filter-map list comprehension suggestion.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
loop_var: Name of the loop variable
|
|
106
|
+
iterable: Source representation of the iterable
|
|
107
|
+
transform_var: Name of the transform result variable
|
|
108
|
+
transform_expr: The transform expression
|
|
109
|
+
use_walrus: Whether to use walrus operator (Python 3.8+)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Code suggestion for refactoring to list comprehension
|
|
113
|
+
"""
|
|
114
|
+
if use_walrus:
|
|
115
|
+
return f"return [{transform_var} for {loop_var} in {iterable} if ({transform_var} := {transform_expr})]"
|
|
116
|
+
return f"return [{transform_expr} for {loop_var} in {iterable} if {transform_expr}]"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_takewhile_suggestion(loop_var: str, iterable: str, condition: str) -> str:
|
|
120
|
+
"""Generate takewhile() refactoring suggestion.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
loop_var: Name of the loop variable
|
|
124
|
+
iterable: Source representation of the iterable
|
|
125
|
+
condition: The condition for takewhile (positive form)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Code suggestion for refactoring to takewhile()
|
|
129
|
+
"""
|
|
130
|
+
return f"return list(takewhile(lambda {loop_var}: {condition}, {iterable}))"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CQS (Command-Query Separation) linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect CQS violations in Python and TypeScript code
|
|
5
|
+
|
|
6
|
+
Overview: Package providing CQS violation detection for Python and TypeScript code.
|
|
7
|
+
Identifies functions that mix INPUT operations (queries that return values captured
|
|
8
|
+
in variables) and OUTPUT operations (commands that perform side effects without
|
|
9
|
+
capturing return values). Functions should either query state and return a value,
|
|
10
|
+
or command a change and return nothing. Mixing these violates CQS principles and
|
|
11
|
+
makes code harder to reason about.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python parsing, tree-sitter for TypeScript parsing
|
|
14
|
+
|
|
15
|
+
Exports: CQSConfig, CQSPattern, CQSRule, FunctionAnalyzer, InputOperation, OutputOperation,
|
|
16
|
+
PythonCQSAnalyzer, TypeScriptCQSAnalyzer, TypeScriptFunctionAnalyzer,
|
|
17
|
+
TypeScriptInputDetector, TypeScriptOutputDetector, build_cqs_violation
|
|
18
|
+
|
|
19
|
+
Interfaces: CQSConfig.from_dict() for YAML configuration loading,
|
|
20
|
+
CQSRule.check() for BaseLintRule interface
|
|
21
|
+
|
|
22
|
+
Implementation: AST-based pattern detection for Python, tree-sitter for TypeScript,
|
|
23
|
+
with configurable ignore rules
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .config import CQSConfig
|
|
27
|
+
from .function_analyzer import FunctionAnalyzer
|
|
28
|
+
from .input_detector import InputDetector
|
|
29
|
+
from .linter import CQSRule
|
|
30
|
+
from .output_detector import OutputDetector
|
|
31
|
+
from .python_analyzer import PythonCQSAnalyzer
|
|
32
|
+
from .types import CQSPattern, InputOperation, OutputOperation
|
|
33
|
+
from .typescript_cqs_analyzer import TypeScriptCQSAnalyzer
|
|
34
|
+
from .typescript_function_analyzer import TypeScriptFunctionAnalyzer
|
|
35
|
+
from .typescript_input_detector import TypeScriptInputDetector
|
|
36
|
+
from .typescript_output_detector import TypeScriptOutputDetector
|
|
37
|
+
from .violation_builder import build_cqs_violation
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"CQSConfig",
|
|
41
|
+
"CQSPattern",
|
|
42
|
+
"CQSRule",
|
|
43
|
+
"FunctionAnalyzer",
|
|
44
|
+
"InputDetector",
|
|
45
|
+
"InputOperation",
|
|
46
|
+
"OutputDetector",
|
|
47
|
+
"OutputOperation",
|
|
48
|
+
"PythonCQSAnalyzer",
|
|
49
|
+
"TypeScriptCQSAnalyzer",
|
|
50
|
+
"TypeScriptFunctionAnalyzer",
|
|
51
|
+
"TypeScriptInputDetector",
|
|
52
|
+
"TypeScriptOutputDetector",
|
|
53
|
+
"build_cqs_violation",
|
|
54
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for CQS (Command-Query Separation) linter
|
|
3
|
+
|
|
4
|
+
Scope: Pattern toggles, ignore patterns, and YAML configuration loading
|
|
5
|
+
|
|
6
|
+
Overview: Provides CQSConfig dataclass with configurable options for CQS violation
|
|
7
|
+
detection. Controls minimum operation thresholds, methods to ignore (constructors
|
|
8
|
+
by default), decorators to ignore (property-like by default), and fluent interface
|
|
9
|
+
detection. Configuration can be loaded from dictionary (YAML) with sensible defaults.
|
|
10
|
+
|
|
11
|
+
Dependencies: dataclasses, typing
|
|
12
|
+
|
|
13
|
+
Exports: CQSConfig
|
|
14
|
+
|
|
15
|
+
Interfaces: CQSConfig.from_dict() for YAML configuration loading
|
|
16
|
+
|
|
17
|
+
Implementation: Dataclass with factory defaults and conservative default settings
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CQSConfig:
|
|
26
|
+
"""Configuration for CQS linter."""
|
|
27
|
+
|
|
28
|
+
enabled: bool = True
|
|
29
|
+
min_operations: int = 1
|
|
30
|
+
ignore_methods: list[str] = field(default_factory=lambda: ["__init__", "__new__"])
|
|
31
|
+
ignore_decorators: list[str] = field(default_factory=lambda: ["property", "cached_property"])
|
|
32
|
+
ignore_patterns: list[str] = field(default_factory=list)
|
|
33
|
+
detect_fluent_interface: bool = True
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, config: dict[str, Any], language: str | None = None) -> "CQSConfig":
|
|
37
|
+
"""Load configuration from dictionary (YAML).
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Dictionary containing configuration values.
|
|
41
|
+
language: Reserved for future multi-language support.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
CQSConfig instance with values from dictionary or defaults.
|
|
45
|
+
"""
|
|
46
|
+
# Language parameter reserved for future multi-language support
|
|
47
|
+
_ = language
|
|
48
|
+
return cls(
|
|
49
|
+
enabled=config.get("enabled", True),
|
|
50
|
+
min_operations=config.get("min_operations", 1),
|
|
51
|
+
ignore_methods=config.get("ignore_methods", ["__init__", "__new__"]),
|
|
52
|
+
ignore_decorators=config.get("ignore_decorators", ["property", "cached_property"]),
|
|
53
|
+
ignore_patterns=config.get("ignore_patterns", []),
|
|
54
|
+
detect_fluent_interface=config.get("detect_fluent_interface", True),
|
|
55
|
+
)
|