thailint 0.2.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 (214) 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 +44 -27
  23. src/core/base.py +95 -5
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +36 -6
  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 +125 -22
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +142 -94
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +68 -21
  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 +20 -82
  73. src/linters/dry/file_analyzer.py +15 -50
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +182 -54
  76. src/linters/dry/python_analyzer.py +108 -336
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/storage_initializer.py +9 -18
  80. src/linters/dry/token_hasher.py +129 -71
  81. src/linters/dry/typescript_analyzer.py +68 -380
  82. src/linters/dry/typescript_constant_extractor.py +138 -0
  83. src/linters/dry/typescript_statement_detector.py +255 -0
  84. src/linters/dry/typescript_value_extractor.py +70 -0
  85. src/linters/dry/violation_builder.py +4 -0
  86. src/linters/dry/violation_filter.py +9 -5
  87. src/linters/dry/violation_generator.py +71 -14
  88. src/linters/file_header/__init__.py +24 -0
  89. src/linters/file_header/atemporal_detector.py +105 -0
  90. src/linters/file_header/base_parser.py +93 -0
  91. src/linters/file_header/bash_parser.py +66 -0
  92. src/linters/file_header/config.py +140 -0
  93. src/linters/file_header/css_parser.py +70 -0
  94. src/linters/file_header/field_validator.py +72 -0
  95. src/linters/file_header/linter.py +309 -0
  96. src/linters/file_header/markdown_parser.py +130 -0
  97. src/linters/file_header/python_parser.py +42 -0
  98. src/linters/file_header/typescript_parser.py +73 -0
  99. src/linters/file_header/violation_builder.py +79 -0
  100. src/linters/file_placement/config_loader.py +3 -1
  101. src/linters/file_placement/directory_matcher.py +4 -0
  102. src/linters/file_placement/linter.py +74 -31
  103. src/linters/file_placement/pattern_matcher.py +41 -6
  104. src/linters/file_placement/pattern_validator.py +31 -12
  105. src/linters/file_placement/rule_checker.py +12 -7
  106. src/linters/lazy_ignores/__init__.py +43 -0
  107. src/linters/lazy_ignores/config.py +74 -0
  108. src/linters/lazy_ignores/directive_utils.py +164 -0
  109. src/linters/lazy_ignores/header_parser.py +177 -0
  110. src/linters/lazy_ignores/linter.py +158 -0
  111. src/linters/lazy_ignores/matcher.py +168 -0
  112. src/linters/lazy_ignores/python_analyzer.py +209 -0
  113. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  114. src/linters/lazy_ignores/skip_detector.py +298 -0
  115. src/linters/lazy_ignores/types.py +71 -0
  116. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  117. src/linters/lazy_ignores/violation_builder.py +135 -0
  118. src/linters/lbyl/__init__.py +31 -0
  119. src/linters/lbyl/config.py +63 -0
  120. src/linters/lbyl/linter.py +67 -0
  121. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  122. src/linters/lbyl/pattern_detectors/base.py +63 -0
  123. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  124. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  125. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  126. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  127. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  128. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  129. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  130. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  131. src/linters/lbyl/python_analyzer.py +215 -0
  132. src/linters/lbyl/violation_builder.py +354 -0
  133. src/linters/magic_numbers/__init__.py +48 -0
  134. src/linters/magic_numbers/config.py +82 -0
  135. src/linters/magic_numbers/context_analyzer.py +249 -0
  136. src/linters/magic_numbers/linter.py +462 -0
  137. src/linters/magic_numbers/python_analyzer.py +64 -0
  138. src/linters/magic_numbers/typescript_analyzer.py +215 -0
  139. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  140. src/linters/magic_numbers/violation_builder.py +98 -0
  141. src/linters/method_property/__init__.py +49 -0
  142. src/linters/method_property/config.py +138 -0
  143. src/linters/method_property/linter.py +414 -0
  144. src/linters/method_property/python_analyzer.py +473 -0
  145. src/linters/method_property/violation_builder.py +119 -0
  146. src/linters/nesting/__init__.py +6 -2
  147. src/linters/nesting/config.py +6 -3
  148. src/linters/nesting/linter.py +31 -34
  149. src/linters/nesting/python_analyzer.py +4 -0
  150. src/linters/nesting/typescript_analyzer.py +6 -11
  151. src/linters/nesting/violation_builder.py +1 -0
  152. src/linters/performance/__init__.py +91 -0
  153. src/linters/performance/config.py +43 -0
  154. src/linters/performance/constants.py +49 -0
  155. src/linters/performance/linter.py +149 -0
  156. src/linters/performance/python_analyzer.py +365 -0
  157. src/linters/performance/regex_analyzer.py +312 -0
  158. src/linters/performance/regex_linter.py +139 -0
  159. src/linters/performance/typescript_analyzer.py +236 -0
  160. src/linters/performance/violation_builder.py +160 -0
  161. src/linters/print_statements/__init__.py +53 -0
  162. src/linters/print_statements/config.py +78 -0
  163. src/linters/print_statements/linter.py +413 -0
  164. src/linters/print_statements/python_analyzer.py +153 -0
  165. src/linters/print_statements/typescript_analyzer.py +125 -0
  166. src/linters/print_statements/violation_builder.py +96 -0
  167. src/linters/srp/__init__.py +3 -3
  168. src/linters/srp/class_analyzer.py +11 -7
  169. src/linters/srp/config.py +12 -6
  170. src/linters/srp/heuristics.py +56 -22
  171. src/linters/srp/linter.py +47 -39
  172. src/linters/srp/python_analyzer.py +55 -20
  173. src/linters/srp/typescript_metrics_calculator.py +110 -50
  174. src/linters/stateless_class/__init__.py +25 -0
  175. src/linters/stateless_class/config.py +58 -0
  176. src/linters/stateless_class/linter.py +349 -0
  177. src/linters/stateless_class/python_analyzer.py +290 -0
  178. src/linters/stringly_typed/__init__.py +36 -0
  179. src/linters/stringly_typed/config.py +189 -0
  180. src/linters/stringly_typed/context_filter.py +451 -0
  181. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  182. src/linters/stringly_typed/ignore_checker.py +100 -0
  183. src/linters/stringly_typed/ignore_utils.py +51 -0
  184. src/linters/stringly_typed/linter.py +376 -0
  185. src/linters/stringly_typed/python/__init__.py +33 -0
  186. src/linters/stringly_typed/python/analyzer.py +348 -0
  187. src/linters/stringly_typed/python/call_tracker.py +175 -0
  188. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  189. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  190. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  191. src/linters/stringly_typed/python/constants.py +21 -0
  192. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  193. src/linters/stringly_typed/python/validation_detector.py +189 -0
  194. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  195. src/linters/stringly_typed/storage.py +620 -0
  196. src/linters/stringly_typed/storage_initializer.py +45 -0
  197. src/linters/stringly_typed/typescript/__init__.py +28 -0
  198. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  199. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  200. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  201. src/linters/stringly_typed/violation_generator.py +419 -0
  202. src/orchestrator/core.py +264 -16
  203. src/orchestrator/language_detector.py +5 -3
  204. src/templates/thailint_config_template.yaml +354 -0
  205. src/utils/project_root.py +138 -16
  206. thailint-0.15.3.dist-info/METADATA +187 -0
  207. thailint-0.15.3.dist-info/RECORD +226 -0
  208. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +1 -1
  209. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  210. src/cli.py +0 -1055
  211. thailint-0.2.0.dist-info/METADATA +0 -980
  212. thailint-0.2.0.dist-info/RECORD +0 -75
  213. thailint-0.2.0.dist-info/entry_points.txt +0 -4
  214. {thailint-0.2.0.dist-info → thailint-0.15.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,342 @@
1
+ """
2
+ Purpose: CLI commands for code smell linters (dry, magic-numbers, stringly-typed)
3
+
4
+ Scope: Commands that detect code smells like duplicate code, magic numbers, and stringly-typed patterns
5
+
6
+ Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
7
+ token-based hashing with SQLite caching, magic-numbers detects unnamed numeric literals that
8
+ should be extracted as named constants, and stringly-typed detects string patterns that should
9
+ use enums. Each command supports standard options (config, format, recursive) plus linter-specific
10
+ options 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, yaml for config loading
14
+
15
+ Exports: dry command, magic_numbers command, stringly_typed 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
+ many parameters by framework design (dry command has 8 params for extra options)
24
+ """
25
+
26
+ import logging
27
+ import sys
28
+ from pathlib import Path
29
+ from typing import TYPE_CHECKING, Any, NoReturn
30
+
31
+ import click
32
+ import yaml
33
+
34
+ from src.cli.linters.shared import (
35
+ ExecuteParams,
36
+ create_linter_command,
37
+ ensure_config_section,
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_project_root_from_context,
45
+ handle_linting_error,
46
+ parallel_option,
47
+ setup_base_orchestrator,
48
+ validate_paths_exist,
49
+ )
50
+ from src.core.cli_utils import format_violations
51
+ from src.core.types import Violation
52
+
53
+ if TYPE_CHECKING:
54
+ from src.orchestrator.core import Orchestrator
55
+
56
+ # Configure module logger
57
+ logger = logging.getLogger(__name__)
58
+
59
+
60
+ # =============================================================================
61
+ # DRY Command (custom options - cannot use create_linter_command)
62
+ # =============================================================================
63
+
64
+
65
+ def _setup_dry_orchestrator(
66
+ path_objs: list[Path],
67
+ config_file: str | None,
68
+ verbose: bool,
69
+ project_root: Path | None = None,
70
+ ) -> "Orchestrator":
71
+ """Set up orchestrator for DRY linting."""
72
+ return setup_base_orchestrator(path_objs, None, verbose, project_root)
73
+
74
+
75
+ def _load_dry_config_file(orchestrator: "Orchestrator", config_file: str, verbose: bool) -> None:
76
+ """Load DRY configuration from file."""
77
+ config_path = Path(config_file)
78
+ if not config_path.exists():
79
+ click.echo(f"Error: Config file not found: {config_file}", err=True)
80
+ sys.exit(2)
81
+
82
+ with config_path.open("r", encoding="utf-8") as f:
83
+ config: dict[str, Any] = yaml.safe_load(f)
84
+
85
+ try:
86
+ dry_config = config["dry"]
87
+ except KeyError:
88
+ return # No DRY config in file
89
+ orchestrator.config.update({"dry": dry_config})
90
+ if verbose:
91
+ logger.info(f"Loaded DRY config from {config_file}")
92
+
93
+
94
+ def _apply_dry_config_override(
95
+ orchestrator: "Orchestrator", min_lines: int | None, no_cache: bool, verbose: bool
96
+ ) -> None:
97
+ """Apply CLI option overrides to DRY config."""
98
+ dry_config = ensure_config_section(orchestrator, "dry")
99
+ set_config_value(dry_config, "min_duplicate_lines", min_lines, verbose)
100
+ if no_cache:
101
+ set_config_value(dry_config, "cache_enabled", False, verbose)
102
+
103
+
104
+ def _clear_dry_cache(orchestrator: "Orchestrator", verbose: bool) -> None:
105
+ """Clear DRY cache before running."""
106
+ cache_path_str = orchestrator.config.get("dry", {}).get("cache_path", ".thailint-cache/dry.db")
107
+ cache_path = orchestrator.project_root / cache_path_str
108
+
109
+ if cache_path.exists():
110
+ cache_path.unlink()
111
+ if verbose:
112
+ logger.info(f"Cleared cache: {cache_path}")
113
+ elif verbose:
114
+ logger.info("Cache file does not exist, nothing to clear")
115
+
116
+
117
+ def _run_dry_lint(
118
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
119
+ ) -> list[Violation]:
120
+ """Run DRY linting and return violations."""
121
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
122
+ return [v for v in all_violations if v.rule_id.startswith("dry.")]
123
+
124
+
125
+ @cli.command("dry")
126
+ @click.argument("paths", nargs=-1, type=click.Path())
127
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
128
+ @format_option
129
+ @click.option("--min-lines", type=int, help="Override min duplicate lines threshold")
130
+ @click.option("--no-cache", is_flag=True, help="Disable SQLite cache (force rehash)")
131
+ @click.option("--clear-cache", is_flag=True, help="Clear cache before running")
132
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
133
+ @parallel_option
134
+ @click.pass_context
135
+ def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
136
+ ctx: click.Context,
137
+ paths: tuple[str, ...],
138
+ config_file: str | None,
139
+ format: str,
140
+ min_lines: int | None,
141
+ no_cache: bool,
142
+ clear_cache: bool,
143
+ recursive: bool,
144
+ parallel: bool,
145
+ ) -> None:
146
+ # Justification for Pylint disables:
147
+ # - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 6 options = 8 params
148
+ """
149
+ Check for duplicate code (DRY principle violations).
150
+
151
+ Detects duplicate code blocks across your project using token-based hashing
152
+ with SQLite caching for fast incremental scans.
153
+
154
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
155
+
156
+ Examples:
157
+
158
+ \b
159
+ # Check current directory (all files recursively)
160
+ thai-lint dry
161
+
162
+ \b
163
+ # Check specific directory
164
+ thai-lint dry src/
165
+
166
+ \b
167
+ # Check single file
168
+ thai-lint dry src/app.py
169
+
170
+ \b
171
+ # Check multiple files
172
+ thai-lint dry src/app.py src/service.py tests/test_app.py
173
+
174
+ \b
175
+ # Use custom config file
176
+ thai-lint dry --config .thailint.yaml src/
177
+
178
+ \b
179
+ # Override minimum duplicate lines threshold
180
+ thai-lint dry --min-lines 5 .
181
+
182
+ \b
183
+ # Disable cache (force re-analysis)
184
+ thai-lint dry --no-cache .
185
+
186
+ \b
187
+ # Clear cache before running
188
+ thai-lint dry --clear-cache .
189
+
190
+ \b
191
+ # Get JSON output
192
+ thai-lint dry --format json .
193
+ """
194
+ verbose: bool = ctx.obj.get("verbose", False)
195
+ project_root = get_project_root_from_context(ctx)
196
+
197
+ if not paths:
198
+ paths = (".",)
199
+
200
+ path_objs = [Path(p) for p in paths]
201
+
202
+ try:
203
+ _execute_dry_lint(
204
+ path_objs,
205
+ config_file,
206
+ format,
207
+ min_lines,
208
+ no_cache,
209
+ clear_cache,
210
+ recursive,
211
+ parallel,
212
+ verbose,
213
+ project_root,
214
+ )
215
+ except Exception as e:
216
+ handle_linting_error(e, verbose)
217
+
218
+
219
+ def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
220
+ path_objs: list[Path],
221
+ config_file: str | None,
222
+ format: str,
223
+ min_lines: int | None,
224
+ no_cache: bool,
225
+ clear_cache: bool,
226
+ recursive: bool,
227
+ parallel: bool,
228
+ verbose: bool,
229
+ project_root: Path | None = None,
230
+ ) -> NoReturn:
231
+ """Execute DRY linting."""
232
+ validate_paths_exist(path_objs)
233
+ orchestrator = _setup_dry_orchestrator(path_objs, config_file, verbose, project_root)
234
+
235
+ if config_file:
236
+ _load_dry_config_file(orchestrator, config_file, verbose)
237
+
238
+ _apply_dry_config_override(orchestrator, min_lines, no_cache, verbose)
239
+
240
+ if clear_cache:
241
+ _clear_dry_cache(orchestrator, verbose)
242
+
243
+ dry_violations = _run_dry_lint(orchestrator, path_objs, recursive, parallel)
244
+
245
+ if verbose:
246
+ logger.info(f"Found {len(dry_violations)} DRY violation(s)")
247
+
248
+ format_violations(dry_violations, format)
249
+ sys.exit(1 if dry_violations else 0)
250
+
251
+
252
+ # =============================================================================
253
+ # Magic Numbers Command
254
+ # =============================================================================
255
+
256
+
257
+ def _setup_magic_numbers_orchestrator(
258
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
259
+ ) -> "Orchestrator":
260
+ """Set up orchestrator for magic-numbers command."""
261
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
262
+
263
+
264
+ def _run_magic_numbers_lint(
265
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
266
+ ) -> list[Violation]:
267
+ """Execute magic-numbers lint on files or directories."""
268
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
269
+ return [v for v in all_violations if "magic-number" in v.rule_id]
270
+
271
+
272
+ def _execute_magic_numbers_lint(params: ExecuteParams) -> NoReturn:
273
+ """Execute magic-numbers lint."""
274
+ validate_paths_exist(params.path_objs)
275
+ orchestrator = _setup_magic_numbers_orchestrator(
276
+ params.path_objs, params.config_file, params.verbose, params.project_root
277
+ )
278
+ magic_numbers_violations = _run_magic_numbers_lint(
279
+ orchestrator, params.path_objs, params.recursive, params.parallel
280
+ )
281
+
282
+ if params.verbose:
283
+ logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
284
+
285
+ format_violations(magic_numbers_violations, params.format)
286
+ sys.exit(1 if magic_numbers_violations else 0)
287
+
288
+
289
+ magic_numbers = create_linter_command(
290
+ "magic-numbers",
291
+ _execute_magic_numbers_lint,
292
+ "Check for magic numbers in code.",
293
+ "Detects unnamed numeric literals in Python and TypeScript/JavaScript code\n"
294
+ " that should be extracted as named constants for better readability.",
295
+ )
296
+
297
+
298
+ # =============================================================================
299
+ # Stringly-Typed Command
300
+ # =============================================================================
301
+
302
+
303
+ def _setup_stringly_typed_orchestrator(
304
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
305
+ ) -> "Orchestrator":
306
+ """Set up orchestrator for stringly-typed command."""
307
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
308
+
309
+
310
+ def _run_stringly_typed_lint(
311
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
312
+ ) -> list[Violation]:
313
+ """Execute stringly-typed lint on files or directories."""
314
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
315
+ return [v for v in all_violations if "stringly-typed" in v.rule_id]
316
+
317
+
318
+ def _execute_stringly_typed_lint(params: ExecuteParams) -> NoReturn:
319
+ """Execute stringly-typed lint."""
320
+ validate_paths_exist(params.path_objs)
321
+ orchestrator = _setup_stringly_typed_orchestrator(
322
+ params.path_objs, params.config_file, params.verbose, params.project_root
323
+ )
324
+ stringly_violations = _run_stringly_typed_lint(
325
+ orchestrator, params.path_objs, params.recursive, params.parallel
326
+ )
327
+
328
+ if params.verbose:
329
+ logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
330
+
331
+ format_violations(stringly_violations, params.format)
332
+ sys.exit(1 if stringly_violations else 0)
333
+
334
+
335
+ stringly_typed = create_linter_command(
336
+ "stringly-typed",
337
+ _execute_stringly_typed_lint,
338
+ "Check for stringly-typed patterns in code.",
339
+ "Detects string patterns in Python and TypeScript/JavaScript code that should\n"
340
+ " use enums or typed alternatives. Finds membership validation, equality chains,\n"
341
+ " and function calls with limited string values across multiple files.",
342
+ )
@@ -0,0 +1,83 @@
1
+ """
2
+ Purpose: CLI commands for documentation linters (file-header)
3
+
4
+ Scope: Commands that validate documentation standards in source files
5
+
6
+ Overview: Provides CLI commands for documentation linting: file-header validates that source files
7
+ have proper documentation headers with required fields (Purpose, Scope, Overview, etc.) and
8
+ detects temporal language patterns (dates, temporal qualifiers, state change references).
9
+ Supports Python, TypeScript, JavaScript, Bash, Markdown, and CSS files. Integrates with the
10
+ orchestrator for execution.
11
+
12
+ Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
13
+
14
+ Exports: file_header 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
+
21
+ import logging
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import TYPE_CHECKING, NoReturn
25
+
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
28
+ from src.core.cli_utils import format_violations
29
+ from src.core.types import Violation
30
+
31
+ if TYPE_CHECKING:
32
+ from src.orchestrator.core import Orchestrator
33
+
34
+ # Configure module logger
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # =============================================================================
39
+ # File Header Command
40
+ # =============================================================================
41
+
42
+
43
+ def _setup_file_header_orchestrator(
44
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
45
+ ) -> "Orchestrator":
46
+ """Set up orchestrator for file-header command."""
47
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
48
+
49
+
50
+ def _run_file_header_lint(
51
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
52
+ ) -> list[Violation]:
53
+ """Execute file-header lint on files or directories."""
54
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
55
+ return [v for v in all_violations if "file-header" in v.rule_id]
56
+
57
+
58
+ def _execute_file_header_lint(params: ExecuteParams) -> NoReturn:
59
+ """Execute file-header lint."""
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(
65
+ orchestrator, params.path_objs, params.recursive, params.parallel
66
+ )
67
+
68
+ if params.verbose:
69
+ logger.info(f"Found {len(file_header_violations)} file header violation(s)")
70
+
71
+ format_violations(file_header_violations, params.format)
72
+ sys.exit(1 if file_header_violations else 0)
73
+
74
+
75
+ file_header = create_linter_command(
76
+ "file-header",
77
+ _execute_file_header_lint,
78
+ "Check file headers for mandatory fields and atemporal language.",
79
+ "Validates that source files have proper documentation headers containing\n"
80
+ " required fields (Purpose, Scope, Overview, etc.) and don't use temporal\n"
81
+ " language (dates, 'currently', 'now', etc.). Supports Python, TypeScript,\n"
82
+ " JavaScript, Bash, Markdown, and CSS files.",
83
+ )