thailint 0.10.0__py3-none-any.whl → 0.12.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 (76) hide show
  1. src/__init__.py +1 -0
  2. src/cli/__init__.py +27 -0
  3. src/cli/__main__.py +22 -0
  4. src/cli/config.py +478 -0
  5. src/cli/linters/__init__.py +58 -0
  6. src/cli/linters/code_patterns.py +372 -0
  7. src/cli/linters/code_smells.py +450 -0
  8. src/cli/linters/documentation.py +155 -0
  9. src/cli/linters/shared.py +89 -0
  10. src/cli/linters/structure.py +313 -0
  11. src/cli/linters/structure_quality.py +316 -0
  12. src/cli/main.py +120 -0
  13. src/cli/utils.py +395 -0
  14. src/cli_main.py +34 -0
  15. src/core/types.py +13 -0
  16. src/core/violation_utils.py +69 -0
  17. src/linter_config/ignore.py +32 -16
  18. src/linters/collection_pipeline/linter.py +2 -2
  19. src/linters/dry/block_filter.py +97 -1
  20. src/linters/dry/cache.py +94 -6
  21. src/linters/dry/config.py +47 -10
  22. src/linters/dry/constant.py +92 -0
  23. src/linters/dry/constant_matcher.py +214 -0
  24. src/linters/dry/constant_violation_builder.py +98 -0
  25. src/linters/dry/linter.py +89 -48
  26. src/linters/dry/python_analyzer.py +12 -415
  27. src/linters/dry/python_constant_extractor.py +101 -0
  28. src/linters/dry/single_statement_detector.py +415 -0
  29. src/linters/dry/token_hasher.py +5 -5
  30. src/linters/dry/typescript_analyzer.py +5 -354
  31. src/linters/dry/typescript_constant_extractor.py +134 -0
  32. src/linters/dry/typescript_statement_detector.py +255 -0
  33. src/linters/dry/typescript_value_extractor.py +66 -0
  34. src/linters/file_header/linter.py +2 -2
  35. src/linters/file_placement/linter.py +2 -2
  36. src/linters/file_placement/pattern_matcher.py +19 -5
  37. src/linters/magic_numbers/linter.py +8 -67
  38. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  39. src/linters/nesting/linter.py +12 -9
  40. src/linters/print_statements/linter.py +7 -24
  41. src/linters/srp/class_analyzer.py +9 -9
  42. src/linters/srp/heuristics.py +2 -2
  43. src/linters/srp/linter.py +2 -2
  44. src/linters/stateless_class/linter.py +2 -2
  45. src/linters/stringly_typed/__init__.py +36 -0
  46. src/linters/stringly_typed/config.py +190 -0
  47. src/linters/stringly_typed/context_filter.py +451 -0
  48. src/linters/stringly_typed/function_call_violation_builder.py +137 -0
  49. src/linters/stringly_typed/ignore_checker.py +102 -0
  50. src/linters/stringly_typed/ignore_utils.py +51 -0
  51. src/linters/stringly_typed/linter.py +344 -0
  52. src/linters/stringly_typed/python/__init__.py +33 -0
  53. src/linters/stringly_typed/python/analyzer.py +344 -0
  54. src/linters/stringly_typed/python/call_tracker.py +172 -0
  55. src/linters/stringly_typed/python/comparison_tracker.py +252 -0
  56. src/linters/stringly_typed/python/condition_extractor.py +131 -0
  57. src/linters/stringly_typed/python/conditional_detector.py +176 -0
  58. src/linters/stringly_typed/python/constants.py +21 -0
  59. src/linters/stringly_typed/python/match_analyzer.py +88 -0
  60. src/linters/stringly_typed/python/validation_detector.py +186 -0
  61. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  62. src/linters/stringly_typed/storage.py +630 -0
  63. src/linters/stringly_typed/storage_initializer.py +45 -0
  64. src/linters/stringly_typed/typescript/__init__.py +28 -0
  65. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  66. src/linters/stringly_typed/typescript/call_tracker.py +329 -0
  67. src/linters/stringly_typed/typescript/comparison_tracker.py +372 -0
  68. src/linters/stringly_typed/violation_generator.py +376 -0
  69. src/orchestrator/core.py +241 -12
  70. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/METADATA +9 -3
  71. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/RECORD +74 -28
  72. thailint-0.12.0.dist-info/entry_points.txt +4 -0
  73. src/cli.py +0 -2141
  74. thailint-0.10.0.dist-info/entry_points.txt +0 -4
  75. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/WHEEL +0 -0
  76. {thailint-0.10.0.dist-info → thailint-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,450 @@
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
+ SRP Exception: CLI command modules follow Click framework patterns requiring similar command
22
+ structure across all linter commands. This is intentional design for consistency.
23
+ """
24
+ # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
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 ensure_config_section, set_config_value
35
+ from src.cli.main import cli
36
+ from src.cli.utils import (
37
+ execute_linting_on_paths,
38
+ format_option,
39
+ get_project_root_from_context,
40
+ handle_linting_error,
41
+ setup_base_orchestrator,
42
+ validate_paths_exist,
43
+ )
44
+ from src.core.cli_utils import format_violations
45
+ from src.core.types import Violation
46
+
47
+ if TYPE_CHECKING:
48
+ from src.orchestrator.core import Orchestrator
49
+
50
+ # Configure module logger
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ # =============================================================================
55
+ # DRY Command
56
+ # =============================================================================
57
+
58
+
59
+ def _setup_dry_orchestrator(
60
+ path_objs: list[Path],
61
+ config_file: str | None,
62
+ verbose: bool,
63
+ project_root: Path | None = None,
64
+ ) -> "Orchestrator":
65
+ """Set up orchestrator for DRY linting."""
66
+ return setup_base_orchestrator(path_objs, None, verbose, project_root)
67
+
68
+
69
+ def _load_dry_config_file(orchestrator: "Orchestrator", config_file: str, verbose: bool) -> None:
70
+ """Load DRY configuration from file."""
71
+ config_path = Path(config_file)
72
+ if not config_path.exists():
73
+ click.echo(f"Error: Config file not found: {config_file}", err=True)
74
+ sys.exit(2)
75
+
76
+ with config_path.open("r", encoding="utf-8") as f:
77
+ config: dict[str, Any] = yaml.safe_load(f)
78
+
79
+ if "dry" in config:
80
+ orchestrator.config.update({"dry": config["dry"]})
81
+
82
+ if verbose:
83
+ logger.info(f"Loaded DRY config from {config_file}")
84
+
85
+
86
+ def _apply_dry_config_override(
87
+ orchestrator: "Orchestrator", min_lines: int | None, no_cache: bool, verbose: bool
88
+ ) -> None:
89
+ """Apply CLI option overrides to DRY config."""
90
+ dry_config = ensure_config_section(orchestrator, "dry")
91
+ set_config_value(dry_config, "min_duplicate_lines", min_lines, verbose)
92
+ if no_cache:
93
+ set_config_value(dry_config, "cache_enabled", False, verbose)
94
+
95
+
96
+ def _clear_dry_cache(orchestrator: "Orchestrator", verbose: bool) -> None:
97
+ """Clear DRY cache before running."""
98
+ cache_path_str = orchestrator.config.get("dry", {}).get("cache_path", ".thailint-cache/dry.db")
99
+ cache_path = orchestrator.project_root / cache_path_str
100
+
101
+ if cache_path.exists():
102
+ cache_path.unlink()
103
+ if verbose:
104
+ logger.info(f"Cleared cache: {cache_path}")
105
+ elif verbose:
106
+ logger.info("Cache file does not exist, nothing to clear")
107
+
108
+
109
+ def _run_dry_lint(
110
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
111
+ ) -> list[Violation]:
112
+ """Run DRY linting and return violations."""
113
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
114
+ return [v for v in all_violations if v.rule_id.startswith("dry.")]
115
+
116
+
117
+ @cli.command("dry")
118
+ @click.argument("paths", nargs=-1, type=click.Path())
119
+ @click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
120
+ @format_option
121
+ @click.option("--min-lines", type=int, help="Override min duplicate lines threshold")
122
+ @click.option("--no-cache", is_flag=True, help="Disable SQLite cache (force rehash)")
123
+ @click.option("--clear-cache", is_flag=True, help="Clear cache before running")
124
+ @click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
125
+ @click.pass_context
126
+ def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
127
+ ctx: click.Context,
128
+ paths: tuple[str, ...],
129
+ config_file: str | None,
130
+ format: str,
131
+ min_lines: int | None,
132
+ no_cache: bool,
133
+ clear_cache: bool,
134
+ recursive: bool,
135
+ ) -> None:
136
+ # Justification for Pylint disables:
137
+ # - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 6 options = 8 params
138
+ """
139
+ Check for duplicate code (DRY principle violations).
140
+
141
+ Detects duplicate code blocks across your project using token-based hashing
142
+ with SQLite caching for fast incremental scans.
143
+
144
+ PATHS: Files or directories to lint (defaults to current directory if none provided)
145
+
146
+ Examples:
147
+
148
+ \b
149
+ # Check current directory (all files recursively)
150
+ thai-lint dry
151
+
152
+ \b
153
+ # Check specific directory
154
+ thai-lint dry src/
155
+
156
+ \b
157
+ # Check single file
158
+ thai-lint dry src/app.py
159
+
160
+ \b
161
+ # Check multiple files
162
+ thai-lint dry src/app.py src/service.py tests/test_app.py
163
+
164
+ \b
165
+ # Use custom config file
166
+ thai-lint dry --config .thailint.yaml src/
167
+
168
+ \b
169
+ # Override minimum duplicate lines threshold
170
+ thai-lint dry --min-lines 5 .
171
+
172
+ \b
173
+ # Disable cache (force re-analysis)
174
+ thai-lint dry --no-cache .
175
+
176
+ \b
177
+ # Clear cache before running
178
+ thai-lint dry --clear-cache .
179
+
180
+ \b
181
+ # Get JSON output
182
+ thai-lint dry --format json .
183
+ """
184
+ verbose: bool = ctx.obj.get("verbose", False)
185
+ project_root = get_project_root_from_context(ctx)
186
+
187
+ if not paths:
188
+ paths = (".",)
189
+
190
+ path_objs = [Path(p) for p in paths]
191
+
192
+ try:
193
+ _execute_dry_lint(
194
+ path_objs,
195
+ config_file,
196
+ format,
197
+ min_lines,
198
+ no_cache,
199
+ clear_cache,
200
+ recursive,
201
+ verbose,
202
+ project_root,
203
+ )
204
+ except Exception as e:
205
+ handle_linting_error(e, verbose)
206
+
207
+
208
+ def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
209
+ path_objs: list[Path],
210
+ config_file: str | None,
211
+ format: str,
212
+ min_lines: int | None,
213
+ no_cache: bool,
214
+ clear_cache: bool,
215
+ recursive: bool,
216
+ verbose: bool,
217
+ project_root: Path | None = None,
218
+ ) -> NoReturn:
219
+ """Execute DRY linting."""
220
+ validate_paths_exist(path_objs)
221
+ orchestrator = _setup_dry_orchestrator(path_objs, config_file, verbose, project_root)
222
+
223
+ if config_file:
224
+ _load_dry_config_file(orchestrator, config_file, verbose)
225
+
226
+ _apply_dry_config_override(orchestrator, min_lines, no_cache, verbose)
227
+
228
+ if clear_cache:
229
+ _clear_dry_cache(orchestrator, verbose)
230
+
231
+ dry_violations = _run_dry_lint(orchestrator, path_objs, recursive)
232
+
233
+ if verbose:
234
+ logger.info(f"Found {len(dry_violations)} DRY violation(s)")
235
+
236
+ format_violations(dry_violations, format)
237
+ sys.exit(1 if dry_violations else 0)
238
+
239
+
240
+ # =============================================================================
241
+ # Magic Numbers Command
242
+ # =============================================================================
243
+
244
+
245
+ def _setup_magic_numbers_orchestrator(
246
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
247
+ ) -> "Orchestrator":
248
+ """Set up orchestrator for magic-numbers command."""
249
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
250
+
251
+
252
+ def _run_magic_numbers_lint(
253
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
254
+ ) -> list[Violation]:
255
+ """Execute magic-numbers lint on files or directories."""
256
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
257
+ return [v for v in all_violations if "magic-number" in v.rule_id]
258
+
259
+
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:
334
+ """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:
340
+ logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
341
+
342
+ format_violations(magic_numbers_violations, format)
343
+ sys.exit(1 if magic_numbers_violations else 0)
344
+
345
+
346
+ # =============================================================================
347
+ # Stringly-Typed Command
348
+ # =============================================================================
349
+
350
+
351
+ def _setup_stringly_typed_orchestrator(
352
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
353
+ ) -> "Orchestrator":
354
+ """Set up orchestrator for stringly-typed command."""
355
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
356
+
357
+
358
+ def _run_stringly_typed_lint(
359
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
360
+ ) -> list[Violation]:
361
+ """Execute stringly-typed lint on files or directories."""
362
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
363
+ return [v for v in all_violations if "stringly-typed" in v.rule_id]
364
+
365
+
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:
441
+ """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)
445
+
446
+ if verbose:
447
+ logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
448
+
449
+ format_violations(stringly_violations, format)
450
+ sys.exit(1 if stringly_violations else 0)
@@ -0,0 +1,155 @@
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
+ SRP Exception: CLI command modules follow Click framework patterns requiring similar command
21
+ structure across all linter commands. This is intentional design for consistency.
22
+ """
23
+ # dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
24
+
25
+ import logging
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import TYPE_CHECKING, NoReturn
29
+
30
+ import click
31
+
32
+ from src.cli.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
+ )
41
+ from src.core.cli_utils import format_violations
42
+ from src.core.types import Violation
43
+
44
+ if TYPE_CHECKING:
45
+ from src.orchestrator.core import Orchestrator
46
+
47
+ # Configure module logger
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ # =============================================================================
52
+ # File Header Command
53
+ # =============================================================================
54
+
55
+
56
+ def _setup_file_header_orchestrator(
57
+ path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
58
+ ) -> "Orchestrator":
59
+ """Set up orchestrator for file-header command."""
60
+ return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
61
+
62
+
63
+ def _run_file_header_lint(
64
+ orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
65
+ ) -> list[Violation]:
66
+ """Execute file-header lint on files or directories."""
67
+ all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
68
+ return [v for v in all_violations if "file-header" in v.rule_id]
69
+
70
+
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:
146
+ """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)
150
+
151
+ if verbose:
152
+ logger.info(f"Found {len(file_header_violations)} file header violation(s)")
153
+
154
+ format_violations(file_header_violations, format)
155
+ sys.exit(1 if file_header_violations else 0)
@@ -0,0 +1,89 @@
1
+ """
2
+ Purpose: Shared utilities for linter CLI commands
3
+
4
+ Scope: Common helper functions and patterns used across all linter command modules
5
+
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.
10
+
11
+ Dependencies: logging for debug output, pathlib for Path type hints
12
+
13
+ Exports: ensure_config_section, set_config_value, filter_violations_by_prefix
14
+
15
+ Interfaces: Orchestrator config dict manipulation, violation list filtering
16
+
17
+ Implementation: Pure helper functions with no side effects beyond config mutation and logging
18
+ """
19
+
20
+ import logging
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from src.core.types import Violation
24
+
25
+ if TYPE_CHECKING:
26
+ from src.orchestrator.core import Orchestrator
27
+
28
+ # Configure module logger
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def ensure_config_section(orchestrator: "Orchestrator", section: str) -> dict[str, Any]:
33
+ """Ensure a config section exists and return it.
34
+
35
+ Args:
36
+ orchestrator: Orchestrator instance with config dict
37
+ section: Name of the config section to ensure exists
38
+
39
+ Returns:
40
+ The config section dict (created if it didn't exist)
41
+ """
42
+ if section not in orchestrator.config:
43
+ orchestrator.config[section] = {}
44
+ config_section: dict[str, Any] = orchestrator.config[section]
45
+ return config_section
46
+
47
+
48
+ def set_config_value(config: dict[str, Any], key: str, value: Any, verbose: bool) -> None:
49
+ """Set a config value with optional debug logging.
50
+
51
+ Only sets the value if it is not None.
52
+
53
+ Args:
54
+ config: Config dict to update
55
+ key: Config key to set
56
+ value: Value to set (skipped if None)
57
+ verbose: Whether to log the override
58
+ """
59
+ if value is None:
60
+ return
61
+ config[key] = value
62
+ if verbose:
63
+ logger.debug(f"Overriding {key} to {value}")
64
+
65
+
66
+ def filter_violations_by_prefix(violations: list[Violation], prefix: str) -> list[Violation]:
67
+ """Filter violations to those matching a rule ID prefix.
68
+
69
+ Args:
70
+ violations: List of violation objects with rule_id attribute
71
+ prefix: Prefix to match against rule_id
72
+
73
+ Returns:
74
+ Filtered list of violations where rule_id contains the prefix
75
+ """
76
+ return [v for v in violations if prefix in v.rule_id]
77
+
78
+
79
+ def filter_violations_by_startswith(violations: list[Violation], prefix: str) -> list[Violation]:
80
+ """Filter violations to those with rule_id starting with prefix.
81
+
82
+ Args:
83
+ violations: List of violation objects with rule_id attribute
84
+ prefix: Prefix that rule_id must start with
85
+
86
+ Returns:
87
+ Filtered list of violations where rule_id starts with the prefix
88
+ """
89
+ return [v for v in violations if v.rule_id.startswith(prefix)]