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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {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
+ )