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,287 @@
1
+ """
2
+ Purpose: CLI commands for performance linters (string-concat-loop, regex-in-loop, perf)
3
+
4
+ Scope: Commands that detect performance anti-patterns in loops
5
+
6
+ Overview: Provides CLI commands for performance anti-pattern detection: string-concat-loop
7
+ finds O(n^2) string concatenation using += in loops, regex-in-loop detects repeated
8
+ regex compilation inside loops. The `perf` command runs all performance rules together
9
+ with optional --rule flag to select specific rules. Each command supports standard options
10
+ (config, format, recursive) and integrates with the orchestrator for execution.
11
+
12
+ Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
13
+ src.cli.linters.shared for linter-specific helpers
14
+
15
+ Exports: string_concat_loop command, regex_in_loop command, perf command
16
+
17
+ Interfaces: Click CLI commands registered to main CLI group
18
+
19
+ Implementation: Click decorators for command definition, orchestrator-based linting execution
20
+
21
+ Suppressions:
22
+ - too-many-arguments,too-many-positional-arguments: Click commands with custom options require
23
+ additional parameters beyond the standard 5 (ctx + 4 standard options). The perf command adds
24
+ --rule option for 6 total parameters - framework design requirement for CLI extensibility.
25
+ """
26
+
27
+ import logging
28
+ import sys
29
+ from pathlib import Path
30
+ from typing import TYPE_CHECKING, NoReturn
31
+
32
+ import click
33
+
34
+ from src.cli.linters.shared import (
35
+ ExecuteParams,
36
+ create_linter_command,
37
+ prepare_standard_command,
38
+ run_linter_command,
39
+ standard_linter_options,
40
+ )
41
+ from src.cli.main import cli
42
+ from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
43
+ from src.core.cli_utils import format_violations
44
+ from src.core.types import Violation
45
+
46
+ if TYPE_CHECKING:
47
+ from src.orchestrator.core import Orchestrator
48
+
49
+ # Configure module logger
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ # =============================================================================
54
+ # String Concat Loop Command
55
+ # =============================================================================
56
+
57
+
58
+ def _setup_performance_orchestrator(
59
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
60
+ ) -> "Orchestrator":
61
+ """Set up orchestrator for performance linting."""
62
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
63
+
64
+
65
+ def _setup_and_validate(params: ExecuteParams) -> "Orchestrator":
66
+ """Validate paths and set up orchestrator for linting.
67
+
68
+ Common setup code extracted to avoid DRY violations across execute functions.
69
+ """
70
+ validate_paths_exist(params.path_objs)
71
+ return _setup_performance_orchestrator(
72
+ params.path_objs, params.config_file, params.verbose, params.project_root
73
+ )
74
+
75
+
76
+ def _run_string_concat_lint(
77
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
78
+ ) -> list[Violation]:
79
+ """Execute string-concat-loop lint on files or directories."""
80
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
81
+ return [v for v in all_violations if v.rule_id == "performance.string-concat-loop"]
82
+
83
+
84
+ def _execute_string_concat_lint(params: ExecuteParams) -> NoReturn:
85
+ """Execute string-concat-loop lint."""
86
+ orchestrator = _setup_and_validate(params)
87
+ violations = _run_string_concat_lint(
88
+ orchestrator, params.path_objs, params.recursive, params.parallel
89
+ )
90
+
91
+ if params.verbose:
92
+ logger.info(f"Found {len(violations)} string-concat-loop violation(s)")
93
+
94
+ format_violations(violations, params.format)
95
+ sys.exit(1 if violations else 0)
96
+
97
+
98
+ string_concat_loop = create_linter_command(
99
+ "string-concat-loop",
100
+ _execute_string_concat_lint,
101
+ "Check for string concatenation in loops.",
102
+ "Detects O(n^2) string building patterns using += in for/while loops.\n"
103
+ " This is a common performance anti-pattern in Python and TypeScript.",
104
+ )
105
+
106
+
107
+ # =============================================================================
108
+ # Regex In Loop Command
109
+ # =============================================================================
110
+
111
+
112
+ def _run_regex_in_loop_lint(
113
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
114
+ ) -> list[Violation]:
115
+ """Execute regex-in-loop lint on files or directories."""
116
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
117
+ return [v for v in all_violations if v.rule_id == "performance.regex-in-loop"]
118
+
119
+
120
+ def _execute_regex_in_loop_lint(params: ExecuteParams) -> NoReturn:
121
+ """Execute regex-in-loop lint."""
122
+ orchestrator = _setup_and_validate(params)
123
+ violations = _run_regex_in_loop_lint(
124
+ orchestrator, params.path_objs, params.recursive, params.parallel
125
+ )
126
+
127
+ if params.verbose:
128
+ logger.info(f"Found {len(violations)} regex-in-loop violation(s)")
129
+
130
+ format_violations(violations, params.format)
131
+ sys.exit(1 if violations else 0)
132
+
133
+
134
+ regex_in_loop = create_linter_command(
135
+ "regex-in-loop",
136
+ _execute_regex_in_loop_lint,
137
+ "Check for regex compilation in loops.",
138
+ "Detects re.match(), re.search(), re.sub(), re.findall(), re.split(), and\n"
139
+ " re.fullmatch() calls inside loops. These recompile the regex pattern on\n"
140
+ " each iteration instead of compiling once with re.compile().",
141
+ )
142
+
143
+
144
+ # =============================================================================
145
+ # Combined Perf Command
146
+ # =============================================================================
147
+
148
+ # Valid rule names for the --rule option
149
+ PERF_RULES = {
150
+ "string-concat": "performance.string-concat-loop",
151
+ "regex-loop": "performance.regex-in-loop",
152
+ # Also accept full rule names
153
+ "string-concat-loop": "performance.string-concat-loop",
154
+ "regex-in-loop": "performance.regex-in-loop",
155
+ }
156
+
157
+
158
+ def _filter_by_rule(violations: list[Violation], rule: str | None) -> list[Violation]:
159
+ """Filter violations by rule name if specified.
160
+
161
+ Args:
162
+ violations: List of violations to filter
163
+ rule: Optional rule name (string-concat, regex-loop, or full rule names)
164
+
165
+ Returns:
166
+ Filtered list of violations
167
+ """
168
+ if not rule:
169
+ return violations
170
+
171
+ rule_id = PERF_RULES.get(rule)
172
+ if not rule_id:
173
+ logger.warning(f"Unknown rule '{rule}'. Valid rules: {', '.join(PERF_RULES.keys())}")
174
+ return violations
175
+
176
+ return [v for v in violations if v.rule_id == rule_id]
177
+
178
+
179
+ def _run_all_perf_lint(
180
+ orchestrator: "Orchestrator",
181
+ path_objs: list[Path],
182
+ recursive: bool,
183
+ rule: str | None,
184
+ parallel: bool = False,
185
+ ) -> list[Violation]:
186
+ """Execute all performance lints on files or directories.
187
+
188
+ Args:
189
+ orchestrator: Configured orchestrator instance
190
+ path_objs: List of paths to analyze
191
+ recursive: Whether to scan directories recursively
192
+ rule: Optional rule filter (string-concat, regex-loop, or full rule names)
193
+ parallel: Whether to use parallel processing
194
+
195
+ Returns:
196
+ List of performance-related violations
197
+ """
198
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
199
+ perf_violations = [v for v in all_violations if v.rule_id.startswith("performance.")]
200
+ return _filter_by_rule(perf_violations, rule)
201
+
202
+
203
+ def _execute_perf_lint(params: ExecuteParams, rule: str | None) -> NoReturn:
204
+ """Execute combined performance lint."""
205
+ orchestrator = _setup_and_validate(params)
206
+ violations = _run_all_perf_lint(
207
+ orchestrator, params.path_objs, params.recursive, rule, params.parallel
208
+ )
209
+
210
+ if params.verbose:
211
+ logger.info(f"Found {len(violations)} performance violation(s)")
212
+
213
+ format_violations(violations, params.format)
214
+ sys.exit(1 if violations else 0)
215
+
216
+
217
+ @cli.command(
218
+ "perf",
219
+ help="""Check for performance anti-patterns in code.
220
+
221
+ Detects common performance issues in loops:
222
+ - string-concat-loop: O(n^2) string building using += in loops
223
+ - regex-in-loop: Regex recompilation on each loop iteration
224
+
225
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
226
+
227
+ Examples:
228
+
229
+ \\b
230
+ # Check current directory for all performance issues
231
+ thai-lint perf
232
+
233
+ \\b
234
+ # Check specific directory
235
+ thai-lint perf src/
236
+
237
+ \\b
238
+ # Check only string concatenation issues
239
+ thai-lint perf --rule string-concat src/
240
+
241
+ \\b
242
+ # Check only regex-in-loop issues
243
+ thai-lint perf --rule regex-loop src/
244
+
245
+ \\b
246
+ # Get JSON output
247
+ thai-lint perf --format json .
248
+
249
+ \\b
250
+ # Use custom config file
251
+ thai-lint perf --config .thailint.yaml src/
252
+ """,
253
+ )
254
+ @click.option(
255
+ "--rule",
256
+ "-r",
257
+ "rule",
258
+ type=click.Choice(["string-concat", "regex-loop"]),
259
+ help="Run only a specific performance rule",
260
+ )
261
+ @standard_linter_options
262
+ def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
263
+ ctx: click.Context,
264
+ paths: tuple[str, ...],
265
+ config_file: str | None,
266
+ format: str,
267
+ recursive: bool,
268
+ parallel: bool,
269
+ rule: str | None,
270
+ ) -> None:
271
+ """Run all performance linters.
272
+
273
+ Args:
274
+ ctx: Click context with global options
275
+ paths: Files or directories to lint
276
+ config_file: Optional path to config file
277
+ format: Output format (text, json, sarif)
278
+ recursive: Whether to scan directories recursively
279
+ parallel: Whether to use parallel processing
280
+ rule: Optional rule filter (string-concat or regex-loop)
281
+ """
282
+ params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
283
+
284
+ def execute_with_rule(p: ExecuteParams) -> None:
285
+ _execute_perf_lint(p, rule)
286
+
287
+ run_linter_command(execute_with_rule, params)
@@ -0,0 +1,331 @@
1
+ """
2
+ Purpose: Shared utilities for linter CLI commands
3
+
4
+ Scope: Common helper functions and patterns used across all linter command modules
5
+
6
+ Overview: Provides reusable utilities for linter CLI commands including config section management,
7
+ config value setting with logging, rule ID filtering, CLI context extraction, and help text
8
+ generation. Centralizes shared patterns to reduce duplication across linter command modules
9
+ (code_quality, code_patterns, structure, documentation, performance). All utilities are designed
10
+ to work with the orchestrator configuration system and Click CLI framework.
11
+
12
+ Dependencies: logging for debug output, pathlib for Path type hints, click for Context type,
13
+ dataclasses for CommandContext
14
+
15
+ Exports: ensure_config_section, set_config_value, filter_violations_by_prefix, CommandContext,
16
+ extract_command_context, make_linter_help, ExecuteParams, prepare_standard_command,
17
+ run_linter_command, standard_linter_options, filter_violations_by_startswith,
18
+ create_linter_command
19
+
20
+ Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI context extraction,
21
+ help text generation
22
+
23
+ Implementation: Pure helper functions with no side effects beyond config mutation and logging
24
+
25
+ Suppressions:
26
+ - too-many-arguments,too-many-positional-arguments: CLI helper functions require many parameters
27
+ by Click framework design (ctx, paths, config_file, format, recursive, parallel = 6 params)
28
+ """
29
+
30
+ import logging
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ import click
36
+
37
+ from src.cli.utils import (
38
+ format_option,
39
+ get_project_root_from_context,
40
+ handle_linting_error,
41
+ parallel_option,
42
+ )
43
+ from src.core.types import Violation
44
+
45
+ if TYPE_CHECKING:
46
+ from collections.abc import Callable
47
+
48
+ from src.orchestrator.core import Orchestrator
49
+
50
+
51
+ def standard_linter_options(f: Any) -> Any:
52
+ """Apply standard linter CLI options to a command.
53
+
54
+ Bundles the common options used by most linter commands:
55
+ - paths argument (variadic)
56
+ - config file option
57
+ - format option
58
+ - recursive option
59
+ - parallel option
60
+ - pass_context
61
+
62
+ Usage:
63
+ @cli.command("my-linter")
64
+ @standard_linter_options
65
+ def my_linter(ctx, paths, config_file, format, recursive, parallel):
66
+ ...
67
+ """
68
+ f = click.pass_context(f)
69
+ f = parallel_option(f)
70
+ f = click.option(
71
+ "--recursive/--no-recursive", default=True, help="Scan directories recursively"
72
+ )(f)
73
+ f = format_option(f)
74
+ f = click.option(
75
+ "--config", "-c", "config_file", type=click.Path(), help="Path to config file"
76
+ )(f)
77
+ f = click.argument("paths", nargs=-1, type=click.Path())(f)
78
+ return f
79
+
80
+
81
+ @dataclass
82
+ class CommandContext:
83
+ """Extracted context from CLI command invocation.
84
+
85
+ Consolidates common CLI command setup into a reusable structure.
86
+ """
87
+
88
+ verbose: bool
89
+ project_root: Path | None
90
+ path_objs: list[Path]
91
+
92
+
93
+ @dataclass
94
+ class ExecuteParams:
95
+ """Parameters for linter execution functions.
96
+
97
+ Bundles the common parameters passed to _execute_*_lint functions
98
+ to reduce function signature duplication across CLI modules.
99
+ """
100
+
101
+ path_objs: list[Path]
102
+ config_file: str | None
103
+ format: str
104
+ recursive: bool
105
+ verbose: bool
106
+ project_root: Path | None
107
+ parallel: bool = False
108
+
109
+
110
+ def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
111
+ """Extract common context values from CLI command invocation.
112
+
113
+ Consolidates the repeated pattern of extracting verbose, project_root,
114
+ and converting paths to Path objects with default handling.
115
+
116
+ Args:
117
+ ctx: Click context from command invocation
118
+ paths: Tuple of path strings from command arguments
119
+
120
+ Returns:
121
+ CommandContext with extracted values
122
+ """
123
+ verbose: bool = ctx.obj.get("verbose", False)
124
+ project_root = get_project_root_from_context(ctx)
125
+
126
+ if not paths:
127
+ paths = (".",)
128
+
129
+ path_objs = [Path(p) for p in paths]
130
+
131
+ return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
132
+
133
+
134
+ def prepare_standard_command( # pylint: disable=too-many-arguments,too-many-positional-arguments
135
+ ctx: click.Context,
136
+ paths: tuple[str, ...],
137
+ config_file: str | None,
138
+ format: str,
139
+ recursive: bool,
140
+ parallel: bool = False,
141
+ ) -> ExecuteParams:
142
+ """Prepare standard linter command execution parameters.
143
+
144
+ Combines context extraction and ExecuteParams creation into a single call.
145
+ Use with commands that have the standard options (config, format, recursive, parallel).
146
+
147
+ Args:
148
+ ctx: Click context from command invocation
149
+ paths: Tuple of path strings from command arguments
150
+ config_file: Optional config file path
151
+ format: Output format
152
+ recursive: Whether to scan recursively
153
+ parallel: Whether to use parallel processing
154
+
155
+ Returns:
156
+ ExecuteParams ready for _execute_*_lint function
157
+ """
158
+ cmd_ctx = extract_command_context(ctx, paths)
159
+ return ExecuteParams(
160
+ path_objs=cmd_ctx.path_objs,
161
+ config_file=config_file,
162
+ format=format,
163
+ recursive=recursive,
164
+ verbose=cmd_ctx.verbose,
165
+ project_root=cmd_ctx.project_root,
166
+ parallel=parallel,
167
+ )
168
+
169
+
170
+ def run_linter_command(
171
+ execute_fn: "Callable[[ExecuteParams], None]",
172
+ params: ExecuteParams,
173
+ ) -> None:
174
+ """Run a linter command with standard error handling.
175
+
176
+ Wraps the try/except pattern used by all linter commands.
177
+
178
+ Args:
179
+ execute_fn: The _execute_*_lint function to call
180
+ params: ExecuteParams for the execution
181
+ """
182
+ try:
183
+ execute_fn(params)
184
+ except Exception as e:
185
+ handle_linting_error(e, params.verbose)
186
+
187
+
188
+ # Configure module logger
189
+ logger = logging.getLogger(__name__)
190
+
191
+
192
+ def ensure_config_section(orchestrator: "Orchestrator", section: str) -> dict[str, Any]:
193
+ """Ensure a config section exists and return it.
194
+
195
+ Args:
196
+ orchestrator: Orchestrator instance with config dict
197
+ section: Name of the config section to ensure exists
198
+
199
+ Returns:
200
+ The config section dict (created if it didn't exist)
201
+ """
202
+ if section not in orchestrator.config:
203
+ orchestrator.config[section] = {}
204
+ config_section: dict[str, Any] = orchestrator.config[section]
205
+ return config_section
206
+
207
+
208
+ def set_config_value(config: dict[str, Any], key: str, value: Any, verbose: bool) -> None:
209
+ """Set a config value with optional debug logging.
210
+
211
+ Only sets the value if it is not None.
212
+
213
+ Args:
214
+ config: Config dict to update
215
+ key: Config key to set
216
+ value: Value to set (skipped if None)
217
+ verbose: Whether to log the override
218
+ """
219
+ if value is None:
220
+ return
221
+ config[key] = value
222
+ if verbose:
223
+ logger.debug(f"Overriding {key} to {value}")
224
+
225
+
226
+ def filter_violations_by_prefix(violations: list[Violation], prefix: str) -> list[Violation]:
227
+ """Filter violations to those matching a rule ID prefix.
228
+
229
+ Args:
230
+ violations: List of violation objects with rule_id attribute
231
+ prefix: Prefix to match against rule_id
232
+
233
+ Returns:
234
+ Filtered list of violations where rule_id contains the prefix
235
+ """
236
+ return [v for v in violations if prefix in v.rule_id]
237
+
238
+
239
+ def filter_violations_by_startswith(violations: list[Violation], prefix: str) -> list[Violation]:
240
+ """Filter violations to those with rule_id starting with prefix.
241
+
242
+ Args:
243
+ violations: List of violation objects with rule_id attribute
244
+ prefix: Prefix that rule_id must start with
245
+
246
+ Returns:
247
+ Filtered list of violations where rule_id starts with the prefix
248
+ """
249
+ return [v for v in violations if v.rule_id.startswith(prefix)]
250
+
251
+
252
+ def create_linter_command(
253
+ name: str,
254
+ execute_fn: "Callable[[ExecuteParams], None]",
255
+ brief: str,
256
+ description: str,
257
+ ) -> "Callable[..., None]":
258
+ """Create a standard linter CLI command.
259
+
260
+ Factory function that generates Click commands with consistent structure,
261
+ eliminating boilerplate duplication across linter command modules.
262
+
263
+ Args:
264
+ name: CLI command name (e.g., "magic-numbers")
265
+ execute_fn: The _execute_*_lint function to call
266
+ brief: Brief one-line description
267
+ description: Detailed multi-line description
268
+
269
+ Returns:
270
+ Decorated Click command function
271
+ """
272
+ from src.cli.main import cli
273
+
274
+ @cli.command(name, help=make_linter_help(name, brief, description))
275
+ @standard_linter_options
276
+ def command( # pylint: disable=too-many-arguments,too-many-positional-arguments
277
+ ctx: click.Context,
278
+ paths: tuple[str, ...],
279
+ config_file: str | None,
280
+ format: str,
281
+ recursive: bool,
282
+ parallel: bool,
283
+ ) -> None:
284
+ params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
285
+ run_linter_command(execute_fn, params)
286
+
287
+ return command
288
+
289
+
290
+ def make_linter_help(command: str, brief: str, description: str) -> str:
291
+ """Generate standardized CLI help text for linter commands.
292
+
293
+ Creates consistent help text following the established pattern with
294
+ examples showing common usage patterns.
295
+
296
+ Args:
297
+ command: CLI command name (e.g., "magic-numbers")
298
+ brief: Brief one-line description
299
+ description: Detailed multi-line description
300
+
301
+ Returns:
302
+ Formatted help text string for Click command
303
+ """
304
+ return f"""{brief}
305
+
306
+ {description}
307
+
308
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
309
+
310
+ Examples:
311
+
312
+ \\b
313
+ # Check current directory (all files recursively)
314
+ thai-lint {command}
315
+
316
+ \\b
317
+ # Check specific directory
318
+ thai-lint {command} src/
319
+
320
+ \\b
321
+ # Check single file
322
+ thai-lint {command} src/app.py
323
+
324
+ \\b
325
+ # Get JSON output
326
+ thai-lint {command} --format json .
327
+
328
+ \\b
329
+ # Use custom config file
330
+ thai-lint {command} --config .thailint.yaml src/
331
+ """