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