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
@@ -1,43 +1,30 @@
1
1
  """
2
- Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class)
2
+ Purpose: CLI commands for code pattern linters (print-statements, method-property, stateless-class, lazy-ignores)
3
3
 
4
4
  Scope: Commands that detect code patterns and anti-patterns in Python code
5
5
 
6
6
  Overview: Provides CLI commands for code pattern linting: print-statements detects print() and
7
7
  console.log calls that should use proper logging, method-property finds methods that should be
8
- @property decorators, and stateless-class detects classes without state that should be module
9
- functions. Each command supports standard options (config, format, recursive) and integrates
10
- with the orchestrator for execution.
8
+ @property decorators, stateless-class detects classes without state that should be module
9
+ functions, and lazy-ignores detects unjustified linting suppressions. Each command supports
10
+ standard options (config, format, recursive) and integrates with the orchestrator for execution.
11
11
 
12
12
  Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
13
13
 
14
- Exports: print_statements command, method_property command, stateless_class command
14
+ Exports: print_statements command, method_property command, stateless_class command, lazy_ignores command
15
15
 
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,90 +55,32 @@ def _run_print_statements_lint(
68
55
  return [v for v in all_violations if "print-statement" in v.rule_id]
69
56
 
70
57
 
71
- @cli.command("print-statements")
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 print_statements( # pylint: disable=too-many-arguments,too-many-positional-arguments
78
- ctx: click.Context,
79
- paths: tuple[str, ...],
80
- config_file: str | None,
81
- format: str,
82
- recursive: bool,
83
- ) -> None:
84
- """Check for print/console statements in code.
85
-
86
- Detects print() calls in Python and console.log/warn/error/debug/info calls
87
- in TypeScript/JavaScript that should be replaced with proper logging.
88
-
89
- PATHS: Files or directories to lint (defaults to current directory if none provided)
90
-
91
- Examples:
92
-
93
- \b
94
- # Check current directory (all files recursively)
95
- thai-lint print-statements
96
-
97
- \b
98
- # Check specific directory
99
- thai-lint print-statements src/
100
-
101
- \b
102
- # Check single file
103
- thai-lint print-statements src/app.py
104
-
105
- \b
106
- # Check multiple files
107
- thai-lint print-statements src/app.py src/utils.ts tests/test_app.py
108
-
109
- \b
110
- # Get JSON output
111
- thai-lint print-statements --format json .
112
-
113
- \b
114
- # Use custom config file
115
- thai-lint print-statements --config .thailint.yaml src/
116
- """
117
- verbose: bool = ctx.obj.get("verbose", False)
118
- project_root = get_project_root_from_context(ctx)
119
-
120
- if not paths:
121
- paths = (".",)
122
-
123
- path_objs = [Path(p) for p in paths]
124
-
125
- try:
126
- _execute_print_statements_lint(
127
- path_objs, config_file, format, recursive, verbose, project_root
128
- )
129
- except Exception as e:
130
- handle_linting_error(e, verbose)
131
-
132
-
133
- def _execute_print_statements_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
134
- path_objs: list[Path],
135
- config_file: str | None,
136
- format: str,
137
- recursive: bool,
138
- verbose: bool,
139
- project_root: Path | None = None,
140
- ) -> NoReturn:
58
+ def _execute_print_statements_lint(params: ExecuteParams) -> NoReturn:
141
59
  """Execute print-statements lint."""
142
- validate_paths_exist(path_objs)
60
+ validate_paths_exist(params.path_objs)
143
61
  orchestrator = _setup_print_statements_orchestrator(
144
- path_objs, config_file, verbose, project_root
62
+ params.path_objs, params.config_file, params.verbose, params.project_root
63
+ )
64
+ print_statements_violations = _run_print_statements_lint(
65
+ orchestrator, params.path_objs, params.recursive
145
66
  )
146
- print_statements_violations = _run_print_statements_lint(orchestrator, path_objs, recursive)
147
67
 
148
- if verbose:
68
+ if params.verbose:
149
69
  logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
150
70
 
151
- format_violations(print_statements_violations, format)
71
+ format_violations(print_statements_violations, params.format)
152
72
  sys.exit(1 if print_statements_violations else 0)
153
73
 
154
74
 
75
+ print_statements = create_linter_command(
76
+ "print-statements",
77
+ _execute_print_statements_lint,
78
+ "Check for print/console statements in code.",
79
+ "Detects print() calls in Python and console.log/warn/error/debug/info calls\n"
80
+ " in TypeScript/JavaScript that should be replaced with proper logging.",
81
+ )
82
+
83
+
155
84
  # =============================================================================
156
85
  # Method Property Command
157
86
  # =============================================================================
@@ -172,97 +101,33 @@ def _run_method_property_lint(
172
101
  return [v for v in all_violations if "method-property" in v.rule_id]
173
102
 
174
103
 
175
- @cli.command("method-property")
176
- @click.argument("paths", nargs=-1, type=click.Path())
177
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
178
- @format_option
179
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
180
- @click.pass_context
181
- def method_property(
182
- ctx: click.Context,
183
- paths: tuple[str, ...],
184
- config_file: str | None,
185
- format: str,
186
- recursive: bool,
187
- ) -> None:
188
- """Check for methods that should be @property decorators.
189
-
190
- Detects Python methods that could be converted to properties following
191
- Pythonic conventions:
192
- - Methods returning only self._attribute or self.attribute
193
- - get_* prefixed methods (Java-style getters)
194
- - Simple computed values with no side effects
195
-
196
- PATHS: Files or directories to lint (defaults to current directory if none provided)
197
-
198
- Examples:
199
-
200
- \b
201
- # Check current directory (all files recursively)
202
- thai-lint method-property
203
-
204
- \b
205
- # Check specific directory
206
- thai-lint method-property src/
207
-
208
- \b
209
- # Check single file
210
- thai-lint method-property src/models.py
211
-
212
- \b
213
- # Check multiple files
214
- thai-lint method-property src/models.py src/services.py
215
-
216
- \b
217
- # Get JSON output
218
- thai-lint method-property --format json .
219
-
220
- \b
221
- # Get SARIF output for CI/CD integration
222
- thai-lint method-property --format sarif src/
223
-
224
- \b
225
- # Use custom config file
226
- thai-lint method-property --config .thailint.yaml src/
227
- """
228
- verbose: bool = ctx.obj.get("verbose", False)
229
- project_root = get_project_root_from_context(ctx)
230
-
231
- if not paths:
232
- paths = (".",)
233
-
234
- path_objs = [Path(p) for p in paths]
235
-
236
- try:
237
- _execute_method_property_lint(
238
- path_objs, config_file, format, recursive, verbose, project_root
239
- )
240
- except Exception as e:
241
- handle_linting_error(e, verbose)
242
-
243
-
244
- def _execute_method_property_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
245
- path_objs: list[Path],
246
- config_file: str | None,
247
- format: str,
248
- recursive: bool,
249
- verbose: bool,
250
- project_root: Path | None = None,
251
- ) -> NoReturn:
104
+ def _execute_method_property_lint(params: ExecuteParams) -> NoReturn:
252
105
  """Execute method-property lint."""
253
- validate_paths_exist(path_objs)
106
+ validate_paths_exist(params.path_objs)
254
107
  orchestrator = _setup_method_property_orchestrator(
255
- path_objs, config_file, verbose, project_root
108
+ params.path_objs, params.config_file, params.verbose, params.project_root
109
+ )
110
+ method_property_violations = _run_method_property_lint(
111
+ orchestrator, params.path_objs, params.recursive
256
112
  )
257
- method_property_violations = _run_method_property_lint(orchestrator, path_objs, recursive)
258
113
 
259
- if verbose:
114
+ if params.verbose:
260
115
  logger.info(f"Found {len(method_property_violations)} method-property violation(s)")
261
116
 
262
- format_violations(method_property_violations, format)
117
+ format_violations(method_property_violations, params.format)
263
118
  sys.exit(1 if method_property_violations else 0)
264
119
 
265
120
 
121
+ method_property = create_linter_command(
122
+ "method-property",
123
+ _execute_method_property_lint,
124
+ "Check for methods that should be @property decorators.",
125
+ "Detects Python methods that could be converted to properties following\n"
126
+ " Pythonic conventions: methods returning self._attribute, get_* prefixed\n"
127
+ " methods (Java-style getters), or simple computed values with no side effects.",
128
+ )
129
+
130
+
266
131
  # =============================================================================
267
132
  # Stateless Class Command
268
133
  # =============================================================================
@@ -283,90 +148,75 @@ def _run_stateless_class_lint(
283
148
  return [v for v in all_violations if "stateless-class" in v.rule_id]
284
149
 
285
150
 
286
- @cli.command("stateless-class")
287
- @click.argument("paths", nargs=-1, type=click.Path())
288
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
289
- @format_option
290
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
291
- @click.pass_context
292
- def stateless_class(
293
- ctx: click.Context,
294
- paths: tuple[str, ...],
295
- config_file: str | None,
296
- format: str,
297
- recursive: bool,
298
- ) -> None:
299
- """Check for stateless classes that should be module functions.
300
-
301
- Detects Python classes that have no constructor (__init__), no instance
302
- state, and 2+ methods - indicating they should be refactored to module-level
303
- functions instead of using a class as a namespace.
304
-
305
- PATHS: Files or directories to lint (defaults to current directory if none provided)
306
-
307
- Examples:
308
-
309
- \b
310
- # Check current directory (all files recursively)
311
- thai-lint stateless-class
312
-
313
- \b
314
- # Check specific directory
315
- thai-lint stateless-class src/
316
-
317
- \b
318
- # Check single file
319
- thai-lint stateless-class src/utils.py
320
-
321
- \b
322
- # Check multiple files
323
- thai-lint stateless-class src/utils.py src/helpers.py
324
-
325
- \b
326
- # Get JSON output
327
- thai-lint stateless-class --format json .
328
-
329
- \b
330
- # Get SARIF output for CI/CD integration
331
- thai-lint stateless-class --format sarif src/
332
-
333
- \b
334
- # Use custom config file
335
- thai-lint stateless-class --config .thailint.yaml src/
336
- """
337
- verbose: bool = ctx.obj.get("verbose", False)
338
- project_root = get_project_root_from_context(ctx)
339
-
340
- if not paths:
341
- paths = (".",)
342
-
343
- path_objs = [Path(p) for p in paths]
344
-
345
- try:
346
- _execute_stateless_class_lint(
347
- path_objs, config_file, format, recursive, verbose, project_root
348
- )
349
- except Exception as e:
350
- handle_linting_error(e, verbose)
351
-
352
-
353
- def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
354
- path_objs: list[Path],
355
- config_file: str | None,
356
- format: str,
357
- recursive: bool,
358
- verbose: bool,
359
- project_root: Path | None = None,
360
- ) -> NoReturn:
151
+ def _execute_stateless_class_lint(params: ExecuteParams) -> NoReturn:
361
152
  """Execute stateless-class lint."""
362
- validate_paths_exist(path_objs)
153
+ validate_paths_exist(params.path_objs)
363
154
  orchestrator = _setup_stateless_class_orchestrator(
364
- path_objs, config_file, verbose, project_root
155
+ params.path_objs, params.config_file, params.verbose, params.project_root
156
+ )
157
+ stateless_class_violations = _run_stateless_class_lint(
158
+ orchestrator, params.path_objs, params.recursive
365
159
  )
366
- stateless_class_violations = _run_stateless_class_lint(orchestrator, path_objs, recursive)
367
160
 
368
- if verbose:
161
+ if params.verbose:
369
162
  logger.info(f"Found {len(stateless_class_violations)} stateless-class violation(s)")
370
163
 
371
- format_violations(stateless_class_violations, format)
164
+ format_violations(stateless_class_violations, params.format)
372
165
  sys.exit(1 if stateless_class_violations else 0)
166
+
167
+
168
+ stateless_class = create_linter_command(
169
+ "stateless-class",
170
+ _execute_stateless_class_lint,
171
+ "Check for stateless classes that should be module functions.",
172
+ "Detects Python classes that have no constructor (__init__), no instance\n"
173
+ " state, and 2+ methods - indicating they should be refactored to module-level\n"
174
+ " functions instead of using a class as a namespace.",
175
+ )
176
+
177
+
178
+ # =============================================================================
179
+ # Lazy Ignores Command
180
+ # =============================================================================
181
+
182
+
183
+ def _setup_lazy_ignores_orchestrator(
184
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
185
+ ) -> "Orchestrator":
186
+ """Set up orchestrator for lazy-ignores command."""
187
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
188
+
189
+
190
+ def _run_lazy_ignores_lint(
191
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
192
+ ) -> list[Violation]:
193
+ """Execute lazy-ignores lint on files or directories."""
194
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
195
+ return [v for v in all_violations if v.rule_id.startswith("lazy-ignores")]
196
+
197
+
198
+ def _execute_lazy_ignores_lint(params: ExecuteParams) -> NoReturn:
199
+ """Execute lazy-ignores lint."""
200
+ validate_paths_exist(params.path_objs)
201
+ orchestrator = _setup_lazy_ignores_orchestrator(
202
+ params.path_objs, params.config_file, params.verbose, params.project_root
203
+ )
204
+ lazy_ignores_violations = _run_lazy_ignores_lint(
205
+ orchestrator, params.path_objs, params.recursive
206
+ )
207
+
208
+ if params.verbose:
209
+ logger.info(f"Found {len(lazy_ignores_violations)} lazy-ignores violation(s)")
210
+
211
+ format_violations(lazy_ignores_violations, params.format)
212
+ sys.exit(1 if lazy_ignores_violations else 0)
213
+
214
+
215
+ lazy_ignores = create_linter_command(
216
+ "lazy-ignores",
217
+ _execute_lazy_ignores_lint,
218
+ "Check for unjustified linting suppressions.",
219
+ "Detects ignore directives (noqa, type:ignore, pylint:disable, nosec) that lack\n"
220
+ " corresponding entries in the file header's Suppressions section. Enforces a\n"
221
+ " header-based suppression model requiring human approval for all linting bypasses.",
222
+ )
@@ -18,10 +18,10 @@ Interfaces: Click CLI commands registered to main CLI group
18
18
 
19
19
  Implementation: Click decorators for command definition, orchestrator-based linting execution
20
20
 
21
- SRP Exception: CLI command modules follow Click framework patterns requiring similar command
22
- structure across all linter commands. This is intentional design for consistency.
21
+ Suppressions:
22
+ - too-many-arguments,too-many-positional-arguments: Click commands with custom options require
23
+ many parameters by framework design (dry command has 8 params for extra options)
23
24
  """
