thailint 0.1.5__py3-none-any.whl → 0.5.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 +7 -2
- src/analyzers/__init__.py +23 -0
- src/analyzers/typescript_base.py +148 -0
- src/api.py +1 -1
- src/cli.py +1111 -144
- src/config.py +12 -33
- src/core/base.py +102 -5
- src/core/cli_utils.py +206 -0
- src/core/config_parser.py +126 -0
- src/core/linter_utils.py +168 -0
- src/core/registry.py +17 -92
- src/core/rule_discovery.py +132 -0
- src/core/violation_builder.py +122 -0
- src/linter_config/ignore.py +112 -40
- src/linter_config/loader.py +3 -13
- src/linters/dry/__init__.py +23 -0
- src/linters/dry/base_token_analyzer.py +76 -0
- src/linters/dry/block_filter.py +265 -0
- src/linters/dry/block_grouper.py +59 -0
- src/linters/dry/cache.py +172 -0
- src/linters/dry/cache_query.py +61 -0
- src/linters/dry/config.py +134 -0
- src/linters/dry/config_loader.py +44 -0
- src/linters/dry/deduplicator.py +120 -0
- src/linters/dry/duplicate_storage.py +63 -0
- src/linters/dry/file_analyzer.py +90 -0
- src/linters/dry/inline_ignore.py +140 -0
- src/linters/dry/linter.py +163 -0
- src/linters/dry/python_analyzer.py +668 -0
- src/linters/dry/storage_initializer.py +42 -0
- src/linters/dry/token_hasher.py +169 -0
- src/linters/dry/typescript_analyzer.py +592 -0
- src/linters/dry/violation_builder.py +74 -0
- src/linters/dry/violation_filter.py +94 -0
- src/linters/dry/violation_generator.py +174 -0
- src/linters/file_header/__init__.py +24 -0
- src/linters/file_header/atemporal_detector.py +87 -0
- src/linters/file_header/config.py +66 -0
- src/linters/file_header/field_validator.py +69 -0
- src/linters/file_header/linter.py +313 -0
- src/linters/file_header/python_parser.py +86 -0
- src/linters/file_header/violation_builder.py +78 -0
- src/linters/file_placement/config_loader.py +86 -0
- src/linters/file_placement/directory_matcher.py +80 -0
- src/linters/file_placement/linter.py +262 -471
- src/linters/file_placement/path_resolver.py +61 -0
- src/linters/file_placement/pattern_matcher.py +55 -0
- src/linters/file_placement/pattern_validator.py +106 -0
- src/linters/file_placement/rule_checker.py +229 -0
- src/linters/file_placement/violation_factory.py +177 -0
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +82 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +516 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +218 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +17 -4
- src/linters/nesting/linter.py +81 -168
- src/linters/nesting/typescript_analyzer.py +39 -102
- src/linters/nesting/typescript_function_extractor.py +130 -0
- src/linters/nesting/violation_builder.py +139 -0
- src/linters/print_statements/__init__.py +53 -0
- src/linters/print_statements/config.py +83 -0
- src/linters/print_statements/linter.py +430 -0
- src/linters/print_statements/python_analyzer.py +155 -0
- src/linters/print_statements/typescript_analyzer.py +135 -0
- src/linters/print_statements/violation_builder.py +98 -0
- src/linters/srp/__init__.py +99 -0
- src/linters/srp/class_analyzer.py +113 -0
- src/linters/srp/config.py +82 -0
- src/linters/srp/heuristics.py +89 -0
- src/linters/srp/linter.py +234 -0
- src/linters/srp/metrics_evaluator.py +47 -0
- src/linters/srp/python_analyzer.py +72 -0
- src/linters/srp/typescript_analyzer.py +75 -0
- src/linters/srp/typescript_metrics_calculator.py +90 -0
- src/linters/srp/violation_builder.py +117 -0
- src/orchestrator/core.py +54 -9
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/__init__.py +4 -0
- src/utils/project_root.py +203 -0
- thailint-0.5.0.dist-info/METADATA +1286 -0
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- src/.ai/layout.yaml +0 -48
- thailint-0.1.5.dist-info/METADATA +0 -629
- thailint-0.1.5.dist-info/RECORD +0 -28
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.1.5.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
src/cli.py
CHANGED
|
@@ -11,12 +11,14 @@ Overview: Provides the main CLI application using Click decorators for command d
|
|
|
11
11
|
|
|
12
12
|
Dependencies: click for CLI framework, logging for structured output, pathlib for file paths
|
|
13
13
|
|
|
14
|
-
Exports: cli (main command group), hello command, config command group, file_placement command
|
|
14
|
+
Exports: cli (main command group), hello command, config command group, file_placement command, dry command
|
|
15
15
|
|
|
16
16
|
Interfaces: Click CLI commands, configuration context via Click ctx, logging integration
|
|
17
17
|
|
|
18
18
|
Implementation: Click decorators for commands, context passing for shared state, comprehensive help text
|
|
19
19
|
"""
|
|
20
|
+
# pylint: disable=too-many-lines
|
|
21
|
+
# Justification: CLI modules naturally have many commands and helper functions
|
|
20
22
|
|
|
21
23
|
import logging
|
|
22
24
|
import sys
|
|
@@ -26,11 +28,20 @@ import click
|
|
|
26
28
|
|
|
27
29
|
from src import __version__
|
|
28
30
|
from src.config import ConfigError, load_config, save_config, validate_config
|
|
31
|
+
from src.core.cli_utils import format_violations
|
|
29
32
|
|
|
30
33
|
# Configure module logger
|
|
31
34
|
logger = logging.getLogger(__name__)
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
# Shared Click option decorators for common CLI options
|
|
38
|
+
def format_option(func):
|
|
39
|
+
"""Add --format option to a command for output format selection."""
|
|
40
|
+
return click.option(
|
|
41
|
+
"--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
|
|
42
|
+
)(func)
|
|
43
|
+
|
|
44
|
+
|
|
34
45
|
def setup_logging(verbose: bool = False):
|
|
35
46
|
"""
|
|
36
47
|
Configure logging for the CLI application.
|
|
@@ -48,12 +59,145 @@ def setup_logging(verbose: bool = False):
|
|
|
48
59
|
)
|
|
49
60
|
|
|
50
61
|
|
|
62
|
+
def _determine_project_root(
|
|
63
|
+
explicit_root: str | None, config_path: str | None, verbose: bool
|
|
64
|
+
) -> Path:
|
|
65
|
+
"""Determine project root with precedence rules.
|
|
66
|
+
|
|
67
|
+
Precedence order:
|
|
68
|
+
1. Explicit --project-root (highest priority)
|
|
69
|
+
2. Inferred from --config path directory
|
|
70
|
+
3. Auto-detection via get_project_root() (fallback)
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
explicit_root: Explicitly specified project root path (from --project-root)
|
|
74
|
+
config_path: Config file path (from --config)
|
|
75
|
+
verbose: Whether verbose logging is enabled
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path to determined project root
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
SystemExit: If explicit_root doesn't exist or is not a directory
|
|
82
|
+
"""
|
|
83
|
+
from src.utils.project_root import get_project_root
|
|
84
|
+
|
|
85
|
+
# Priority 1: Explicit --project-root
|
|
86
|
+
if explicit_root:
|
|
87
|
+
return _resolve_explicit_project_root(explicit_root, verbose)
|
|
88
|
+
|
|
89
|
+
# Priority 2: Infer from --config path
|
|
90
|
+
if config_path:
|
|
91
|
+
return _infer_root_from_config(config_path, verbose)
|
|
92
|
+
|
|
93
|
+
# Priority 3: Auto-detection (fallback)
|
|
94
|
+
return _autodetect_project_root(verbose, get_project_root)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_explicit_project_root(explicit_root: str, verbose: bool) -> Path:
|
|
98
|
+
"""Resolve and validate explicitly specified project root.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
explicit_root: Explicitly specified project root path
|
|
102
|
+
verbose: Whether verbose logging is enabled
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Resolved project root path
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
SystemExit: If explicit_root doesn't exist or is not a directory
|
|
109
|
+
"""
|
|
110
|
+
root = Path(explicit_root)
|
|
111
|
+
# Check existence before resolving to handle relative paths in test environments
|
|
112
|
+
if not root.exists():
|
|
113
|
+
click.echo(f"Error: Project root does not exist: {explicit_root}", err=True)
|
|
114
|
+
sys.exit(2)
|
|
115
|
+
if not root.is_dir():
|
|
116
|
+
click.echo(f"Error: Project root must be a directory: {explicit_root}", err=True)
|
|
117
|
+
sys.exit(2)
|
|
118
|
+
|
|
119
|
+
# Now resolve after validation
|
|
120
|
+
root = root.resolve()
|
|
121
|
+
|
|
122
|
+
if verbose:
|
|
123
|
+
logger.debug(f"Using explicit project root: {root}")
|
|
124
|
+
return root
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _infer_root_from_config(config_path: str, verbose: bool) -> Path:
|
|
128
|
+
"""Infer project root from config file path.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
config_path: Config file path
|
|
132
|
+
verbose: Whether verbose logging is enabled
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Inferred project root (parent directory of config file)
|
|
136
|
+
"""
|
|
137
|
+
config_file = Path(config_path).resolve()
|
|
138
|
+
inferred_root = config_file.parent
|
|
139
|
+
|
|
140
|
+
if verbose:
|
|
141
|
+
logger.debug(f"Inferred project root from config path: {inferred_root}")
|
|
142
|
+
return inferred_root
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _autodetect_project_root(verbose: bool, get_project_root) -> Path:
|
|
146
|
+
"""Auto-detect project root using project root detection.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
verbose: Whether verbose logging is enabled
|
|
150
|
+
get_project_root: Function to detect project root
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Auto-detected project root
|
|
154
|
+
"""
|
|
155
|
+
auto_root = get_project_root(None)
|
|
156
|
+
if verbose:
|
|
157
|
+
logger.debug(f"Auto-detected project root: {auto_root}")
|
|
158
|
+
return auto_root
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _get_project_root_from_context(ctx) -> Path:
|
|
162
|
+
"""Get or determine project root from Click context.
|
|
163
|
+
|
|
164
|
+
This function defers the actual determination until needed to avoid
|
|
165
|
+
importing pyprojroot in test environments where it may not be available.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
ctx: Click context containing CLI options
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Path to determined project root
|
|
172
|
+
"""
|
|
173
|
+
# Check if already determined and cached
|
|
174
|
+
if "project_root" in ctx.obj:
|
|
175
|
+
return ctx.obj["project_root"]
|
|
176
|
+
|
|
177
|
+
# Determine project root using stored CLI options
|
|
178
|
+
explicit_root = ctx.obj.get("cli_project_root")
|
|
179
|
+
config_path = ctx.obj.get("cli_config_path")
|
|
180
|
+
verbose = ctx.obj.get("verbose", False)
|
|
181
|
+
|
|
182
|
+
project_root = _determine_project_root(explicit_root, config_path, verbose)
|
|
183
|
+
|
|
184
|
+
# Cache for future use
|
|
185
|
+
ctx.obj["project_root"] = project_root
|
|
186
|
+
|
|
187
|
+
return project_root
|
|
188
|
+
|
|
189
|
+
|
|
51
190
|
@click.group()
|
|
52
191
|
@click.version_option(version=__version__)
|
|
53
192
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
54
193
|
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
|
|
194
|
+
@click.option(
|
|
195
|
+
"--project-root",
|
|
196
|
+
type=click.Path(),
|
|
197
|
+
help="Explicitly specify project root directory (overrides auto-detection)",
|
|
198
|
+
)
|
|
55
199
|
@click.pass_context
|
|
56
|
-
def cli(ctx, verbose: bool, config: str | None):
|
|
200
|
+
def cli(ctx, verbose: bool, config: str | None, project_root: str | None):
|
|
57
201
|
"""
|
|
58
202
|
thai-lint - AI code linter and governance tool
|
|
59
203
|
|
|
@@ -62,6 +206,10 @@ def cli(ctx, verbose: bool, config: str | None):
|
|
|
62
206
|
|
|
63
207
|
Examples:
|
|
64
208
|
|
|
209
|
+
\b
|
|
210
|
+
# Check for duplicate code (DRY violations)
|
|
211
|
+
thai-lint dry .
|
|
212
|
+
|
|
65
213
|
\b
|
|
66
214
|
# Lint current directory for file placement issues
|
|
67
215
|
thai-lint file-placement .
|
|
@@ -70,6 +218,10 @@ def cli(ctx, verbose: bool, config: str | None):
|
|
|
70
218
|
# Lint with custom config
|
|
71
219
|
thai-lint file-placement --config .thailint.yaml src/
|
|
72
220
|
|
|
221
|
+
\b
|
|
222
|
+
# Specify project root explicitly (useful in Docker)
|
|
223
|
+
thai-lint --project-root /workspace/root magic-numbers backend/
|
|
224
|
+
|
|
73
225
|
\b
|
|
74
226
|
# Get JSON output
|
|
75
227
|
thai-lint file-placement --format json .
|
|
@@ -84,6 +236,11 @@ def cli(ctx, verbose: bool, config: str | None):
|
|
|
84
236
|
# Setup logging
|
|
85
237
|
setup_logging(verbose)
|
|
86
238
|
|
|
239
|
+
# Store CLI options for later project root determination
|
|
240
|
+
# (deferred to avoid pyprojroot import issues in test environments)
|
|
241
|
+
ctx.obj["cli_project_root"] = project_root
|
|
242
|
+
ctx.obj["cli_config_path"] = config
|
|
243
|
+
|
|
87
244
|
# Load configuration
|
|
88
245
|
try:
|
|
89
246
|
if config:
|
|
@@ -347,17 +504,155 @@ def config_reset(ctx, yes: bool):
|
|
|
347
504
|
sys.exit(1)
|
|
348
505
|
|
|
349
506
|
|
|
507
|
+
@cli.command("init-config")
|
|
508
|
+
@click.option(
|
|
509
|
+
"--preset",
|
|
510
|
+
"-p",
|
|
511
|
+
type=click.Choice(["strict", "standard", "lenient"]),
|
|
512
|
+
default="standard",
|
|
513
|
+
help="Configuration preset",
|
|
514
|
+
)
|
|
515
|
+
@click.option("--non-interactive", is_flag=True, help="Skip interactive prompts (for AI agents)")
|
|
516
|
+
@click.option("--force", is_flag=True, help="Overwrite existing .thailint.yaml file")
|
|
517
|
+
@click.option(
|
|
518
|
+
"--output", "-o", type=click.Path(), default=".thailint.yaml", help="Output file path"
|
|
519
|
+
)
|
|
520
|
+
def init_config(preset: str, non_interactive: bool, force: bool, output: str):
|
|
521
|
+
"""
|
|
522
|
+
Generate a .thailint.yaml configuration file with preset values.
|
|
523
|
+
|
|
524
|
+
Creates a richly-commented configuration file with sensible defaults
|
|
525
|
+
and optional customizations for different strictness levels.
|
|
526
|
+
|
|
527
|
+
For AI agents, use --non-interactive mode:
|
|
528
|
+
thailint init-config --non-interactive --preset lenient
|
|
529
|
+
|
|
530
|
+
Presets:
|
|
531
|
+
strict: Minimal allowed numbers (only -1, 0, 1)
|
|
532
|
+
standard: Balanced defaults (includes 2, 3, 4, 5, 10, 100, 1000)
|
|
533
|
+
lenient: Includes time conversions (adds 60, 3600)
|
|
534
|
+
|
|
535
|
+
Examples:
|
|
536
|
+
|
|
537
|
+
\\b
|
|
538
|
+
# Interactive mode (default, for humans)
|
|
539
|
+
thailint init-config
|
|
540
|
+
|
|
541
|
+
\\b
|
|
542
|
+
# Non-interactive mode (for AI agents)
|
|
543
|
+
thailint init-config --non-interactive
|
|
544
|
+
|
|
545
|
+
\\b
|
|
546
|
+
# Generate with lenient preset
|
|
547
|
+
thailint init-config --preset lenient
|
|
548
|
+
|
|
549
|
+
\\b
|
|
550
|
+
# Overwrite existing config
|
|
551
|
+
thailint init-config --force
|
|
552
|
+
|
|
553
|
+
\\b
|
|
554
|
+
# Custom output path
|
|
555
|
+
thailint init-config --output my-config.yaml
|
|
556
|
+
"""
|
|
557
|
+
output_path = Path(output)
|
|
558
|
+
|
|
559
|
+
# Check if file exists (unless --force)
|
|
560
|
+
if output_path.exists() and not force:
|
|
561
|
+
click.echo(f"Error: {output} already exists", err=True)
|
|
562
|
+
click.echo("", err=True)
|
|
563
|
+
click.echo("Use --force to overwrite:", err=True)
|
|
564
|
+
click.echo(" thailint init-config --force", err=True)
|
|
565
|
+
sys.exit(1)
|
|
566
|
+
|
|
567
|
+
# Interactive mode: Ask user for preferences
|
|
568
|
+
if not non_interactive:
|
|
569
|
+
click.echo("thai-lint Configuration Generator")
|
|
570
|
+
click.echo("=" * 50)
|
|
571
|
+
click.echo("")
|
|
572
|
+
click.echo("This will create a .thailint.yaml configuration file.")
|
|
573
|
+
click.echo("For non-interactive mode (AI agents), use:")
|
|
574
|
+
click.echo(" thailint init-config --non-interactive")
|
|
575
|
+
click.echo("")
|
|
576
|
+
|
|
577
|
+
# Ask for preset
|
|
578
|
+
click.echo("Available presets:")
|
|
579
|
+
click.echo(" strict: Only -1, 0, 1 allowed (strictest)")
|
|
580
|
+
click.echo(" standard: -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000 (balanced)")
|
|
581
|
+
click.echo(" lenient: Includes time conversions 60, 3600 (most permissive)")
|
|
582
|
+
click.echo("")
|
|
583
|
+
|
|
584
|
+
preset = click.prompt(
|
|
585
|
+
"Choose preset", type=click.Choice(["strict", "standard", "lenient"]), default=preset
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Generate config based on preset
|
|
589
|
+
config_content = _generate_config_content(preset)
|
|
590
|
+
|
|
591
|
+
# Write config file
|
|
592
|
+
try:
|
|
593
|
+
output_path.write_text(config_content, encoding="utf-8")
|
|
594
|
+
click.echo("")
|
|
595
|
+
click.echo(f"✓ Created {output}")
|
|
596
|
+
click.echo(f"✓ Preset: {preset}")
|
|
597
|
+
click.echo("")
|
|
598
|
+
click.echo("Next steps:")
|
|
599
|
+
click.echo(f" 1. Review and customize {output}")
|
|
600
|
+
click.echo(" 2. Run: thailint magic-numbers .")
|
|
601
|
+
click.echo(" 3. See docs: https://github.com/your-org/thai-lint")
|
|
602
|
+
except OSError as e:
|
|
603
|
+
click.echo(f"Error writing config file: {e}", err=True)
|
|
604
|
+
sys.exit(1)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _generate_config_content(preset: str) -> str:
|
|
608
|
+
"""Generate config file content based on preset."""
|
|
609
|
+
# Preset configurations
|
|
610
|
+
presets = {
|
|
611
|
+
"strict": {
|
|
612
|
+
"allowed_numbers": "[-1, 0, 1]",
|
|
613
|
+
"max_small_integer": "3",
|
|
614
|
+
"description": "Strict (only universal values)",
|
|
615
|
+
},
|
|
616
|
+
"standard": {
|
|
617
|
+
"allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000]",
|
|
618
|
+
"max_small_integer": "10",
|
|
619
|
+
"description": "Standard (balanced defaults)",
|
|
620
|
+
},
|
|
621
|
+
"lenient": {
|
|
622
|
+
"allowed_numbers": "[-1, 0, 1, 2, 3, 4, 5, 10, 60, 100, 1000, 3600]",
|
|
623
|
+
"max_small_integer": "10",
|
|
624
|
+
"description": "Lenient (includes time conversions)",
|
|
625
|
+
},
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
config = presets[preset]
|
|
629
|
+
|
|
630
|
+
# Read template
|
|
631
|
+
template_path = Path(__file__).parent / "templates" / "thailint_config_template.yaml"
|
|
632
|
+
template = template_path.read_text(encoding="utf-8")
|
|
633
|
+
|
|
634
|
+
# Replace placeholders
|
|
635
|
+
content = template.replace("{{PRESET}}", config["description"])
|
|
636
|
+
content = content.replace("{{ALLOWED_NUMBERS}}", config["allowed_numbers"])
|
|
637
|
+
content = content.replace("{{MAX_SMALL_INTEGER}}", config["max_small_integer"])
|
|
638
|
+
|
|
639
|
+
return content
|
|
640
|
+
|
|
641
|
+
|
|
350
642
|
@cli.command("file-placement")
|
|
351
|
-
@click.argument("
|
|
643
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
352
644
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
353
645
|
@click.option("--rules", "-r", help="Inline JSON rules configuration")
|
|
354
|
-
@
|
|
355
|
-
"--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
|
|
356
|
-
)
|
|
646
|
+
@format_option
|
|
357
647
|
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
358
648
|
@click.pass_context
|
|
359
649
|
def file_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-statements
|
|
360
|
-
ctx,
|
|
650
|
+
ctx,
|
|
651
|
+
paths: tuple[str, ...],
|
|
652
|
+
config_file: str | None,
|
|
653
|
+
rules: str | None,
|
|
654
|
+
format: str,
|
|
655
|
+
recursive: bool,
|
|
361
656
|
):
|
|
362
657
|
# Justification for Pylint disables:
|
|
363
658
|
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 4 options = 6 params
|
|
@@ -369,18 +664,26 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
369
664
|
Checks that files are placed in appropriate directories according to
|
|
370
665
|
configured rules and patterns.
|
|
371
666
|
|
|
372
|
-
|
|
667
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
373
668
|
|
|
374
669
|
Examples:
|
|
375
670
|
|
|
376
671
|
\b
|
|
377
|
-
# Lint current directory
|
|
672
|
+
# Lint current directory (all files recursively)
|
|
378
673
|
thai-lint file-placement
|
|
379
674
|
|
|
380
675
|
\b
|
|
381
676
|
# Lint specific directory
|
|
382
677
|
thai-lint file-placement src/
|
|
383
678
|
|
|
679
|
+
\b
|
|
680
|
+
# Lint single file
|
|
681
|
+
thai-lint file-placement src/app.py
|
|
682
|
+
|
|
683
|
+
\b
|
|
684
|
+
# Lint multiple files
|
|
685
|
+
thai-lint file-placement src/app.py src/utils.py tests/test_app.py
|
|
686
|
+
|
|
384
687
|
\b
|
|
385
688
|
# Use custom config
|
|
386
689
|
thai-lint file-placement --config rules.json .
|
|
@@ -390,25 +693,36 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
390
693
|
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
391
694
|
"""
|
|
392
695
|
verbose = ctx.obj.get("verbose", False)
|
|
393
|
-
|
|
696
|
+
project_root = _get_project_root_from_context(ctx)
|
|
697
|
+
|
|
698
|
+
if not paths:
|
|
699
|
+
paths = (".",)
|
|
700
|
+
|
|
701
|
+
path_objs = [Path(p) for p in paths]
|
|
394
702
|
|
|
395
703
|
try:
|
|
396
|
-
_execute_file_placement_lint(
|
|
704
|
+
_execute_file_placement_lint(
|
|
705
|
+
path_objs, config_file, rules, format, recursive, verbose, project_root
|
|
706
|
+
)
|
|
397
707
|
except Exception as e:
|
|
398
708
|
_handle_linting_error(e, verbose)
|
|
399
709
|
|
|
400
710
|
|
|
401
711
|
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
402
|
-
|
|
712
|
+
path_objs, config_file, rules, format, recursive, verbose, project_root=None
|
|
403
713
|
):
|
|
404
714
|
"""Execute file placement linting."""
|
|
405
|
-
|
|
406
|
-
|
|
715
|
+
_validate_paths_exist(path_objs)
|
|
716
|
+
orchestrator = _setup_orchestrator(path_objs, config_file, rules, verbose, project_root)
|
|
717
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
718
|
+
|
|
719
|
+
# Filter to only file-placement violations
|
|
720
|
+
violations = [v for v in all_violations if v.rule_id.startswith("file-placement")]
|
|
407
721
|
|
|
408
722
|
if verbose:
|
|
409
723
|
logger.info(f"Found {len(violations)} violation(s)")
|
|
410
724
|
|
|
411
|
-
|
|
725
|
+
format_violations(violations, format)
|
|
412
726
|
sys.exit(1 if violations else 0)
|
|
413
727
|
|
|
414
728
|
|
|
@@ -420,26 +734,98 @@ def _handle_linting_error(error: Exception, verbose: bool) -> None:
|
|
|
420
734
|
sys.exit(2)
|
|
421
735
|
|
|
422
736
|
|
|
423
|
-
def
|
|
737
|
+
def _validate_paths_exist(path_objs: list[Path]) -> None:
|
|
738
|
+
"""Validate that all provided paths exist.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
path_objs: List of Path objects to validate
|
|
742
|
+
|
|
743
|
+
Raises:
|
|
744
|
+
SystemExit: If any path doesn't exist (exit code 2)
|
|
745
|
+
"""
|
|
746
|
+
for path in path_objs:
|
|
747
|
+
if not path.exists():
|
|
748
|
+
click.echo(f"Error: Path does not exist: {path}", err=True)
|
|
749
|
+
click.echo("", err=True)
|
|
750
|
+
click.echo(
|
|
751
|
+
"Hint: When using Docker, ensure paths are inside the mounted volume:", err=True
|
|
752
|
+
)
|
|
753
|
+
click.echo(
|
|
754
|
+
" docker run -v $(pwd):/data thailint <command> /data/your-file.py", err=True
|
|
755
|
+
)
|
|
756
|
+
sys.exit(2)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _find_project_root(start_path: Path) -> Path:
|
|
760
|
+
"""Find project root by looking for .git or pyproject.toml.
|
|
761
|
+
|
|
762
|
+
DEPRECATED: Use src.utils.project_root.get_project_root() instead.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
start_path: Directory to start searching from
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Path to project root, or start_path if no markers found
|
|
769
|
+
"""
|
|
770
|
+
from src.utils.project_root import get_project_root
|
|
771
|
+
|
|
772
|
+
return get_project_root(start_path)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _setup_orchestrator(path_objs, config_file, rules, verbose, project_root=None):
|
|
424
776
|
"""Set up and configure the orchestrator."""
|
|
425
777
|
from src.orchestrator.core import Orchestrator
|
|
778
|
+
from src.utils.project_root import get_project_root
|
|
779
|
+
|
|
780
|
+
# Use provided project_root or fall back to auto-detection
|
|
781
|
+
project_root = _get_or_detect_project_root(path_objs, project_root, get_project_root)
|
|
426
782
|
|
|
427
|
-
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
428
783
|
orchestrator = Orchestrator(project_root=project_root)
|
|
784
|
+
_apply_orchestrator_config(orchestrator, config_file, rules, verbose)
|
|
785
|
+
|
|
786
|
+
return orchestrator
|
|
787
|
+
|
|
429
788
|
|
|
789
|
+
def _get_or_detect_project_root(path_objs, project_root, get_project_root):
|
|
790
|
+
"""Get provided project root or auto-detect from paths.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
path_objs: List of path objects
|
|
794
|
+
project_root: Optionally provided project root
|
|
795
|
+
get_project_root: Function to detect project root
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Project root path
|
|
799
|
+
"""
|
|
800
|
+
if project_root is not None:
|
|
801
|
+
return project_root
|
|
802
|
+
|
|
803
|
+
# Find actual project root (where .git or pyproject.toml exists)
|
|
804
|
+
# This ensures .artifacts/ is always created at project root, not in subdirectories
|
|
805
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
806
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
807
|
+
return get_project_root(search_start)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _apply_orchestrator_config(orchestrator, config_file, rules, verbose):
|
|
811
|
+
"""Apply configuration to orchestrator.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
orchestrator: Orchestrator instance
|
|
815
|
+
config_file: Path to config file (optional)
|
|
816
|
+
rules: Inline JSON rules (optional)
|
|
817
|
+
verbose: Whether verbose logging is enabled
|
|
818
|
+
"""
|
|
430
819
|
if rules:
|
|
431
820
|
_apply_inline_rules(orchestrator, rules, verbose)
|
|
432
821
|
elif config_file:
|
|
433
822
|
_load_config_file(orchestrator, config_file, verbose)
|
|
434
823
|
|
|
435
|
-
return orchestrator
|
|
436
|
-
|
|
437
824
|
|
|
438
825
|
def _apply_inline_rules(orchestrator, rules, verbose):
|
|
439
826
|
"""Parse and apply inline JSON rules."""
|
|
440
827
|
rules_config = _parse_json_rules(rules)
|
|
441
828
|
orchestrator.config.update(rules_config)
|
|
442
|
-
_write_layout_config(orchestrator, rules_config, verbose)
|
|
443
829
|
_log_applied_rules(rules_config, verbose)
|
|
444
830
|
|
|
445
831
|
|
|
@@ -454,40 +840,6 @@ def _parse_json_rules(rules: str) -> dict:
|
|
|
454
840
|
sys.exit(2)
|
|
455
841
|
|
|
456
842
|
|
|
457
|
-
def _write_layout_config(orchestrator, rules_config: dict, verbose: bool) -> None:
|
|
458
|
-
"""Write layout config to .ai/layout.yaml if possible."""
|
|
459
|
-
ai_dir = orchestrator.project_root / ".ai"
|
|
460
|
-
layout_file = ai_dir / "layout.yaml"
|
|
461
|
-
|
|
462
|
-
try:
|
|
463
|
-
_write_layout_yaml_file(ai_dir, layout_file, rules_config)
|
|
464
|
-
_log_layout_written(layout_file, verbose)
|
|
465
|
-
except OSError as e:
|
|
466
|
-
_log_layout_error(e, verbose)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def _write_layout_yaml_file(ai_dir, layout_file, rules_config):
|
|
470
|
-
"""Write layout YAML file."""
|
|
471
|
-
import yaml
|
|
472
|
-
|
|
473
|
-
ai_dir.mkdir(exist_ok=True)
|
|
474
|
-
layout_config = {"file-placement": rules_config}
|
|
475
|
-
with layout_file.open("w", encoding="utf-8") as f:
|
|
476
|
-
yaml.dump(layout_config, f)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
def _log_layout_written(layout_file, verbose):
|
|
480
|
-
"""Log layout file written."""
|
|
481
|
-
if verbose:
|
|
482
|
-
logger.debug(f"Written layout config to: {layout_file}")
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def _log_layout_error(error, verbose):
|
|
486
|
-
"""Log layout write error."""
|
|
487
|
-
if verbose:
|
|
488
|
-
logger.debug(f"Could not write layout config: {error}")
|
|
489
|
-
|
|
490
|
-
|
|
491
843
|
def _log_applied_rules(rules_config: dict, verbose: bool) -> None:
|
|
492
844
|
"""Log applied rules if verbose."""
|
|
493
845
|
if verbose:
|
|
@@ -504,99 +856,95 @@ def _load_config_file(orchestrator, config_file, verbose):
|
|
|
504
856
|
# Load config into orchestrator
|
|
505
857
|
orchestrator.config = orchestrator.config_loader.load(config_path)
|
|
506
858
|
|
|
507
|
-
|
|
508
|
-
|
|
859
|
+
if verbose:
|
|
860
|
+
logger.debug(f"Loaded config from: {config_file}")
|
|
509
861
|
|
|
510
862
|
|
|
511
|
-
def
|
|
512
|
-
"""
|
|
513
|
-
|
|
514
|
-
|
|
863
|
+
def _execute_linting(orchestrator, path_obj, recursive):
|
|
864
|
+
"""Execute linting on file or directory."""
|
|
865
|
+
if path_obj.is_file():
|
|
866
|
+
return orchestrator.lint_file(path_obj)
|
|
867
|
+
return orchestrator.lint_directory(path_obj, recursive=recursive)
|
|
515
868
|
|
|
516
|
-
try:
|
|
517
|
-
_write_config_yaml(ai_dir, layout_file, orchestrator.config)
|
|
518
|
-
_log_config_loaded(config_file, layout_file, verbose)
|
|
519
|
-
except OSError as e:
|
|
520
|
-
_log_layout_error(e, verbose)
|
|
521
869
|
|
|
870
|
+
def _separate_files_and_dirs(path_objs: list[Path]) -> tuple[list[Path], list[Path]]:
|
|
871
|
+
"""Separate file paths from directory paths.
|
|
522
872
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
import yaml
|
|
873
|
+
Args:
|
|
874
|
+
path_objs: List of Path objects
|
|
526
875
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
876
|
+
Returns:
|
|
877
|
+
Tuple of (files, directories)
|
|
878
|
+
"""
|
|
879
|
+
files = [p for p in path_objs if p.is_file()]
|
|
880
|
+
dirs = [p for p in path_objs if p.is_dir()]
|
|
881
|
+
return files, dirs
|
|
530
882
|
|
|
531
883
|
|
|
532
|
-
def
|
|
533
|
-
"""
|
|
534
|
-
if verbose:
|
|
535
|
-
logger.debug(f"Loaded config from: {config_file}")
|
|
536
|
-
logger.debug(f"Written layout config to: {layout_file}")
|
|
884
|
+
def _lint_files_if_any(orchestrator, files: list[Path]) -> list:
|
|
885
|
+
"""Lint files if list is non-empty.
|
|
537
886
|
|
|
887
|
+
Args:
|
|
888
|
+
orchestrator: Orchestrator instance
|
|
889
|
+
files: List of file paths
|
|
538
890
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
891
|
+
Returns:
|
|
892
|
+
List of violations
|
|
893
|
+
"""
|
|
894
|
+
if files:
|
|
895
|
+
return orchestrator.lint_files(files)
|
|
896
|
+
return []
|
|
544
897
|
|
|
545
898
|
|
|
546
|
-
def
|
|
547
|
-
"""
|
|
548
|
-
if format == "json":
|
|
549
|
-
_output_json(violations)
|
|
550
|
-
else:
|
|
551
|
-
_output_text(violations)
|
|
899
|
+
def _lint_directories(orchestrator, dirs: list[Path], recursive: bool) -> list:
|
|
900
|
+
"""Lint all directories.
|
|
552
901
|
|
|
902
|
+
Args:
|
|
903
|
+
orchestrator: Orchestrator instance
|
|
904
|
+
dirs: List of directory paths
|
|
905
|
+
recursive: Whether to scan recursively
|
|
553
906
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
907
|
+
Returns:
|
|
908
|
+
List of violations from all directories
|
|
909
|
+
"""
|
|
910
|
+
violations = []
|
|
911
|
+
for dir_path in dirs:
|
|
912
|
+
violations.extend(orchestrator.lint_directory(dir_path, recursive=recursive))
|
|
913
|
+
return violations
|
|
557
914
|
|
|
558
|
-
output = {
|
|
559
|
-
"violations": [
|
|
560
|
-
{
|
|
561
|
-
"rule_id": v.rule_id,
|
|
562
|
-
"file_path": str(v.file_path),
|
|
563
|
-
"line": v.line,
|
|
564
|
-
"column": v.column,
|
|
565
|
-
"message": v.message,
|
|
566
|
-
"severity": v.severity.name,
|
|
567
|
-
}
|
|
568
|
-
for v in violations
|
|
569
|
-
],
|
|
570
|
-
"total": len(violations),
|
|
571
|
-
}
|
|
572
|
-
click.echo(json.dumps(output, indent=2))
|
|
573
915
|
|
|
916
|
+
def _execute_linting_on_paths(orchestrator, path_objs: list[Path], recursive: bool) -> list:
|
|
917
|
+
"""Execute linting on list of file/directory paths.
|
|
574
918
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return
|
|
919
|
+
Args:
|
|
920
|
+
orchestrator: Orchestrator instance
|
|
921
|
+
path_objs: List of Path objects (files or directories)
|
|
922
|
+
recursive: Whether to scan directories recursively
|
|
580
923
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
924
|
+
Returns:
|
|
925
|
+
List of violations from all paths
|
|
926
|
+
"""
|
|
927
|
+
files, dirs = _separate_files_and_dirs(path_objs)
|
|
584
928
|
|
|
929
|
+
violations = []
|
|
930
|
+
violations.extend(_lint_files_if_any(orchestrator, files))
|
|
931
|
+
violations.extend(_lint_directories(orchestrator, dirs, recursive))
|
|
585
932
|
|
|
586
|
-
|
|
587
|
-
"""Print a single violation in text format."""
|
|
588
|
-
location = f"{v.file_path}:{v.line}" if v.line else str(v.file_path)
|
|
589
|
-
if v.column:
|
|
590
|
-
location += f":{v.column}"
|
|
591
|
-
click.echo(f" {location}")
|
|
592
|
-
click.echo(f" [{v.severity.name}] {v.rule_id}: {v.message}")
|
|
593
|
-
click.echo()
|
|
933
|
+
return violations
|
|
594
934
|
|
|
595
935
|
|
|
596
|
-
def _setup_nesting_orchestrator(
|
|
936
|
+
def _setup_nesting_orchestrator(
|
|
937
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
938
|
+
):
|
|
597
939
|
"""Set up orchestrator for nesting command."""
|
|
598
|
-
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
599
940
|
from src.orchestrator.core import Orchestrator
|
|
941
|
+
from src.utils.project_root import get_project_root
|
|
942
|
+
|
|
943
|
+
# Use provided project_root or fall back to auto-detection
|
|
944
|
+
if project_root is None:
|
|
945
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
946
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
947
|
+
project_root = get_project_root(search_start)
|
|
600
948
|
|
|
601
949
|
orchestrator = Orchestrator(project_root=project_root)
|
|
602
950
|
|
|
@@ -611,53 +959,84 @@ def _apply_nesting_config_override(orchestrator, max_depth: int | None, verbose:
|
|
|
611
959
|
if max_depth is None:
|
|
612
960
|
return
|
|
613
961
|
|
|
962
|
+
# Ensure nesting config exists
|
|
614
963
|
if "nesting" not in orchestrator.config:
|
|
615
964
|
orchestrator.config["nesting"] = {}
|
|
616
|
-
|
|
965
|
+
|
|
966
|
+
nesting_config = orchestrator.config["nesting"]
|
|
967
|
+
|
|
968
|
+
# Set top-level max_nesting_depth
|
|
969
|
+
nesting_config["max_nesting_depth"] = max_depth
|
|
970
|
+
|
|
971
|
+
# Override language-specific configs to ensure CLI option takes precedence
|
|
972
|
+
_override_language_specific_nesting(nesting_config, max_depth)
|
|
617
973
|
|
|
618
974
|
if verbose:
|
|
619
975
|
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
620
976
|
|
|
621
977
|
|
|
622
|
-
def
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
978
|
+
def _override_language_specific_nesting(nesting_config: dict, max_depth: int):
|
|
979
|
+
"""Override language-specific nesting depth configs.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
nesting_config: Nesting configuration dictionary
|
|
983
|
+
max_depth: Maximum nesting depth to set
|
|
984
|
+
"""
|
|
985
|
+
for lang in ["python", "typescript", "javascript"]:
|
|
986
|
+
if lang in nesting_config:
|
|
987
|
+
nesting_config[lang]["max_nesting_depth"] = max_depth
|
|
628
988
|
|
|
629
|
-
|
|
989
|
+
|
|
990
|
+
def _run_nesting_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
991
|
+
"""Execute nesting lint on files or directories."""
|
|
992
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
993
|
+
return [v for v in all_violations if "nesting" in v.rule_id]
|
|
630
994
|
|
|
631
995
|
|
|
632
996
|
@cli.command("nesting")
|
|
633
|
-
@click.argument("
|
|
997
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
634
998
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
635
|
-
@
|
|
636
|
-
"--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
|
|
637
|
-
)
|
|
999
|
+
@format_option
|
|
638
1000
|
@click.option("--max-depth", type=int, help="Override max nesting depth (default: 4)")
|
|
639
1001
|
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
640
1002
|
@click.pass_context
|
|
641
1003
|
def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
642
|
-
ctx,
|
|
1004
|
+
ctx,
|
|
1005
|
+
paths: tuple[str, ...],
|
|
1006
|
+
config_file: str | None,
|
|
1007
|
+
format: str,
|
|
1008
|
+
max_depth: int | None,
|
|
1009
|
+
recursive: bool,
|
|
643
1010
|
):
|
|
644
1011
|
"""Check for excessive nesting depth in code.
|
|
645
1012
|
|
|
646
1013
|
Analyzes Python and TypeScript files for deeply nested code structures
|
|
647
1014
|
(if/for/while/try statements) and reports violations.
|
|
648
1015
|
|
|
649
|
-
|
|
1016
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
650
1017
|
|
|
651
1018
|
Examples:
|
|
652
1019
|
|
|
653
1020
|
\b
|
|
654
|
-
# Check current directory
|
|
1021
|
+
# Check current directory (all files recursively)
|
|
655
1022
|
thai-lint nesting
|
|
656
1023
|
|
|
657
1024
|
\b
|
|
658
1025
|
# Check specific directory
|
|
659
1026
|
thai-lint nesting src/
|
|
660
1027
|
|
|
1028
|
+
\b
|
|
1029
|
+
# Check single file
|
|
1030
|
+
thai-lint nesting src/app.py
|
|
1031
|
+
|
|
1032
|
+
\b
|
|
1033
|
+
# Check multiple files
|
|
1034
|
+
thai-lint nesting src/app.py src/utils.py tests/test_app.py
|
|
1035
|
+
|
|
1036
|
+
\b
|
|
1037
|
+
# Check mix of files and directories
|
|
1038
|
+
thai-lint nesting src/app.py tests/
|
|
1039
|
+
|
|
661
1040
|
\b
|
|
662
1041
|
# Use custom max depth
|
|
663
1042
|
thai-lint nesting --max-depth 3 src/
|
|
@@ -671,28 +1050,616 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
671
1050
|
thai-lint nesting --config .thailint.yaml src/
|
|
672
1051
|
"""
|
|
673
1052
|
verbose = ctx.obj.get("verbose", False)
|
|
674
|
-
|
|
1053
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1054
|
+
|
|
1055
|
+
# Default to current directory if no paths provided
|
|
1056
|
+
if not paths:
|
|
1057
|
+
paths = (".",)
|
|
1058
|
+
|
|
1059
|
+
path_objs = [Path(p) for p in paths]
|
|
675
1060
|
|
|
676
1061
|
try:
|
|
677
|
-
_execute_nesting_lint(
|
|
1062
|
+
_execute_nesting_lint(
|
|
1063
|
+
path_objs, config_file, format, max_depth, recursive, verbose, project_root
|
|
1064
|
+
)
|
|
678
1065
|
except Exception as e:
|
|
679
1066
|
_handle_linting_error(e, verbose)
|
|
680
1067
|
|
|
681
1068
|
|
|
682
1069
|
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
683
|
-
|
|
1070
|
+
path_objs, config_file, format, max_depth, recursive, verbose, project_root=None
|
|
684
1071
|
):
|
|
685
1072
|
"""Execute nesting lint."""
|
|
686
|
-
|
|
1073
|
+
_validate_paths_exist(path_objs)
|
|
1074
|
+
orchestrator = _setup_nesting_orchestrator(path_objs, config_file, verbose, project_root)
|
|
687
1075
|
_apply_nesting_config_override(orchestrator, max_depth, verbose)
|
|
688
|
-
nesting_violations = _run_nesting_lint(orchestrator,
|
|
1076
|
+
nesting_violations = _run_nesting_lint(orchestrator, path_objs, recursive)
|
|
689
1077
|
|
|
690
1078
|
if verbose:
|
|
691
1079
|
logger.info(f"Found {len(nesting_violations)} nesting violation(s)")
|
|
692
1080
|
|
|
693
|
-
|
|
1081
|
+
format_violations(nesting_violations, format)
|
|
694
1082
|
sys.exit(1 if nesting_violations else 0)
|
|
695
1083
|
|
|
696
1084
|
|
|
1085
|
+
def _setup_srp_orchestrator(
|
|
1086
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
1087
|
+
):
|
|
1088
|
+
"""Set up orchestrator for SRP command."""
|
|
1089
|
+
from src.orchestrator.core import Orchestrator
|
|
1090
|
+
from src.utils.project_root import get_project_root
|
|
1091
|
+
|
|
1092
|
+
# Use provided project_root or fall back to auto-detection
|
|
1093
|
+
if project_root is None:
|
|
1094
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
1095
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
1096
|
+
project_root = get_project_root(search_start)
|
|
1097
|
+
|
|
1098
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
1099
|
+
|
|
1100
|
+
if config_file:
|
|
1101
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
1102
|
+
|
|
1103
|
+
return orchestrator
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def _apply_srp_config_override(
|
|
1107
|
+
orchestrator, max_methods: int | None, max_loc: int | None, verbose: bool
|
|
1108
|
+
):
|
|
1109
|
+
"""Apply max_methods and max_loc overrides to orchestrator config."""
|
|
1110
|
+
if max_methods is None and max_loc is None:
|
|
1111
|
+
return
|
|
1112
|
+
|
|
1113
|
+
if "srp" not in orchestrator.config:
|
|
1114
|
+
orchestrator.config["srp"] = {}
|
|
1115
|
+
|
|
1116
|
+
_apply_srp_max_methods(orchestrator, max_methods, verbose)
|
|
1117
|
+
_apply_srp_max_loc(orchestrator, max_loc, verbose)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _apply_srp_max_methods(orchestrator, max_methods: int | None, verbose: bool):
|
|
1121
|
+
"""Apply max_methods override."""
|
|
1122
|
+
if max_methods is not None:
|
|
1123
|
+
orchestrator.config["srp"]["max_methods"] = max_methods
|
|
1124
|
+
if verbose:
|
|
1125
|
+
logger.debug(f"Overriding max_methods to {max_methods}")
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _apply_srp_max_loc(orchestrator, max_loc: int | None, verbose: bool):
|
|
1129
|
+
"""Apply max_loc override."""
|
|
1130
|
+
if max_loc is not None:
|
|
1131
|
+
orchestrator.config["srp"]["max_loc"] = max_loc
|
|
1132
|
+
if verbose:
|
|
1133
|
+
logger.debug(f"Overriding max_loc to {max_loc}")
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _run_srp_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
1137
|
+
"""Execute SRP lint on files or directories."""
|
|
1138
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
1139
|
+
return [v for v in all_violations if "srp" in v.rule_id]
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
@cli.command("srp")
|
|
1143
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
1144
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
1145
|
+
@format_option
|
|
1146
|
+
@click.option("--max-methods", type=int, help="Override max methods per class (default: 7)")
|
|
1147
|
+
@click.option("--max-loc", type=int, help="Override max lines of code per class (default: 200)")
|
|
1148
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
1149
|
+
@click.pass_context
|
|
1150
|
+
def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1151
|
+
ctx,
|
|
1152
|
+
paths: tuple[str, ...],
|
|
1153
|
+
config_file: str | None,
|
|
1154
|
+
format: str,
|
|
1155
|
+
max_methods: int | None,
|
|
1156
|
+
max_loc: int | None,
|
|
1157
|
+
recursive: bool,
|
|
1158
|
+
):
|
|
1159
|
+
"""Check for Single Responsibility Principle violations.
|
|
1160
|
+
|
|
1161
|
+
Analyzes Python and TypeScript classes for SRP violations using heuristics:
|
|
1162
|
+
- Method count exceeding threshold (default: 7)
|
|
1163
|
+
- Lines of code exceeding threshold (default: 200)
|
|
1164
|
+
- Responsibility keywords in class names (Manager, Handler, Processor, etc.)
|
|
1165
|
+
|
|
1166
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
1167
|
+
|
|
1168
|
+
Examples:
|
|
1169
|
+
|
|
1170
|
+
\b
|
|
1171
|
+
# Check current directory (all files recursively)
|
|
1172
|
+
thai-lint srp
|
|
1173
|
+
|
|
1174
|
+
\b
|
|
1175
|
+
# Check specific directory
|
|
1176
|
+
thai-lint srp src/
|
|
1177
|
+
|
|
1178
|
+
\b
|
|
1179
|
+
# Check single file
|
|
1180
|
+
thai-lint srp src/app.py
|
|
1181
|
+
|
|
1182
|
+
\b
|
|
1183
|
+
# Check multiple files
|
|
1184
|
+
thai-lint srp src/app.py src/service.py tests/test_app.py
|
|
1185
|
+
|
|
1186
|
+
\b
|
|
1187
|
+
# Use custom thresholds
|
|
1188
|
+
thai-lint srp --max-methods 10 --max-loc 300 src/
|
|
1189
|
+
|
|
1190
|
+
\b
|
|
1191
|
+
# Get JSON output
|
|
1192
|
+
thai-lint srp --format json .
|
|
1193
|
+
|
|
1194
|
+
\b
|
|
1195
|
+
# Use custom config file
|
|
1196
|
+
thai-lint srp --config .thailint.yaml src/
|
|
1197
|
+
"""
|
|
1198
|
+
verbose = ctx.obj.get("verbose", False)
|
|
1199
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1200
|
+
|
|
1201
|
+
if not paths:
|
|
1202
|
+
paths = (".",)
|
|
1203
|
+
|
|
1204
|
+
path_objs = [Path(p) for p in paths]
|
|
1205
|
+
|
|
1206
|
+
try:
|
|
1207
|
+
_execute_srp_lint(
|
|
1208
|
+
path_objs, config_file, format, max_methods, max_loc, recursive, verbose, project_root
|
|
1209
|
+
)
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
_handle_linting_error(e, verbose)
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1215
|
+
path_objs, config_file, format, max_methods, max_loc, recursive, verbose, project_root=None
|
|
1216
|
+
):
|
|
1217
|
+
"""Execute SRP lint."""
|
|
1218
|
+
_validate_paths_exist(path_objs)
|
|
1219
|
+
orchestrator = _setup_srp_orchestrator(path_objs, config_file, verbose, project_root)
|
|
1220
|
+
_apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
|
|
1221
|
+
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive)
|
|
1222
|
+
|
|
1223
|
+
if verbose:
|
|
1224
|
+
logger.info(f"Found {len(srp_violations)} SRP violation(s)")
|
|
1225
|
+
|
|
1226
|
+
format_violations(srp_violations, format)
|
|
1227
|
+
sys.exit(1 if srp_violations else 0)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
@cli.command("dry")
|
|
1231
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
1232
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
1233
|
+
@format_option
|
|
1234
|
+
@click.option("--min-lines", type=int, help="Override min duplicate lines threshold")
|
|
1235
|
+
@click.option("--no-cache", is_flag=True, help="Disable SQLite cache (force rehash)")
|
|
1236
|
+
@click.option("--clear-cache", is_flag=True, help="Clear cache before running")
|
|
1237
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
1238
|
+
@click.pass_context
|
|
1239
|
+
def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1240
|
+
ctx,
|
|
1241
|
+
paths: tuple[str, ...],
|
|
1242
|
+
config_file: str | None,
|
|
1243
|
+
format: str,
|
|
1244
|
+
min_lines: int | None,
|
|
1245
|
+
no_cache: bool,
|
|
1246
|
+
clear_cache: bool,
|
|
1247
|
+
recursive: bool,
|
|
1248
|
+
):
|
|
1249
|
+
# Justification for Pylint disables:
|
|
1250
|
+
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 6 options = 8 params
|
|
1251
|
+
# All parameters are necessary for flexible DRY linter CLI usage.
|
|
1252
|
+
"""
|
|
1253
|
+
Check for duplicate code (DRY principle violations).
|
|
1254
|
+
|
|
1255
|
+
Detects duplicate code blocks across your project using token-based hashing
|
|
1256
|
+
with SQLite caching for fast incremental scans.
|
|
1257
|
+
|
|
1258
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
1259
|
+
|
|
1260
|
+
Examples:
|
|
1261
|
+
|
|
1262
|
+
\b
|
|
1263
|
+
# Check current directory (all files recursively)
|
|
1264
|
+
thai-lint dry
|
|
1265
|
+
|
|
1266
|
+
\b
|
|
1267
|
+
# Check specific directory
|
|
1268
|
+
thai-lint dry src/
|
|
1269
|
+
|
|
1270
|
+
\b
|
|
1271
|
+
# Check single file
|
|
1272
|
+
thai-lint dry src/app.py
|
|
1273
|
+
|
|
1274
|
+
\b
|
|
1275
|
+
# Check multiple files
|
|
1276
|
+
thai-lint dry src/app.py src/service.py tests/test_app.py
|
|
1277
|
+
|
|
1278
|
+
\b
|
|
1279
|
+
# Use custom config file
|
|
1280
|
+
thai-lint dry --config .thailint.yaml src/
|
|
1281
|
+
|
|
1282
|
+
\b
|
|
1283
|
+
# Override minimum duplicate lines threshold
|
|
1284
|
+
thai-lint dry --min-lines 5 .
|
|
1285
|
+
|
|
1286
|
+
\b
|
|
1287
|
+
# Disable cache (force re-analysis)
|
|
1288
|
+
thai-lint dry --no-cache .
|
|
1289
|
+
|
|
1290
|
+
\b
|
|
1291
|
+
# Clear cache before running
|
|
1292
|
+
thai-lint dry --clear-cache .
|
|
1293
|
+
|
|
1294
|
+
\b
|
|
1295
|
+
# Get JSON output
|
|
1296
|
+
thai-lint dry --format json .
|
|
1297
|
+
"""
|
|
1298
|
+
verbose = ctx.obj.get("verbose", False)
|
|
1299
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1300
|
+
|
|
1301
|
+
if not paths:
|
|
1302
|
+
paths = (".",)
|
|
1303
|
+
|
|
1304
|
+
path_objs = [Path(p) for p in paths]
|
|
1305
|
+
|
|
1306
|
+
try:
|
|
1307
|
+
_execute_dry_lint(
|
|
1308
|
+
path_objs,
|
|
1309
|
+
config_file,
|
|
1310
|
+
format,
|
|
1311
|
+
min_lines,
|
|
1312
|
+
no_cache,
|
|
1313
|
+
clear_cache,
|
|
1314
|
+
recursive,
|
|
1315
|
+
verbose,
|
|
1316
|
+
project_root,
|
|
1317
|
+
)
|
|
1318
|
+
except Exception as e:
|
|
1319
|
+
_handle_linting_error(e, verbose)
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1323
|
+
path_objs,
|
|
1324
|
+
config_file,
|
|
1325
|
+
format,
|
|
1326
|
+
min_lines,
|
|
1327
|
+
no_cache,
|
|
1328
|
+
clear_cache,
|
|
1329
|
+
recursive,
|
|
1330
|
+
verbose,
|
|
1331
|
+
project_root=None,
|
|
1332
|
+
):
|
|
1333
|
+
"""Execute DRY linting."""
|
|
1334
|
+
_validate_paths_exist(path_objs)
|
|
1335
|
+
orchestrator = _setup_dry_orchestrator(path_objs, config_file, verbose, project_root)
|
|
1336
|
+
_apply_dry_config_override(orchestrator, min_lines, no_cache, verbose)
|
|
1337
|
+
|
|
1338
|
+
if clear_cache:
|
|
1339
|
+
_clear_dry_cache(orchestrator, verbose)
|
|
1340
|
+
|
|
1341
|
+
dry_violations = _run_dry_lint(orchestrator, path_objs, recursive)
|
|
1342
|
+
|
|
1343
|
+
if verbose:
|
|
1344
|
+
logger.info(f"Found {len(dry_violations)} DRY violation(s)")
|
|
1345
|
+
|
|
1346
|
+
format_violations(dry_violations, format)
|
|
1347
|
+
sys.exit(1 if dry_violations else 0)
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _setup_dry_orchestrator(path_objs, config_file, verbose, project_root=None):
|
|
1351
|
+
"""Set up orchestrator for DRY linting."""
|
|
1352
|
+
from src.orchestrator.core import Orchestrator
|
|
1353
|
+
from src.utils.project_root import get_project_root
|
|
1354
|
+
|
|
1355
|
+
# Use provided project_root or fall back to auto-detection
|
|
1356
|
+
if project_root is None:
|
|
1357
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
1358
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
1359
|
+
project_root = get_project_root(search_start)
|
|
1360
|
+
|
|
1361
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
1362
|
+
|
|
1363
|
+
if config_file:
|
|
1364
|
+
_load_dry_config_file(orchestrator, config_file, verbose)
|
|
1365
|
+
|
|
1366
|
+
return orchestrator
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _load_dry_config_file(orchestrator, config_file, verbose):
|
|
1370
|
+
"""Load DRY configuration from file."""
|
|
1371
|
+
import yaml
|
|
1372
|
+
|
|
1373
|
+
config_path = Path(config_file)
|
|
1374
|
+
if not config_path.exists():
|
|
1375
|
+
click.echo(f"Error: Config file not found: {config_file}", err=True)
|
|
1376
|
+
sys.exit(2)
|
|
1377
|
+
|
|
1378
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
1379
|
+
config = yaml.safe_load(f)
|
|
1380
|
+
|
|
1381
|
+
if "dry" in config:
|
|
1382
|
+
orchestrator.config.update({"dry": config["dry"]})
|
|
1383
|
+
|
|
1384
|
+
if verbose:
|
|
1385
|
+
logger.info(f"Loaded DRY config from {config_file}")
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _apply_dry_config_override(orchestrator, min_lines, no_cache, verbose):
|
|
1389
|
+
"""Apply CLI option overrides to DRY config."""
|
|
1390
|
+
_ensure_dry_config_exists(orchestrator)
|
|
1391
|
+
_apply_min_lines_override(orchestrator, min_lines, verbose)
|
|
1392
|
+
_apply_cache_override(orchestrator, no_cache, verbose)
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _ensure_dry_config_exists(orchestrator):
|
|
1396
|
+
"""Ensure dry config section exists."""
|
|
1397
|
+
if "dry" not in orchestrator.config:
|
|
1398
|
+
orchestrator.config["dry"] = {}
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _apply_min_lines_override(orchestrator, min_lines, verbose):
|
|
1402
|
+
"""Apply min_lines override if provided."""
|
|
1403
|
+
if min_lines is None:
|
|
1404
|
+
return
|
|
1405
|
+
|
|
1406
|
+
orchestrator.config["dry"]["min_duplicate_lines"] = min_lines
|
|
1407
|
+
if verbose:
|
|
1408
|
+
logger.info(f"Override: min_duplicate_lines = {min_lines}")
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def _apply_cache_override(orchestrator, no_cache, verbose):
|
|
1412
|
+
"""Apply cache override if requested."""
|
|
1413
|
+
if not no_cache:
|
|
1414
|
+
return
|
|
1415
|
+
|
|
1416
|
+
orchestrator.config["dry"]["cache_enabled"] = False
|
|
1417
|
+
if verbose:
|
|
1418
|
+
logger.info("Override: cache_enabled = False")
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def _clear_dry_cache(orchestrator, verbose):
|
|
1422
|
+
"""Clear DRY cache before running."""
|
|
1423
|
+
cache_path_str = orchestrator.config.get("dry", {}).get("cache_path", ".thailint-cache/dry.db")
|
|
1424
|
+
cache_path = orchestrator.project_root / cache_path_str
|
|
1425
|
+
|
|
1426
|
+
if cache_path.exists():
|
|
1427
|
+
cache_path.unlink()
|
|
1428
|
+
if verbose:
|
|
1429
|
+
logger.info(f"Cleared cache: {cache_path}")
|
|
1430
|
+
else:
|
|
1431
|
+
if verbose:
|
|
1432
|
+
logger.info("Cache file does not exist, nothing to clear")
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _run_dry_lint(orchestrator, path_objs, recursive):
|
|
1436
|
+
"""Run DRY linting and return violations."""
|
|
1437
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
1438
|
+
|
|
1439
|
+
# Filter to only DRY violations
|
|
1440
|
+
dry_violations = [v for v in all_violations if v.rule_id.startswith("dry.")]
|
|
1441
|
+
|
|
1442
|
+
return dry_violations
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def _setup_magic_numbers_orchestrator(
|
|
1446
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
1447
|
+
):
|
|
1448
|
+
"""Set up orchestrator for magic-numbers command."""
|
|
1449
|
+
from src.orchestrator.core import Orchestrator
|
|
1450
|
+
from src.utils.project_root import get_project_root
|
|
1451
|
+
|
|
1452
|
+
# Use provided project_root or fall back to auto-detection
|
|
1453
|
+
if project_root is None:
|
|
1454
|
+
# Find actual project root (where .git or .thailint.yaml exists)
|
|
1455
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
1456
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
1457
|
+
project_root = get_project_root(search_start)
|
|
1458
|
+
|
|
1459
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
1460
|
+
|
|
1461
|
+
if config_file:
|
|
1462
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
1463
|
+
|
|
1464
|
+
return orchestrator
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _run_magic_numbers_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
1468
|
+
"""Execute magic-numbers lint on files or directories."""
|
|
1469
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
1470
|
+
return [v for v in all_violations if "magic-number" in v.rule_id]
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
@cli.command("magic-numbers")
|
|
1474
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
1475
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
1476
|
+
@format_option
|
|
1477
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
1478
|
+
@click.pass_context
|
|
1479
|
+
def magic_numbers( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1480
|
+
ctx,
|
|
1481
|
+
paths: tuple[str, ...],
|
|
1482
|
+
config_file: str | None,
|
|
1483
|
+
format: str,
|
|
1484
|
+
recursive: bool,
|
|
1485
|
+
):
|
|
1486
|
+
"""Check for magic numbers in code.
|
|
1487
|
+
|
|
1488
|
+
Detects unnamed numeric literals in Python and TypeScript/JavaScript code
|
|
1489
|
+
that should be extracted as named constants for better readability.
|
|
1490
|
+
|
|
1491
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
1492
|
+
|
|
1493
|
+
Examples:
|
|
1494
|
+
|
|
1495
|
+
\b
|
|
1496
|
+
# Check current directory (all files recursively)
|
|
1497
|
+
thai-lint magic-numbers
|
|
1498
|
+
|
|
1499
|
+
\b
|
|
1500
|
+
# Check specific directory
|
|
1501
|
+
thai-lint magic-numbers src/
|
|
1502
|
+
|
|
1503
|
+
\b
|
|
1504
|
+
# Check single file
|
|
1505
|
+
thai-lint magic-numbers src/app.py
|
|
1506
|
+
|
|
1507
|
+
\b
|
|
1508
|
+
# Check multiple files
|
|
1509
|
+
thai-lint magic-numbers src/app.py src/utils.py tests/test_app.py
|
|
1510
|
+
|
|
1511
|
+
\b
|
|
1512
|
+
# Check mix of files and directories
|
|
1513
|
+
thai-lint magic-numbers src/app.py tests/
|
|
1514
|
+
|
|
1515
|
+
\b
|
|
1516
|
+
# Get JSON output
|
|
1517
|
+
thai-lint magic-numbers --format json .
|
|
1518
|
+
|
|
1519
|
+
\b
|
|
1520
|
+
# Use custom config file
|
|
1521
|
+
thai-lint magic-numbers --config .thailint.yaml src/
|
|
1522
|
+
"""
|
|
1523
|
+
verbose = ctx.obj.get("verbose", False)
|
|
1524
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1525
|
+
|
|
1526
|
+
if not paths:
|
|
1527
|
+
paths = (".",)
|
|
1528
|
+
|
|
1529
|
+
path_objs = [Path(p) for p in paths]
|
|
1530
|
+
|
|
1531
|
+
try:
|
|
1532
|
+
_execute_magic_numbers_lint(
|
|
1533
|
+
path_objs, config_file, format, recursive, verbose, project_root
|
|
1534
|
+
)
|
|
1535
|
+
except Exception as e:
|
|
1536
|
+
_handle_linting_error(e, verbose)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1540
|
+
path_objs, config_file, format, recursive, verbose, project_root=None
|
|
1541
|
+
):
|
|
1542
|
+
"""Execute magic-numbers lint."""
|
|
1543
|
+
_validate_paths_exist(path_objs)
|
|
1544
|
+
orchestrator = _setup_magic_numbers_orchestrator(path_objs, config_file, verbose, project_root)
|
|
1545
|
+
magic_numbers_violations = _run_magic_numbers_lint(orchestrator, path_objs, recursive)
|
|
1546
|
+
|
|
1547
|
+
if verbose:
|
|
1548
|
+
logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
|
|
1549
|
+
|
|
1550
|
+
format_violations(magic_numbers_violations, format)
|
|
1551
|
+
sys.exit(1 if magic_numbers_violations else 0)
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
# =============================================================================
|
|
1555
|
+
# Print Statements Linter Command
|
|
1556
|
+
# =============================================================================
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _setup_print_statements_orchestrator(
|
|
1560
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
1561
|
+
):
|
|
1562
|
+
"""Set up orchestrator for print-statements command."""
|
|
1563
|
+
from src.orchestrator.core import Orchestrator
|
|
1564
|
+
from src.utils.project_root import get_project_root
|
|
1565
|
+
|
|
1566
|
+
if project_root is None:
|
|
1567
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
1568
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
1569
|
+
project_root = get_project_root(search_start)
|
|
1570
|
+
|
|
1571
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
1572
|
+
|
|
1573
|
+
if config_file:
|
|
1574
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
1575
|
+
|
|
1576
|
+
return orchestrator
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _run_print_statements_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
1580
|
+
"""Execute print-statements lint on files or directories."""
|
|
1581
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
1582
|
+
return [v for v in all_violations if "print-statement" in v.rule_id]
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
@cli.command("print-statements")
|
|
1586
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
1587
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
1588
|
+
@format_option
|
|
1589
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
1590
|
+
@click.pass_context
|
|
1591
|
+
def print_statements( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1592
|
+
ctx,
|
|
1593
|
+
paths: tuple[str, ...],
|
|
1594
|
+
config_file: str | None,
|
|
1595
|
+
format: str,
|
|
1596
|
+
recursive: bool,
|
|
1597
|
+
):
|
|
1598
|
+
"""Check for print/console statements in code.
|
|
1599
|
+
|
|
1600
|
+
Detects print() calls in Python and console.log/warn/error/debug/info calls
|
|
1601
|
+
in TypeScript/JavaScript that should be replaced with proper logging.
|
|
1602
|
+
|
|
1603
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
1604
|
+
|
|
1605
|
+
Examples:
|
|
1606
|
+
|
|
1607
|
+
\b
|
|
1608
|
+
# Check current directory (all files recursively)
|
|
1609
|
+
thai-lint print-statements
|
|
1610
|
+
|
|
1611
|
+
\b
|
|
1612
|
+
# Check specific directory
|
|
1613
|
+
thai-lint print-statements src/
|
|
1614
|
+
|
|
1615
|
+
\b
|
|
1616
|
+
# Check single file
|
|
1617
|
+
thai-lint print-statements src/app.py
|
|
1618
|
+
|
|
1619
|
+
\b
|
|
1620
|
+
# Check multiple files
|
|
1621
|
+
thai-lint print-statements src/app.py src/utils.ts tests/test_app.py
|
|
1622
|
+
|
|
1623
|
+
\b
|
|
1624
|
+
# Get JSON output
|
|
1625
|
+
thai-lint print-statements --format json .
|
|
1626
|
+
|
|
1627
|
+
\b
|
|
1628
|
+
# Use custom config file
|
|
1629
|
+
thai-lint print-statements --config .thailint.yaml src/
|
|
1630
|
+
"""
|
|
1631
|
+
verbose = ctx.obj.get("verbose", False)
|
|
1632
|
+
project_root = _get_project_root_from_context(ctx)
|
|
1633
|
+
|
|
1634
|
+
if not paths:
|
|
1635
|
+
paths = (".",)
|
|
1636
|
+
|
|
1637
|
+
path_objs = [Path(p) for p in paths]
|
|
1638
|
+
|
|
1639
|
+
try:
|
|
1640
|
+
_execute_print_statements_lint(
|
|
1641
|
+
path_objs, config_file, format, recursive, verbose, project_root
|
|
1642
|
+
)
|
|
1643
|
+
except Exception as e:
|
|
1644
|
+
_handle_linting_error(e, verbose)
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
def _execute_print_statements_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1648
|
+
path_objs, config_file, format, recursive, verbose, project_root=None
|
|
1649
|
+
):
|
|
1650
|
+
"""Execute print-statements lint."""
|
|
1651
|
+
_validate_paths_exist(path_objs)
|
|
1652
|
+
orchestrator = _setup_print_statements_orchestrator(
|
|
1653
|
+
path_objs, config_file, verbose, project_root
|
|
1654
|
+
)
|
|
1655
|
+
print_statements_violations = _run_print_statements_lint(orchestrator, path_objs, recursive)
|
|
1656
|
+
|
|
1657
|
+
if verbose:
|
|
1658
|
+
logger.info(f"Found {len(print_statements_violations)} print statement violation(s)")
|
|
1659
|
+
|
|
1660
|
+
format_violations(print_statements_violations, format)
|
|
1661
|
+
sys.exit(1 if print_statements_violations else 0)
|
|
1662
|
+
|
|
1663
|
+
|
|
697
1664
|
if __name__ == "__main__":
|
|
698
1665
|
cli()
|