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,327 @@
1
+ """
2
+ Purpose: CLI commands for project structure linters (file-placement, pipeline)
3
+
4
+ Scope: File placement validation and collection pipeline anti-pattern detection commands
5
+
6
+ Overview: Provides CLI commands for project structure linting: file-placement checks that files are
7
+ in appropriate directories according to configured rules, and pipeline detects for loops with
8
+ embedded if/continue filtering that could use collection pipelines. Both commands support
9
+ standard options (config, format, recursive) and integrate with the orchestrator for execution.
10
+
11
+ Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
12
+ src.cli.linters.shared for linter-specific helpers
13
+
14
+ Exports: file_placement command, pipeline command
15
+
16
+ Interfaces: Click CLI commands registered to main CLI group
17
+
18
+ Implementation: Click decorators for command definition, orchestrator-based linting execution
19
+
20
+ SRP Exception: CLI command modules follow Click framework patterns requiring similar command
21
+ structure across all linter commands. This is intentional design for consistency.
22
+
23
+ Suppressions:
24
+ - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
25
+ """
26
+
27
+ import json
28
+ import logging
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, Any, NoReturn
32
+
33
+ import click
34
+
35
+ from src.cli.linters.shared import (
36
+ ensure_config_section,
37
+ extract_command_context,
38
+ set_config_value,
39
+ )
40
+ from src.cli.main import cli
41
+ from src.cli.utils import (
42
+ execute_linting_on_paths,
43
+ format_option,
44
+ get_or_detect_project_root,
45
+ handle_linting_error,
46
+ load_config_file,
47
+ parallel_option,
48
+ setup_base_orchestrator,
49
+ validate_paths_exist,
50
+ )
51
+ from src.core.cli_utils import format_violations
52
+ from src.core.types import Violation
53
+
54
+ if TYPE_CHECKING:
55
+ from src.orchestrator.core import Orchestrator
56
+
57
+ # Configure module logger
58
+ logger = logging.getLogger(__name__)
59
+
60
+
61
+ # =============================================================================
62
+ # File Placement Command
63
+ # =============================================================================
64
+
65
+
66
+ def _setup_orchestrator(
67
+ path_objs: list[Path],
68
+ config_file: str | None,
69
+ rules: str | None,
70
+ verbose: bool,
71
+ project_root: Path | None = None,
72
+ ) -> "Orchestrator":
73
+ """Set up and configure the orchestrator for file-placement."""
74
+ from src.orchestrator.core import Orchestrator
75
+
76
+ project_root = get_or_detect_project_root(path_objs, project_root)
77
+ orchestrator = Orchestrator(project_root=project_root)
78
+ _apply_orchestrator_config(orchestrator, config_file, rules, verbose)
79
+ return orchestrator
80
+
81
+
82
+ def _apply_orchestrator_config(
83
+ orchestrator: "Orchestrator", config_file: str | None, rules: str | None, verbose: bool
84
+ ) -> None:
85
+ """Apply configuration to orchestrator."""
86
+ if rules:
87
+ _apply_inline_rules(orchestrator, rules, verbose)
88
+ elif config_file:
89
+ load_config_file(orchestrator, config_file, verbose)
90
+
91
+
92
+ def _apply_inline_rules(orchestrator: "Orchestrator", rules: str, verbose: bool) -> None:
93
+ """Parse and apply inline JSON rules."""
94
+ rules_config = _parse_json_rules(rules)
95
+ orchestrator.config.update(rules_config)
96
+ if verbose:
97
+ logger.debug(f"Applied inline rules: {rules_config}")
98
+
99
+
100
+ def _parse_json_rules(rules: str) -> dict[str, Any]:
101
+ """Parse JSON rules string, exit on error."""
102
+ try:
103
+ result: dict[str, Any] = json.loads(rules)
104
+ return result
105
+ except json.JSONDecodeError as e:
106
+ click.echo(f"Error: Invalid JSON in --rules: {e}", err=True)
107
+ sys.exit(2)
108
+
109
+
110
+ @cli.command("file-placement")
111
+ @click.argument("paths", nargs=-1, type=click.Path())
112
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
113
+ @click.option("--rules", "-r", help="Inline JSON rules configuration")
114
+ @format_option
115
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
116
+ @parallel_option
117
+ @click.pass_context
118
+ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments
119
+ ctx: click.Context,
120
+ paths: tuple[str, ...],
121
+ config_file: str | None,
122
+ rules: str | None,
123
+ format: str,
124
+ recursive: bool,
125
+ parallel: bool,
126
+ ) -> None:
127
+ # Justification for Pylint disables:
128
+ # - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 4 options = 6 params
129
+ """
130
+ Lint files for proper file placement.
131
+
132
+ Checks that files are placed in appropriate directories according to
133
+ configured rules and patterns.
134
+
135
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
136
+
137
+ Examples:
138
+
139
+ \b
140
+ # Lint current directory (all files recursively)
141
+ thai-lint file-placement
142
+
143
+ \b
144
+ # Lint specific directory
145
+ thai-lint file-placement src/
146
+
147
+ \b
148
+ # Lint single file
149
+ thai-lint file-placement src/app.py
150
+
151
+ \b
152
+ # Lint multiple files
153
+ thai-lint file-placement src/app.py src/utils.py tests/test_app.py
154
+
155
+ \b
156
+ # Use custom config
157
+ thai-lint file-placement --config rules.json .
158
+
159
+ \b
160
+ # Inline JSON rules
161
+ thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
162
+ """
163
+ cmd_ctx = extract_command_context(ctx, paths)
164
+
165
+ try:
166
+ _execute_file_placement_lint(
167
+ cmd_ctx.path_objs,
168
+ config_file,
169
+ rules,
170
+ format,
171
+ recursive,
172
+ parallel,
173
+ cmd_ctx.verbose,
174
+ cmd_ctx.project_root,
175
+ )
176
+ except Exception as e:
177
+ handle_linting_error(e, cmd_ctx.verbose)
178
+
179
+
180
+ def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
181
+ path_objs: list[Path],
182
+ config_file: str | None,
183
+ rules: str | None,
184
+ format: str,
185
+ recursive: bool,
186
+ parallel: bool,
187
+ verbose: bool,
188
+ project_root: Path | None = None,
189
+ ) -> NoReturn:
190
+ """Execute file placement linting."""
191
+ validate_paths_exist(path_objs)
192
+ orchestrator = _setup_orchestrator(path_objs, config_file, rules, verbose, project_root)
193
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
194
+
195
+ # Filter to only file-placement violations
196
+ violations = [v for v in all_violations if v.rule_id.startswith("file-placement")]
197
+
198
+ if verbose:
199
+ logger.info(f"Found {len(violations)} violation(s)")
200
+
201
+ format_violations(violations, format)
202
+ sys.exit(1 if violations else 0)
203
+
204
+
205
+ # =============================================================================
206
+ # Collection Pipeline Command
207
+ # =============================================================================
208
+
209
+
210
+ def _setup_pipeline_orchestrator(
211
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
212
+ ) -> "Orchestrator":
213
+ """Set up orchestrator for pipeline command."""
214
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
215
+
216
+
217
+ def _apply_pipeline_config_override(
218
+ orchestrator: "Orchestrator", min_continues: int | None, verbose: bool
219
+ ) -> None:
220
+ """Apply min_continues override to orchestrator config."""
221
+ if min_continues is None:
222
+ return
223
+
224
+ pipeline_config = ensure_config_section(orchestrator, "collection_pipeline")
225
+ set_config_value(pipeline_config, "min_continues", min_continues, verbose)
226
+
227
+
228
+ def _run_pipeline_lint(
229
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
230
+ ) -> list[Violation]:
231
+ """Execute collection-pipeline lint on files or directories."""
232
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
233
+ return [v for v in all_violations if "collection-pipeline" in v.rule_id]
234
+
235
+
236
+ @cli.command("pipeline")
237
+ @click.argument("paths", nargs=-1, type=click.Path())
238
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
239
+ @format_option
240
+ @click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
241
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
242
+ @parallel_option
243
+ @click.pass_context
244
+ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
245
+ ctx: click.Context,
246
+ paths: tuple[str, ...],
247
+ config_file: str | None,
248
+ format: str,
249
+ min_continues: int | None,
250
+ recursive: bool,
251
+ parallel: bool,
252
+ ) -> None:
253
+ """Check for collection pipeline anti-patterns in code.
254
+
255
+ Detects for loops with embedded if/continue filtering patterns that could
256
+ be refactored to use collection pipelines (generator expressions, filter()).
257
+
258
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
259
+
260
+ Examples:
261
+
262
+ \b
263
+ # Check current directory (all Python files recursively)
264
+ thai-lint pipeline
265
+
266
+ \b
267
+ # Check specific directory
268
+ thai-lint pipeline src/
269
+
270
+ \b
271
+ # Check single file
272
+ thai-lint pipeline src/app.py
273
+
274
+ \b
275
+ # Only flag loops with 2+ continue guards
276
+ thai-lint pipeline --min-continues 2 src/
277
+
278
+ \b
279
+ # Get JSON output
280
+ thai-lint pipeline --format json .
281
+
282
+ \b
283
+ # Get SARIF output for CI/CD integration
284
+ thai-lint pipeline --format sarif src/
285
+
286
+ \b
287
+ # Use custom config file
288
+ thai-lint pipeline --config .thailint.yaml src/
289
+ """
290
+ cmd_ctx = extract_command_context(ctx, paths)
291
+
292
+ try:
293
+ _execute_pipeline_lint(
294
+ cmd_ctx.path_objs,
295
+ config_file,
296
+ format,
297
+ min_continues,
298
+ recursive,
299
+ parallel,
300
+ cmd_ctx.verbose,
301
+ cmd_ctx.project_root,
302
+ )
303
+ except Exception as e:
304
+ handle_linting_error(e, cmd_ctx.verbose)
305
+
306
+
307
+ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
308
+ path_objs: list[Path],
309
+ config_file: str | None,
310
+ format: str,
311
+ min_continues: int | None,
312
+ recursive: bool,
313
+ parallel: bool,
314
+ verbose: bool,
315
+ project_root: Path | None = None,
316
+ ) -> NoReturn:
317
+ """Execute collection-pipeline lint."""
318
+ validate_paths_exist(path_objs)
319
+ orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
320
+ _apply_pipeline_config_override(orchestrator, min_continues, verbose)
321
+ pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive, parallel)
322
+
323
+ if verbose:
324
+ logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
325
+
326
+ format_violations(pipeline_violations, format)
327
+ sys.exit(1 if pipeline_violations else 0)
@@ -0,0 +1,328 @@
1
+ """
2
+ Purpose: CLI commands for structure quality linters (nesting, srp)
3
+
4
+ Scope: Commands that analyze code structure for quality issues
5
+
6
+ Overview: Provides CLI commands for structure quality linting: nesting checks for excessive nesting
7
+ depth in control flow statements, and srp detects Single Responsibility Principle violations in
8
+ classes. Each command supports standard options (config, format, recursive) plus linter-specific
9
+ options (max-depth, max-methods, max-loc) and integrates with the orchestrator for execution.
10
+
11
+ Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
12
+ src.cli.linters.shared for linter-specific helpers
13
+
14
+ Exports: nesting command, srp command
15
+
16
+ Interfaces: Click CLI commands registered to main CLI group
17
+
18
+ Implementation: Click decorators for command definition, orchestrator-based linting execution
19
+
20
+ SRP Exception: CLI command modules follow Click framework patterns requiring similar command
21
+ structure across all linter commands. This is intentional design for consistency.
22
+
23
+ Suppressions:
24
+ - too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
25
+ """
26
+
27
+ import logging
28
+ import sys
29
+ from contextlib import suppress
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, NoReturn
32
+
33
+ import click
34
+
35
+ from src.cli.linters.shared import (
36
+ ensure_config_section,
37
+ extract_command_context,
38
+ set_config_value,
39
+ )
40
+ from src.cli.main import cli
41
+ from src.cli.utils import (
42
+ execute_linting_on_paths,
43
+ format_option,
44
+ handle_linting_error,
45
+ parallel_option,
46
+ setup_base_orchestrator,
47
+ validate_paths_exist,
48
+ )
49
+ from src.core.cli_utils import format_violations
50
+ from src.core.types import Violation
51
+
52
+ if TYPE_CHECKING:
53
+ from src.orchestrator.core import Orchestrator
54
+
55
+ # Configure module logger
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ # =============================================================================
60
+ # Nesting Command
61
+ # =============================================================================
62
+
63
+
64
+ def _setup_nesting_orchestrator(
65
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
66
+ ) -> "Orchestrator":
67
+ """Set up orchestrator for nesting command."""
68
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
69
+
70
+
71
+ def _apply_nesting_config_override(
72
+ orchestrator: "Orchestrator", max_depth: int | None, verbose: bool
73
+ ) -> None:
74
+ """Apply max_depth override to orchestrator config."""
75
+ if max_depth is None:
76
+ return
77
+
78
+ nesting_config = ensure_config_section(orchestrator, "nesting")
79
+ nesting_config["max_nesting_depth"] = max_depth
80
+ _apply_nesting_to_languages(nesting_config, max_depth)
81
+
82
+ if verbose:
83
+ logger.debug(f"Overriding max_nesting_depth to {max_depth}")
84
+
85
+
86
+ def _apply_nesting_to_languages(nesting_config: dict, max_depth: int) -> None:
87
+ """Apply max_depth to language-specific configs."""
88
+ for lang in ["python", "typescript", "javascript"]:
89
+ with suppress(KeyError):
90
+ nesting_config[lang]["max_nesting_depth"] = max_depth
91
+
92
+
93
+ def _run_nesting_lint(
94
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
95
+ ) -> list[Violation]:
96
+ """Execute nesting lint on files or directories."""
97
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
98
+ return [v for v in all_violations if "nesting" in v.rule_id]
99
+
100
+
101
+ @cli.command("nesting")
102
+ @click.argument("paths", nargs=-1, type=click.Path())
103
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
104
+ @format_option
105
+ @click.option("--max-depth", type=int, help="Override max nesting depth (default: 4)")
106
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
107
+ @parallel_option
108
+ @click.pass_context
109
+ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
110
+ ctx: click.Context,
111
+ paths: tuple[str, ...],
112
+ config_file: str | None,
113
+ format: str,
114
+ max_depth: int | None,
115
+ recursive: bool,
116
+ parallel: bool,
117
+ ) -> None:
118
+ """Check for excessive nesting depth in code.
119
+
120
+ Analyzes Python and TypeScript files for deeply nested code structures
121
+ (if/for/while/try statements) and reports violations.
122
+
123
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
124
+
125
+ Examples:
126
+
127
+ \b
128
+ # Check current directory (all files recursively)
129
+ thai-lint nesting
130
+
131
+ \b
132
+ # Check specific directory
133
+ thai-lint nesting src/
134
+
135
+ \b
136
+ # Check single file
137
+ thai-lint nesting src/app.py
138
+
139
+ \b
140
+ # Check multiple files
141
+ thai-lint nesting src/app.py src/utils.py tests/test_app.py
142
+
143
+ \b
144
+ # Check mix of files and directories
145
+ thai-lint nesting src/app.py tests/
146
+
147
+ \b
148
+ # Use custom max depth
149
+ thai-lint nesting --max-depth 3 src/
150
+
151
+ \b
152
+ # Get JSON output
153
+ thai-lint nesting --format json .
154
+
155
+ \b
156
+ # Use custom config file
157
+ thai-lint nesting --config .thailint.yaml src/
158
+ """
159
+ cmd_ctx = extract_command_context(ctx, paths)
160
+
161
+ try:
162
+ _execute_nesting_lint(
163
+ cmd_ctx.path_objs,
164
+ config_file,
165
+ format,
166
+ max_depth,
167
+ recursive,
168
+ parallel,
169
+ cmd_ctx.verbose,
170
+ cmd_ctx.project_root,
171
+ )
172
+ except Exception as e:
173
+ handle_linting_error(e, cmd_ctx.verbose)
174
+
175
+
176
+ def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
177
+ path_objs: list[Path],
178
+ config_file: str | None,
179
+ format: str,
180
+ max_depth: int | None,
181
+ recursive: bool,
182
+ parallel: bool,
183
+ verbose: bool,
184
+ project_root: Path | None = None,
185
+ ) -> NoReturn:
186
+ """Execute nesting lint."""
187
+ validate_paths_exist(path_objs)
188
+ orchestrator = _setup_nesting_orchestrator(path_objs, config_file, verbose, project_root)
189
+ _apply_nesting_config_override(orchestrator, max_depth, verbose)
190
+ nesting_violations = _run_nesting_lint(orchestrator, path_objs, recursive, parallel)
191
+
192
+ if verbose:
193
+ logger.info(f"Found {len(nesting_violations)} nesting violation(s)")
194
+
195
+ format_violations(nesting_violations, format)
196
+ sys.exit(1 if nesting_violations else 0)
197
+
198
+
199
+ # =============================================================================
200
+ # SRP Command
201
+ # =============================================================================
202
+
203
+
204
+ def _setup_srp_orchestrator(
205
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
206
+ ) -> "Orchestrator":
207
+ """Set up orchestrator for SRP command."""
208
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
209
+
210
+
211
+ def _apply_srp_config_override(
212
+ orchestrator: "Orchestrator", max_methods: int | None, max_loc: int | None, verbose: bool
213
+ ) -> None:
214
+ """Apply max_methods and max_loc overrides to orchestrator config."""
215
+ if max_methods is None and max_loc is None:
216
+ return
217
+
218
+ srp_config = ensure_config_section(orchestrator, "srp")
219
+ set_config_value(srp_config, "max_methods", max_methods, verbose)
220
+ set_config_value(srp_config, "max_loc", max_loc, verbose)
221
+
222
+
223
+ def _run_srp_lint(
224
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
225
+ ) -> list[Violation]:
226
+ """Execute SRP lint on files or directories."""
227
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
228
+ return [v for v in all_violations if "srp" in v.rule_id]
229
+
230
+
231
+ @cli.command("srp")
232
+ @click.argument("paths", nargs=-1, type=click.Path())
233
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
234
+ @format_option
235
+ @click.option("--max-methods", type=int, help="Override max methods per class (default: 7)")
236
+ @click.option("--max-loc", type=int, help="Override max lines of code per class (default: 200)")
237
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
238
+ @parallel_option
239
+ @click.pass_context
240
+ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
241
+ ctx: click.Context,
242
+ paths: tuple[str, ...],
243
+ config_file: str | None,
244
+ format: str,
245
+ max_methods: int | None,
246
+ max_loc: int | None,
247
+ recursive: bool,
248
+ parallel: bool,
249
+ ) -> None:
250
+ """Check for Single Responsibility Principle violations.
251
+
252
+ Analyzes Python and TypeScript classes for SRP violations using heuristics:
253
+ - Method count exceeding threshold (default: 7)
254
+ - Lines of code exceeding threshold (default: 200)
255
+ - Responsibility keywords in class names (Manager, Handler, Processor, etc.)
256
+
257
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
258
+
259
+ Examples:
260
+
261
+ \b
262
+ # Check current directory (all files recursively)
263
+ thai-lint srp
264
+
265
+ \b
266
+ # Check specific directory
267
+ thai-lint srp src/
268
+
269
+ \b
270
+ # Check single file
271
+ thai-lint srp src/app.py
272
+
273
+ \b
274
+ # Check multiple files
275
+ thai-lint srp src/app.py src/service.py tests/test_app.py
276
+
277
+ \b
278
+ # Use custom thresholds
279
+ thai-lint srp --max-methods 10 --max-loc 300 src/
280
+
281
+ \b
282
+ # Get JSON output
283
+ thai-lint srp --format json .
284
+
285
+ \b
286
+ # Use custom config file
287
+ thai-lint srp --config .thailint.yaml src/
288
+ """
289
+ cmd_ctx = extract_command_context(ctx, paths)
290
+
291
+ try:
292
+ _execute_srp_lint(
293
+ cmd_ctx.path_objs,
294
+ config_file,
295
+ format,
296
+ max_methods,
297
+ max_loc,
298
+ recursive,
299
+ parallel,
300
+ cmd_ctx.verbose,
301
+ cmd_ctx.project_root,
302
+ )
303
+ except Exception as e:
304
+ handle_linting_error(e, cmd_ctx.verbose)
305
+
306
+
307
+ def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
308
+ path_objs: list[Path],
309
+ config_file: str | None,
310
+ format: str,
311
+ max_methods: int | None,
312
+ max_loc: int | None,
313
+ recursive: bool,
314
+ parallel: bool,
315
+ verbose: bool,
316
+ project_root: Path | None = None,
317
+ ) -> NoReturn:
318
+ """Execute SRP lint."""
319
+ validate_paths_exist(path_objs)
320
+ orchestrator = _setup_srp_orchestrator(path_objs, config_file, verbose, project_root)
321
+ _apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
322
+ srp_violations = _run_srp_lint(orchestrator, path_objs, recursive, parallel)
323
+
324
+ if verbose:
325
+ logger.info(f"Found {len(srp_violations)} SRP violation(s)")
326
+
327
+ format_violations(srp_violations, format)
328
+ sys.exit(1 if srp_violations else 0)