24
- # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
25
25
 
26
26
  import logging
27
27
  import sys
@@ -31,7 +31,12 @@ from typing import TYPE_CHECKING, Any, NoReturn
31
31
  import click
32
32
  import yaml
33
33
 
34
- from src.cli.linters.shared import ensure_config_section, set_config_value
34
+ from src.cli.linters.shared import (
35
+ ExecuteParams,
36
+ create_linter_command,
37
+ ensure_config_section,
38
+ set_config_value,
39
+ )
35
40
  from src.cli.main import cli
36
41
  from src.cli.utils import (
37
42
  execute_linting_on_paths,
@@ -52,7 +57,7 @@ logger = logging.getLogger(__name__)
52
57
 
53
58
 
54
59
  # =============================================================================
55
- # DRY Command
60
+ # DRY Command (custom options - cannot use create_linter_command)
56
61
  # =============================================================================
57
62
 
58
63
 
@@ -257,92 +262,32 @@ def _run_magic_numbers_lint(
257
262
  return [v for v in all_violations if "magic-number" in v.rule_id]
258
263
 
259
264
 
260
- @cli.command("magic-numbers")
261
- @click.argument("paths", nargs=-1, type=click.Path())
262
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
263
- @format_option
264
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
265
- @click.pass_context
266
- def magic_numbers( # pylint: disable=too-many-arguments,too-many-positional-arguments
267
- ctx: click.Context,
268
- paths: tuple[str, ...],
269
- config_file: str | None,
270
- format: str,
271
- recursive: bool,
272
- ) -> None:
273
- """Check for magic numbers in code.
274
-
275
- Detects unnamed numeric literals in Python and TypeScript/JavaScript code
276
- that should be extracted as named constants for better readability.
277
-
278
- PATHS: Files or directories to lint (defaults to current directory if none provided)
279
-
280
- Examples:
281
-
282
- \b
283
- # Check current directory (all files recursively)
284
- thai-lint magic-numbers
285
-
286
- \b
287
- # Check specific directory
288
- thai-lint magic-numbers src/
289
-
290
- \b
291
- # Check single file
292
- thai-lint magic-numbers src/app.py
293
-
294
- \b
295
- # Check multiple files
296
- thai-lint magic-numbers src/app.py src/utils.py tests/test_app.py
297
-
298
- \b
299
- # Check mix of files and directories
300
- thai-lint magic-numbers src/app.py tests/
301
-
302
- \b
303
- # Get JSON output
304
- thai-lint magic-numbers --format json .
305
-
306
- \b
307
- # Use custom config file
308
- thai-lint magic-numbers --config .thailint.yaml src/
309
- """
310
- verbose: bool = ctx.obj.get("verbose", False)
311
- project_root = get_project_root_from_context(ctx)
312
-
313
- if not paths:
314
- paths = (".",)
315
-
316
- path_objs = [Path(p) for p in paths]
317
-
318
- try:
319
- _execute_magic_numbers_lint(
320
- path_objs, config_file, format, recursive, verbose, project_root
321
- )
322
- except Exception as e:
323
- handle_linting_error(e, verbose)
324
-
325
-
326
- def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
327
- path_objs: list[Path],
328
- config_file: str | None,
329
- format: str,
330
- recursive: bool,
331
- verbose: bool,
332
- project_root: Path | None = None,
333
- ) -> NoReturn:
265
+ def _execute_magic_numbers_lint(params: ExecuteParams) -> NoReturn:
334
266
  """Execute magic-numbers lint."""
335
- validate_paths_exist(path_objs)
336
- orchestrator = _setup_magic_numbers_orchestrator(path_objs, config_file, verbose, project_root)
337
- magic_numbers_violations = _run_magic_numbers_lint(orchestrator, path_objs, recursive)
338
-
339
- if verbose:
267
+ validate_paths_exist(params.path_objs)
268
+ orchestrator = _setup_magic_numbers_orchestrator(
269
+ params.path_objs, params.config_file, params.verbose, params.project_root
270
+ )
271
+ magic_numbers_violations = _run_magic_numbers_lint(
272
+ orchestrator, params.path_objs, params.recursive
273
+ )
274
+
275
+ if params.verbose:
340
276
  logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
341
277
 
342
- format_violations(magic_numbers_violations, format)
278
+ format_violations(magic_numbers_violations, params.format)
343
279
  sys.exit(1 if magic_numbers_violations else 0)
344
280
 
345
281
 
282
+ magic_numbers = create_linter_command(
283
+ "magic-numbers",
284
+ _execute_magic_numbers_lint,
285
+ "Check for magic numbers in code.",
286
+ "Detects unnamed numeric literals in Python and TypeScript/JavaScript code\n"
287
+ " that should be extracted as named constants for better readability.",
288
+ )
289
+
290
+
346
291
  # =============================================================================
347
292
  # Stringly-Typed Command
348
293
  # =============================================================================
@@ -363,88 +308,26 @@ def _run_stringly_typed_lint(
363
308
  return [v for v in all_violations if "stringly-typed" in v.rule_id]
364
309
 
365
310
 
366
- @cli.command("stringly-typed")
367
- @click.argument("paths", nargs=-1, type=click.Path())
368
- @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
369
- @format_option
370
- @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
371
- @click.pass_context
372
- def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
373
- ctx: click.Context,
374
- paths: tuple[str, ...],
375
- config_file: str | None,
376
- format: str,
377
- recursive: bool,
378
- ) -> None:
379
- """Check for stringly-typed patterns in code.
380
-
381
- Detects string patterns in Python and TypeScript/JavaScript code that should
382
- use enums or typed alternatives. Finds membership validation, equality chains,
383
- and function calls with limited string values across multiple files.
384
-
385
- PATHS: Files or directories to lint (defaults to current directory if none provided)
386
-
387
- Examples:
388
-
389
- \b
390
- # Check current directory (all files recursively)
391
- thai-lint stringly-typed
392
-
393
- \b
394
- # Check specific directory
395
- thai-lint stringly-typed src/
396
-
397
- \b
398
- # Check single file
399
- thai-lint stringly-typed src/handlers.py
400
-
401
- \b
402
- # Check multiple files
403
- thai-lint stringly-typed src/handlers.py src/services.py
404
-
405
- \b
406
- # Get JSON output
407
- thai-lint stringly-typed --format json .
408
-
409
- \b
410
- # Get SARIF output for IDE integration
411
- thai-lint stringly-typed --format sarif .
412
-
413
- \b
414
- # Use custom config file
415
- thai-lint stringly-typed --config .thailint.yaml src/
416
- """
417
- verbose: bool = ctx.obj.get("verbose", False)
418
- project_root = get_project_root_from_context(ctx)
419
-
420
- if not paths:
421
- paths = (".",)
422
-
423
- path_objs = [Path(p) for p in paths]
424
-
425
- try:
426
- _execute_stringly_typed_lint(
427
- path_objs, config_file, format, recursive, verbose, project_root
428
- )
429
- except Exception as e:
430
- handle_linting_error(e, verbose)
431
-
432
-
433
- def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
434
- path_objs: list[Path],
435
- config_file: str | None,
436
- format: str,
437
- recursive: bool,
438
- verbose: bool,
439
- project_root: Path | None = None,
440
- ) -> NoReturn:
311
+ def _execute_stringly_typed_lint(params: ExecuteParams) -> NoReturn:
441
312
  """Execute stringly-typed lint."""
442
- validate_paths_exist(path_objs)
443
- orchestrator = _setup_stringly_typed_orchestrator(path_objs, config_file, verbose, project_root)
444
- stringly_violations = _run_stringly_typed_lint(orchestrator, path_objs, recursive)
313
+ validate_paths_exist(params.path_objs)
314
+ orchestrator = _setup_stringly_typed_orchestrator(
315
+ params.path_objs, params.config_file, params.verbose, params.project_root
316
+ )
317
+ stringly_violations = _run_stringly_typed_lint(orchestrator, params.path_objs, params.recursive)
445
318
 
446
- if verbose:
319
+ if params.verbose:
447
320
  logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
448
321
 
449
- format_violations(stringly_violations, format)
322
+ format_violations(stringly_violations, params.format)
450
323
  sys.exit(1 if stringly_violations else 0)
324
+
325
+
326
+ stringly_typed = create_linter_command(
327
+ "stringly-typed",
328
+ _execute_stringly_typed_lint,
329
+ "Check for stringly-typed patterns in code.",
330
+ "Detects string patterns in Python and TypeScript/JavaScript code that should\n"
331
+ " use enums or typed alternatives. Finds membership validation, equality chains,\n"
332
+ " and function calls with limited string values across multiple files.",
333
+ )