thailint 0.9.0__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/cli/__init__.py +27 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +478 -0
- src/cli/linters/__init__.py +58 -0
- src/cli/linters/code_patterns.py +372 -0
- src/cli/linters/code_smells.py +343 -0
- src/cli/linters/documentation.py +155 -0
- src/cli/linters/shared.py +89 -0
- src/cli/linters/structure.py +313 -0
- src/cli/linters/structure_quality.py +316 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +375 -0
- src/cli_main.py +34 -0
- src/config.py +2 -3
- src/core/rule_discovery.py +43 -10
- src/core/types.py +13 -0
- src/core/violation_utils.py +69 -0
- src/linter_config/ignore.py +32 -16
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/config.py +63 -0
- src/linters/collection_pipeline/continue_analyzer.py +100 -0
- src/linters/collection_pipeline/detector.py +130 -0
- src/linters/collection_pipeline/linter.py +437 -0
- src/linters/collection_pipeline/suggestion_builder.py +63 -0
- src/linters/dry/block_filter.py +99 -9
- src/linters/dry/cache.py +94 -6
- src/linters/dry/config.py +47 -10
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +214 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/linter.py +89 -48
- src/linters/dry/python_analyzer.py +44 -431
- src/linters/dry/python_constant_extractor.py +101 -0
- src/linters/dry/single_statement_detector.py +415 -0
- src/linters/dry/token_hasher.py +5 -5
- src/linters/dry/typescript_analyzer.py +63 -382
- src/linters/dry/typescript_constant_extractor.py +134 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +66 -0
- src/linters/file_header/linter.py +9 -13
- src/linters/file_placement/linter.py +30 -10
- src/linters/file_placement/pattern_matcher.py +19 -5
- src/linters/magic_numbers/linter.py +8 -67
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/nesting/linter.py +12 -9
- src/linters/print_statements/linter.py +7 -24
- src/linters/srp/class_analyzer.py +9 -9
- src/linters/srp/heuristics.py +6 -5
- src/linters/srp/linter.py +4 -5
- src/linters/stateless_class/linter.py +2 -2
- src/linters/stringly_typed/__init__.py +23 -0
- src/linters/stringly_typed/config.py +165 -0
- src/linters/stringly_typed/python/__init__.py +29 -0
- src/linters/stringly_typed/python/analyzer.py +198 -0
- src/linters/stringly_typed/python/condition_extractor.py +131 -0
- src/linters/stringly_typed/python/conditional_detector.py +176 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +88 -0
- src/linters/stringly_typed/python/validation_detector.py +186 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/orchestrator/core.py +241 -12
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/METADATA +116 -3
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/RECORD +67 -29
- thailint-0.11.0.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -2014
- thailint-0.9.0.dist-info/entry_points.txt +0 -4
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for code smell linters (dry, magic-numbers)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect code smells like duplicate code and magic numbers
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for code smell detection: dry finds duplicate code blocks using
|
|
7
|
+
token-based hashing with SQLite caching, and magic-numbers detects unnamed numeric literals that
|
|
8
|
+
should be extracted as named constants. Each command supports standard options (config, format,
|
|
9
|
+
recursive) plus linter-specific options (min-lines, no-cache, clear-cache) and integrates with
|
|
10
|
+
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
|
|
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)
|
|
@@ -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)]
|