thailint 0.12.0__py3-none-any.whl → 0.14.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 (135) hide show
  1. src/analyzers/__init__.py +4 -3
  2. src/analyzers/ast_utils.py +54 -0
  3. src/analyzers/typescript_base.py +4 -0
  4. src/cli/__init__.py +3 -0
  5. src/cli/config.py +12 -12
  6. src/cli/config_merge.py +241 -0
  7. src/cli/linters/__init__.py +9 -0
  8. src/cli/linters/code_patterns.py +107 -257
  9. src/cli/linters/code_smells.py +48 -165
  10. src/cli/linters/documentation.py +21 -95
  11. src/cli/linters/performance.py +274 -0
  12. src/cli/linters/shared.py +232 -6
  13. src/cli/linters/structure.py +26 -21
  14. src/cli/linters/structure_quality.py +28 -21
  15. src/cli_main.py +3 -0
  16. src/config.py +2 -1
  17. src/core/base.py +3 -2
  18. src/core/cli_utils.py +3 -1
  19. src/core/config_parser.py +5 -2
  20. src/core/constants.py +54 -0
  21. src/core/linter_utils.py +95 -6
  22. src/core/rule_discovery.py +5 -1
  23. src/core/violation_builder.py +3 -0
  24. src/linter_config/directive_markers.py +109 -0
  25. src/linter_config/ignore.py +225 -383
  26. src/linter_config/pattern_utils.py +65 -0
  27. src/linter_config/rule_matcher.py +89 -0
  28. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  29. src/linters/collection_pipeline/ast_utils.py +40 -0
  30. src/linters/collection_pipeline/config.py +12 -0
  31. src/linters/collection_pipeline/continue_analyzer.py +2 -8
  32. src/linters/collection_pipeline/detector.py +262 -32
  33. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  34. src/linters/collection_pipeline/linter.py +18 -35
  35. src/linters/collection_pipeline/suggestion_builder.py +68 -1
  36. src/linters/dry/base_token_analyzer.py +16 -9
  37. src/linters/dry/block_filter.py +7 -4
  38. src/linters/dry/cache.py +7 -2
  39. src/linters/dry/config.py +7 -1
  40. src/linters/dry/constant_matcher.py +34 -25
  41. src/linters/dry/file_analyzer.py +4 -2
  42. src/linters/dry/inline_ignore.py +7 -16
  43. src/linters/dry/linter.py +48 -25
  44. src/linters/dry/python_analyzer.py +18 -10
  45. src/linters/dry/python_constant_extractor.py +51 -52
  46. src/linters/dry/single_statement_detector.py +14 -12
  47. src/linters/dry/token_hasher.py +115 -115
  48. src/linters/dry/typescript_analyzer.py +11 -6
  49. src/linters/dry/typescript_constant_extractor.py +4 -0
  50. src/linters/dry/typescript_statement_detector.py +208 -208
  51. src/linters/dry/typescript_value_extractor.py +3 -0
  52. src/linters/dry/violation_filter.py +1 -4
  53. src/linters/dry/violation_generator.py +1 -4
  54. src/linters/file_header/atemporal_detector.py +58 -40
  55. src/linters/file_header/base_parser.py +4 -0
  56. src/linters/file_header/bash_parser.py +4 -0
  57. src/linters/file_header/config.py +14 -0
  58. src/linters/file_header/field_validator.py +5 -8
  59. src/linters/file_header/linter.py +19 -12
  60. src/linters/file_header/markdown_parser.py +6 -0
  61. src/linters/file_placement/config_loader.py +3 -1
  62. src/linters/file_placement/linter.py +22 -8
  63. src/linters/file_placement/pattern_matcher.py +21 -4
  64. src/linters/file_placement/pattern_validator.py +21 -7
  65. src/linters/file_placement/rule_checker.py +2 -2
  66. src/linters/lazy_ignores/__init__.py +43 -0
  67. src/linters/lazy_ignores/config.py +66 -0
  68. src/linters/lazy_ignores/directive_utils.py +121 -0
  69. src/linters/lazy_ignores/header_parser.py +177 -0
  70. src/linters/lazy_ignores/linter.py +158 -0
  71. src/linters/lazy_ignores/matcher.py +135 -0
  72. src/linters/lazy_ignores/python_analyzer.py +205 -0
  73. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  74. src/linters/lazy_ignores/skip_detector.py +298 -0
  75. src/linters/lazy_ignores/types.py +69 -0
  76. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  77. src/linters/lazy_ignores/violation_builder.py +131 -0
  78. src/linters/lbyl/__init__.py +29 -0
  79. src/linters/lbyl/config.py +63 -0
  80. src/linters/lbyl/pattern_detectors/__init__.py +25 -0
  81. src/linters/lbyl/pattern_detectors/base.py +46 -0
  82. src/linters/magic_numbers/context_analyzer.py +227 -229
  83. src/linters/magic_numbers/linter.py +20 -15
  84. src/linters/magic_numbers/python_analyzer.py +4 -16
  85. src/linters/magic_numbers/typescript_analyzer.py +9 -16
  86. src/linters/method_property/config.py +4 -1
  87. src/linters/method_property/linter.py +5 -10
  88. src/linters/method_property/python_analyzer.py +5 -4
  89. src/linters/method_property/violation_builder.py +3 -0
  90. src/linters/nesting/linter.py +11 -6
  91. src/linters/nesting/typescript_analyzer.py +6 -12
  92. src/linters/nesting/typescript_function_extractor.py +0 -4
  93. src/linters/nesting/violation_builder.py +1 -0
  94. src/linters/performance/__init__.py +91 -0
  95. src/linters/performance/config.py +43 -0
  96. src/linters/performance/constants.py +49 -0
  97. src/linters/performance/linter.py +149 -0
  98. src/linters/performance/python_analyzer.py +365 -0
  99. src/linters/performance/regex_analyzer.py +312 -0
  100. src/linters/performance/regex_linter.py +139 -0
  101. src/linters/performance/typescript_analyzer.py +236 -0
  102. src/linters/performance/violation_builder.py +160 -0
  103. src/linters/print_statements/linter.py +6 -4
  104. src/linters/print_statements/python_analyzer.py +85 -81
  105. src/linters/print_statements/typescript_analyzer.py +6 -15
  106. src/linters/srp/heuristics.py +4 -4
  107. src/linters/srp/linter.py +12 -12
  108. src/linters/srp/violation_builder.py +0 -4
  109. src/linters/stateless_class/linter.py +30 -36
  110. src/linters/stateless_class/python_analyzer.py +11 -20
  111. src/linters/stringly_typed/config.py +4 -5
  112. src/linters/stringly_typed/context_filter.py +410 -410
  113. src/linters/stringly_typed/function_call_violation_builder.py +93 -95
  114. src/linters/stringly_typed/linter.py +48 -16
  115. src/linters/stringly_typed/python/analyzer.py +5 -1
  116. src/linters/stringly_typed/python/call_tracker.py +8 -5
  117. src/linters/stringly_typed/python/comparison_tracker.py +10 -5
  118. src/linters/stringly_typed/python/condition_extractor.py +3 -0
  119. src/linters/stringly_typed/python/conditional_detector.py +4 -1
  120. src/linters/stringly_typed/python/match_analyzer.py +8 -2
  121. src/linters/stringly_typed/python/validation_detector.py +3 -0
  122. src/linters/stringly_typed/storage.py +14 -14
  123. src/linters/stringly_typed/typescript/call_tracker.py +9 -3
  124. src/linters/stringly_typed/typescript/comparison_tracker.py +9 -3
  125. src/linters/stringly_typed/violation_generator.py +288 -259
  126. src/orchestrator/core.py +13 -4
  127. src/templates/thailint_config_template.yaml +196 -0
  128. src/utils/project_root.py +3 -0
  129. thailint-0.14.0.dist-info/METADATA +185 -0
  130. thailint-0.14.0.dist-info/RECORD +199 -0
  131. thailint-0.12.0.dist-info/METADATA +0 -1667
  132. thailint-0.12.0.dist-info/RECORD +0 -164
  133. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  134. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  135. {thailint-0.12.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,28 +16,15 @@ Exports: file_header command
16
16
  Interfaces: Click CLI commands registered to main CLI group
17
17
 
18
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
19
  """
23
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
20
 
25
21
  import logging
26
22
  import sys
27
23
  from pathlib import Path
28
24
  from typing import TYPE_CHECKING, NoReturn
29
25
 
30
- import click
31
-
32
- from src.cli.main import cli
33
- from src.cli.utils import (
34
- execute_linting_on_paths,
35
- format_option,
36
- get_project_root_from_context,
37
- handle_linting_error,
38
- setup_base_orchestrator,
39
- validate_paths_exist,
40
- )
26
+ from src.cli.linters.shared import ExecuteParams, create_linter_command
27
+ from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
41
28
  from src.core.cli_utils import format_violations
42
29
  from src.core.types import Violation
43
30
 
@@ -68,88 +55,27 @@ def _run_file_header_lint(
68
55
  return [v for v in all_violations if "file-header" in v.rule_id]
69
56
 
70
57
 
71
- @cli.command("file-header")
72
- @click.argument("paths", nargs=-1, type=click.Path())
73
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
74
- @format_option
75
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
76
- @click.pass_context
77
- def file_header(
78
- ctx: click.Context,
79
- paths: tuple[str, ...],
80
- config_file: str | None,
81
- format: str,
82
- recursive: bool,
83
- ) -> None:
84
- """Check file headers for mandatory fields and atemporal language.
85
-
86
- Validates that source files have proper documentation headers containing
87
- required fields (Purpose, Scope, Overview, etc.) and don't use temporal
88
- language (dates, "currently", "now", etc.).
89
-
90
- Supports Python, TypeScript, JavaScript, Bash, Markdown, and CSS files.
91
-
92
- PATHS: Files or directories to lint (defaults to current directory if none provided)
93
-
94
- Examples:
95
-
96
- \b
97
- # Check current directory (all files recursively)
98
- thai-lint file-header
99
-
100
- \b
101
- # Check specific directory
102
- thai-lint file-header src/
103
-
104
- \b
105
- # Check single file
106
- thai-lint file-header src/cli.py
107
-
108
- \b
109
- # Check multiple files
110
- thai-lint file-header src/cli.py src/api.py tests/
111
-
112
- \b
113
- # Get JSON output
114
- thai-lint file-header --format json .
115
-
116
- \b
117
- # Get SARIF output for CI/CD integration
118
- thai-lint file-header --format sarif src/
119
-
120
- \b
121
- # Use custom config file
122
- thai-lint file-header --config .thailint.yaml src/
123
- """
124
- verbose: bool = ctx.obj.get("verbose", False)
125
- project_root = get_project_root_from_context(ctx)
126
-
127
- if not paths:
128
- paths = (".",)
129
-
130
- path_objs = [Path(p) for p in paths]
131
-
132
- try:
133
- _execute_file_header_lint(path_objs, config_file, format, recursive, verbose, project_root)
134
- except Exception as e:
135
- handle_linting_error(e, verbose)
136
-
137
-
138
- def _execute_file_header_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
139
- path_objs: list[Path],
140
- config_file: str | None,
141
- format: str,
142
- recursive: bool,
143
- verbose: bool,
144
- project_root: Path | None = None,
145
- ) -> NoReturn:
58
+ def _execute_file_header_lint(params: ExecuteParams) -> NoReturn:
146
59
  """Execute file-header lint."""
147
- validate_paths_exist(path_objs)
148
- orchestrator = _setup_file_header_orchestrator(path_objs, config_file, verbose, project_root)
149
- file_header_violations = _run_file_header_lint(orchestrator, path_objs, recursive)
60
+ validate_paths_exist(params.path_objs)
61
+ orchestrator = _setup_file_header_orchestrator(
62
+ params.path_objs, params.config_file, params.verbose, params.project_root
63
+ )
64
+ file_header_violations = _run_file_header_lint(orchestrator, params.path_objs, params.recursive)
150
65
 
151
- if verbose:
66
+ if params.verbose:
152
67
  logger.info(f"Found {len(file_header_violations)} file header violation(s)")
153
68
 
154
- format_violations(file_header_violations, format)
69
+ format_violations(file_header_violations, params.format)
155
70
  sys.exit(1 if file_header_violations else 0)
71
+
72
+
73
+ file_header = create_linter_command(
74
+ "file-header",
75
+ _execute_file_header_lint,
76
+ "Check file headers for mandatory fields and atemporal language.",
77
+ "Validates that source files have proper documentation headers containing\n"
78
+ " required fields (Purpose, Scope, Overview, etc.) and don't use temporal\n"
79
+ " language (dates, 'currently', 'now', etc.). Supports Python, TypeScript,\n"
80
+ " JavaScript, Bash, Markdown, and CSS files.",
81
+ )
@@ -0,0 +1,274 @@
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
78
+ ) -> list[Violation]:
79
+ """Execute string-concat-loop lint on files or directories."""
80
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
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(orchestrator, params.path_objs, params.recursive)
88
+
89
+ if params.verbose:
90
+ logger.info(f"Found {len(violations)} string-concat-loop violation(s)")
91
+
92
+ format_violations(violations, params.format)
93
+ sys.exit(1 if violations else 0)
94
+
95
+
96
+ string_concat_loop = create_linter_command(
97
+ "string-concat-loop",
98
+ _execute_string_concat_lint,
99
+ "Check for string concatenation in loops.",
100
+ "Detects O(n^2) string building patterns using += in for/while loops.\n"
101
+ " This is a common performance anti-pattern in Python and TypeScript.",
102
+ )
103
+
104
+
105
+ # =============================================================================
106
+ # Regex In Loop Command
107
+ # =============================================================================
108
+
109
+
110
+ def _run_regex_in_loop_lint(
111
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
112
+ ) -> list[Violation]:
113
+ """Execute regex-in-loop lint on files or directories."""
114
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
115
+ return [v for v in all_violations if v.rule_id == "performance.regex-in-loop"]
116
+
117
+
118
+ def _execute_regex_in_loop_lint(params: ExecuteParams) -> NoReturn:
119
+ """Execute regex-in-loop lint."""
120
+ orchestrator = _setup_and_validate(params)
121
+ violations = _run_regex_in_loop_lint(orchestrator, params.path_objs, params.recursive)
122
+
123
+ if params.verbose:
124
+ logger.info(f"Found {len(violations)} regex-in-loop violation(s)")
125
+
126
+ format_violations(violations, params.format)
127
+ sys.exit(1 if violations else 0)
128
+
129
+
130
+ regex_in_loop = create_linter_command(
131
+ "regex-in-loop",
132
+ _execute_regex_in_loop_lint,
133
+ "Check for regex compilation in loops.",
134
+ "Detects re.match(), re.search(), re.sub(), re.findall(), re.split(), and\n"
135
+ " re.fullmatch() calls inside loops. These recompile the regex pattern on\n"
136
+ " each iteration instead of compiling once with re.compile().",
137
+ )
138
+
139
+
140
+ # =============================================================================
141
+ # Combined Perf Command
142
+ # =============================================================================
143
+
144
+ # Valid rule names for the --rule option
145
+ PERF_RULES = {
146
+ "string-concat": "performance.string-concat-loop",
147
+ "regex-loop": "performance.regex-in-loop",
148
+ # Also accept full rule names
149
+ "string-concat-loop": "performance.string-concat-loop",
150
+ "regex-in-loop": "performance.regex-in-loop",
151
+ }
152
+
153
+
154
+ def _filter_by_rule(violations: list[Violation], rule: str | None) -> list[Violation]:
155
+ """Filter violations by rule name if specified.
156
+
157
+ Args:
158
+ violations: List of violations to filter
159
+ rule: Optional rule name (string-concat, regex-loop, or full rule names)
160
+
161
+ Returns:
162
+ Filtered list of violations
163
+ """
164
+ if not rule:
165
+ return violations
166
+
167
+ rule_id = PERF_RULES.get(rule)
168
+ if not rule_id:
169
+ logger.warning(f"Unknown rule '{rule}'. Valid rules: {', '.join(PERF_RULES.keys())}")
170
+ return violations
171
+
172
+ return [v for v in violations if v.rule_id == rule_id]
173
+
174
+
175
+ def _run_all_perf_lint(
176
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, rule: str | None
177
+ ) -> list[Violation]:
178
+ """Execute all performance lints on files or directories.
179
+
180
+ Args:
181
+ orchestrator: Configured orchestrator instance
182
+ path_objs: List of paths to analyze
183
+ recursive: Whether to scan directories recursively
184
+ rule: Optional rule filter (string-concat, regex-loop, or full rule names)
185
+
186
+ Returns:
187
+ List of performance-related violations
188
+ """
189
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
190
+ perf_violations = [v for v in all_violations if v.rule_id.startswith("performance.")]
191
+ return _filter_by_rule(perf_violations, rule)
192
+
193
+
194
+ def _execute_perf_lint(params: ExecuteParams, rule: str | None) -> NoReturn:
195
+ """Execute combined performance lint."""
196
+ orchestrator = _setup_and_validate(params)
197
+ violations = _run_all_perf_lint(orchestrator, params.path_objs, params.recursive, rule)
198
+
199
+ if params.verbose:
200
+ logger.info(f"Found {len(violations)} performance violation(s)")
201
+
202
+ format_violations(violations, params.format)
203
+ sys.exit(1 if violations else 0)
204
+
205
+
206
+ @cli.command(
207
+ "perf",
208
+ help="""Check for performance anti-patterns in code.
209
+
210
+ Detects common performance issues in loops:
211
+ - string-concat-loop: O(n^2) string building using += in loops
212
+ - regex-in-loop: Regex recompilation on each loop iteration
213
+
214
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
215
+
216
+ Examples:
217
+
218
+ \\b
219
+ # Check current directory for all performance issues
220
+ thai-lint perf
221
+
222
+ \\b
223
+ # Check specific directory
224
+ thai-lint perf src/
225
+
226
+ \\b
227
+ # Check only string concatenation issues
228
+ thai-lint perf --rule string-concat src/
229
+
230
+ \\b
231
+ # Check only regex-in-loop issues
232
+ thai-lint perf --rule regex-loop src/
233
+
234
+ \\b
235
+ # Get JSON output
236
+ thai-lint perf --format json .
237
+
238
+ \\b
239
+ # Use custom config file
240
+ thai-lint perf --config .thailint.yaml src/
241
+ """,
242
+ )
243
+ @click.option(
244
+ "--rule",
245
+ "-r",
246
+ "rule",
247
+ type=click.Choice(["string-concat", "regex-loop"]),
248
+ help="Run only a specific performance rule",
249
+ )
250
+ @standard_linter_options
251
+ def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
252
+ ctx: click.Context,
253
+ paths: tuple[str, ...],
254
+ config_file: str | None,
255
+ format: str,
256
+ recursive: bool,
257
+ rule: str | None,
258
+ ) -> None:
259
+ """Run all performance linters.
260
+
261
+ Args:
262
+ ctx: Click context with global options
263
+ paths: Files or directories to lint
264
+ config_file: Optional path to config file
265
+ format: Output format (text, json, sarif)
266
+ recursive: Whether to scan directories recursively
267
+ rule: Optional rule filter (string-concat or regex-loop)
268
+ """
269
+ params = prepare_standard_command(ctx, paths, config_file, format, recursive)
270
+
271
+ def execute_with_rule(p: ExecuteParams) -> None:
272
+ _execute_perf_lint(p, rule)
273
+
274
+ run_linter_command(execute_with_rule, params)
src/cli/linters/shared.py CHANGED
@@ -4,27 +4,172 @@ Purpose: Shared utilities for linter CLI commands
4
4
  Scope: Common helper functions and patterns used across all linter command modules
5
5
 
6
6
  Overview: Provides reusable utilities for linter CLI commands including config section management,
7
- config value setting with logging, and rule ID filtering. Centralizes shared patterns to reduce
8
- duplication across linter command modules (code_quality, code_patterns, structure, documentation).
9
- All utilities are designed to work with the orchestrator configuration system.
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.
10
11
 
11
- Dependencies: logging for debug output, pathlib for Path type hints
12
+ Dependencies: logging for debug output, pathlib for Path type hints, click for Context type,
13
+ dataclasses for CommandContext
12
14
 
13
- Exports: ensure_config_section, set_config_value, filter_violations_by_prefix
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
14
19
 
15
- Interfaces: Orchestrator config dict manipulation, violation list filtering
20
+ Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI context extraction,
21
+ help text generation
16
22
 
17
23
  Implementation: Pure helper functions with no side effects beyond config mutation and logging
18
24
  """
19
25
 
20
26
  import logging
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
21
29
  from typing import TYPE_CHECKING, Any
22
30
 
31
+ import click
32
+
33
+ from src.cli.utils import format_option, get_project_root_from_context, handle_linting_error
23
34
  from src.core.types import Violation
24
35
 
25
36
  if TYPE_CHECKING:
37
+ from collections.abc import Callable
38
+
26
39
  from src.orchestrator.core import Orchestrator
27
40
 
41
+
42
+ def standard_linter_options(f: Any) -> Any:
43
+ """Apply standard linter CLI options to a command.
44
+
45
+ Bundles the common options used by most linter commands:
46
+ - paths argument (variadic)
47
+ - config file option
48
+ - format option
49
+ - recursive option
50
+ - pass_context
51
+
52
+ Usage:
53
+ @cli.command("my-linter")
54
+ @standard_linter_options
55
+ def my_linter(ctx, paths, config_file, format, recursive):
56
+ ...
57
+ """
58
+ f = click.pass_context(f)
59
+ f = click.option(
60
+ "--recursive/--no-recursive", default=True, help="Scan directories recursively"
61
+ )(f)
62
+ f = format_option(f)
63
+ f = click.option(
64
+ "--config", "-c", "config_file", type=click.Path(), help="Path to config file"
65
+ )(f)
66
+ f = click.argument("paths", nargs=-1, type=click.Path())(f)
67
+ return f
68
+
69
+
70
+ @dataclass
71
+ class CommandContext:
72
+ """Extracted context from CLI command invocation.
73
+
74
+ Consolidates common CLI command setup into a reusable structure.
75
+ """
76
+
77
+ verbose: bool
78
+ project_root: Path | None
79
+ path_objs: list[Path]
80
+
81
+
82
+ @dataclass
83
+ class ExecuteParams:
84
+ """Parameters for linter execution functions.
85
+
86
+ Bundles the common parameters passed to _execute_*_lint functions
87
+ to reduce function signature duplication across CLI modules.
88
+ """
89
+
90
+ path_objs: list[Path]
91
+ config_file: str | None
92
+ format: str
93
+ recursive: bool
94
+ verbose: bool
95
+ project_root: Path | None
96
+
97
+
98
+ def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
99
+ """Extract common context values from CLI command invocation.
100
+
101
+ Consolidates the repeated pattern of extracting verbose, project_root,
102
+ and converting paths to Path objects with default handling.
103
+
104
+ Args:
105
+ ctx: Click context from command invocation
106
+ paths: Tuple of path strings from command arguments
107
+
108
+ Returns:
109
+ CommandContext with extracted values
110
+ """
111
+ verbose: bool = ctx.obj.get("verbose", False)
112
+ project_root = get_project_root_from_context(ctx)
113
+
114
+ if not paths:
115
+ paths = (".",)
116
+
117
+ path_objs = [Path(p) for p in paths]
118
+
119
+ return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
120
+
121
+
122
+ def prepare_standard_command(
123
+ ctx: click.Context,
124
+ paths: tuple[str, ...],
125
+ config_file: str | None,
126
+ format: str,
127
+ recursive: bool,
128
+ ) -> ExecuteParams:
129
+ """Prepare standard linter command execution parameters.
130
+
131
+ Combines context extraction and ExecuteParams creation into a single call.
132
+ Use with commands that have the standard options (config, format, recursive).
133
+
134
+ Args:
135
+ ctx: Click context from command invocation
136
+ paths: Tuple of path strings from command arguments
137
+ config_file: Optional config file path
138
+ format: Output format
139
+ recursive: Whether to scan recursively
140
+
141
+ Returns:
142
+ ExecuteParams ready for _execute_*_lint function
143
+ """
144
+ cmd_ctx = extract_command_context(ctx, paths)
145
+ return ExecuteParams(
146
+ path_objs=cmd_ctx.path_objs,
147
+ config_file=config_file,
148
+ format=format,
149
+ recursive=recursive,
150
+ verbose=cmd_ctx.verbose,
151
+ project_root=cmd_ctx.project_root,
152
+ )
153
+
154
+
155
+ def run_linter_command(
156
+ execute_fn: "Callable[[ExecuteParams], None]",
157
+ params: ExecuteParams,
158
+ ) -> None:
159
+ """Run a linter command with standard error handling.
160
+
161
+ Wraps the try/except pattern used by all linter commands.
162
+
163
+ Args:
164
+ execute_fn: The _execute_*_lint function to call
165
+ params: ExecuteParams for the execution
166
+ """
167
+ try:
168
+ execute_fn(params)
169
+ except Exception as e:
170
+ handle_linting_error(e, params.verbose)
171
+
172
+
28
173
  # Configure module logger
29
174
  logger = logging.getLogger(__name__)
30
175
 
@@ -87,3 +232,84 @@ def filter_violations_by_startswith(violations: list[Violation], prefix: str) ->
87
232
  Filtered list of violations where rule_id starts with the prefix
88
233
  """
89
234
  return [v for v in violations if v.rule_id.startswith(prefix)]
235
+
236
+
237
+ def create_linter_command(
238
+ name: str,
239
+ execute_fn: "Callable[[ExecuteParams], None]",
240
+ brief: str,
241
+ description: str,
242
+ ) -> "Callable[..., None]":
243
+ """Create a standard linter CLI command.
244
+
245
+ Factory function that generates Click commands with consistent structure,
246
+ eliminating boilerplate duplication across linter command modules.
247
+
248
+ Args:
249
+ name: CLI command name (e.g., "magic-numbers")
250
+ execute_fn: The _execute_*_lint function to call
251
+ brief: Brief one-line description
252
+ description: Detailed multi-line description
253
+
254
+ Returns:
255
+ Decorated Click command function
256
+ """
257
+ from src.cli.main import cli
258
+
259
+ @cli.command(name, help=make_linter_help(name, brief, description))
260
+ @standard_linter_options
261
+ def command(
262
+ ctx: click.Context,
263
+ paths: tuple[str, ...],
264
+ config_file: str | None,
265
+ format: str,
266
+ recursive: bool,
267
+ ) -> None:
268
+ params = prepare_standard_command(ctx, paths, config_file, format, recursive)
269
+ run_linter_command(execute_fn, params)
270
+
271
+ return command
272
+
273
+
274
+ def make_linter_help(command: str, brief: str, description: str) -> str:
275
+ """Generate standardized CLI help text for linter commands.
276
+
277
+ Creates consistent help text following the established pattern with
278
+ examples showing common usage patterns.
279
+
280
+ Args:
281
+ command: CLI command name (e.g., "magic-numbers")
282
+ brief: Brief one-line description
283
+ description: Detailed multi-line description
284
+
285
+ Returns:
286
+ Formatted help text string for Click command
287
+ """
288
+ return f"""{brief}
289
+
290
+ {description}
291
+
292
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
293
+
294
+ Examples:
295
+
296
+ \\b
297
+ # Check current directory (all files recursively)
298
+ thai-lint {command}
299
+
300
+ \\b
301
+ # Check specific directory
302
+ thai-lint {command} src/
303
+
304
+ \\b
305
+ # Check single file
306
+ thai-lint {command} src/app.py
307
+
308
+ \\b
309
+ # Get JSON output
310
+ thai-lint {command} --format json .
311
+
312
+ \\b
313
+ # Use custom config file
314
+ thai-lint {command} --config .thailint.yaml src/
315
+ """