thailint 0.2.0__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/cli.py +646 -36
- src/config.py +6 -2
- src/core/base.py +90 -5
- src/core/config_parser.py +31 -4
- src/linters/dry/block_filter.py +5 -2
- src/linters/dry/cache.py +46 -92
- src/linters/dry/config.py +17 -13
- src/linters/dry/duplicate_storage.py +17 -80
- src/linters/dry/file_analyzer.py +11 -48
- src/linters/dry/linter.py +5 -12
- src/linters/dry/python_analyzer.py +188 -37
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/token_hasher.py +63 -9
- src/linters/dry/typescript_analyzer.py +7 -5
- src/linters/dry/violation_filter.py +4 -1
- 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/linter.py +15 -4
- 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 +6 -3
- src/linters/nesting/linter.py +8 -19
- src/linters/nesting/typescript_analyzer.py +1 -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 +3 -3
- src/linters/srp/config.py +12 -6
- src/linters/srp/linter.py +33 -24
- src/orchestrator/core.py +12 -2
- src/templates/thailint_config_template.yaml +158 -0
- src/utils/project_root.py +135 -16
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/METADATA +387 -81
- thailint-0.5.0.dist-info/RECORD +96 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/WHEEL +1 -1
- thailint-0.2.0.dist-info/RECORD +0 -75
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.2.0.dist-info → thailint-0.5.0.dist-info/licenses}/LICENSE +0 -0
src/cli.py
CHANGED
|
@@ -59,12 +59,145 @@ def setup_logging(verbose: bool = False):
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
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
|
+
|
|
62
190
|
@click.group()
|
|
63
191
|
@click.version_option(version=__version__)
|
|
64
192
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
65
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
|
+
)
|
|
66
199
|
@click.pass_context
|
|
67
|
-
def cli(ctx, verbose: bool, config: str | None):
|
|
200
|
+
def cli(ctx, verbose: bool, config: str | None, project_root: str | None):
|
|
68
201
|
"""
|
|
69
202
|
thai-lint - AI code linter and governance tool
|
|
70
203
|
|
|
@@ -85,6 +218,10 @@ def cli(ctx, verbose: bool, config: str | None):
|
|
|
85
218
|
# Lint with custom config
|
|
86
219
|
thai-lint file-placement --config .thailint.yaml src/
|
|
87
220
|
|
|
221
|
+
\b
|
|
222
|
+
# Specify project root explicitly (useful in Docker)
|
|
223
|
+
thai-lint --project-root /workspace/root magic-numbers backend/
|
|
224
|
+
|
|
88
225
|
\b
|
|
89
226
|
# Get JSON output
|
|
90
227
|
thai-lint file-placement --format json .
|
|
@@ -99,6 +236,11 @@ def cli(ctx, verbose: bool, config: str | None):
|
|
|
99
236
|
# Setup logging
|
|
100
237
|
setup_logging(verbose)
|
|
101
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
|
+
|
|
102
244
|
# Load configuration
|
|
103
245
|
try:
|
|
104
246
|
if config:
|
|
@@ -362,8 +504,143 @@ def config_reset(ctx, yes: bool):
|
|
|
362
504
|
sys.exit(1)
|
|
363
505
|
|
|
364
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
|
+
|
|
365
642
|
@cli.command("file-placement")
|
|
366
|
-
@click.argument("paths", nargs=-1, type=click.Path(
|
|
643
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
367
644
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
368
645
|
@click.option("--rules", "-r", help="Inline JSON rules configuration")
|
|
369
646
|
@format_option
|
|
@@ -416,6 +693,7 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
416
693
|
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
417
694
|
"""
|
|
418
695
|
verbose = ctx.obj.get("verbose", False)
|
|
696
|
+
project_root = _get_project_root_from_context(ctx)
|
|
419
697
|
|
|
420
698
|
if not paths:
|
|
421
699
|
paths = (".",)
|
|
@@ -423,16 +701,19 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
423
701
|
path_objs = [Path(p) for p in paths]
|
|
424
702
|
|
|
425
703
|
try:
|
|
426
|
-
_execute_file_placement_lint(
|
|
704
|
+
_execute_file_placement_lint(
|
|
705
|
+
path_objs, config_file, rules, format, recursive, verbose, project_root
|
|
706
|
+
)
|
|
427
707
|
except Exception as e:
|
|
428
708
|
_handle_linting_error(e, verbose)
|
|
429
709
|
|
|
430
710
|
|
|
431
711
|
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
432
|
-
path_objs, config_file, rules, format, recursive, verbose
|
|
712
|
+
path_objs, config_file, rules, format, recursive, verbose, project_root=None
|
|
433
713
|
):
|
|
434
714
|
"""Execute file placement linting."""
|
|
435
|
-
|
|
715
|
+
_validate_paths_exist(path_objs)
|
|
716
|
+
orchestrator = _setup_orchestrator(path_objs, config_file, rules, verbose, project_root)
|
|
436
717
|
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
437
718
|
|
|
438
719
|
# Filter to only file-placement violations
|
|
@@ -453,6 +734,28 @@ def _handle_linting_error(error: Exception, verbose: bool) -> None:
|
|
|
453
734
|
sys.exit(2)
|
|
454
735
|
|
|
455
736
|
|
|
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
|
+
|
|
456
759
|
def _find_project_root(start_path: Path) -> Path:
|
|
457
760
|
"""Find project root by looking for .git or pyproject.toml.
|
|
458
761
|
|
|
@@ -469,26 +772,55 @@ def _find_project_root(start_path: Path) -> Path:
|
|
|
469
772
|
return get_project_root(start_path)
|
|
470
773
|
|
|
471
774
|
|
|
472
|
-
def _setup_orchestrator(path_objs, config_file, rules, verbose):
|
|
775
|
+
def _setup_orchestrator(path_objs, config_file, rules, verbose, project_root=None):
|
|
473
776
|
"""Set up and configure the orchestrator."""
|
|
474
777
|
from src.orchestrator.core import Orchestrator
|
|
475
778
|
from src.utils.project_root import get_project_root
|
|
476
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)
|
|
782
|
+
|
|
783
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
784
|
+
_apply_orchestrator_config(orchestrator, config_file, rules, verbose)
|
|
785
|
+
|
|
786
|
+
return orchestrator
|
|
787
|
+
|
|
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
|
+
|
|
477
803
|
# Find actual project root (where .git or pyproject.toml exists)
|
|
478
804
|
# This ensures .artifacts/ is always created at project root, not in subdirectories
|
|
479
805
|
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
480
806
|
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
481
|
-
|
|
807
|
+
return get_project_root(search_start)
|
|
482
808
|
|
|
483
|
-
orchestrator = Orchestrator(project_root=project_root)
|
|
484
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
|
+
"""
|
|
485
819
|
if rules:
|
|
486
820
|
_apply_inline_rules(orchestrator, rules, verbose)
|
|
487
821
|
elif config_file:
|
|
488
822
|
_load_config_file(orchestrator, config_file, verbose)
|
|
489
823
|
|
|
490
|
-
return orchestrator
|
|
491
|
-
|
|
492
824
|
|
|
493
825
|
def _apply_inline_rules(orchestrator, rules, verbose):
|
|
494
826
|
"""Parse and apply inline JSON rules."""
|
|
@@ -601,13 +933,18 @@ def _execute_linting_on_paths(orchestrator, path_objs: list[Path], recursive: bo
|
|
|
601
933
|
return violations
|
|
602
934
|
|
|
603
935
|
|
|
604
|
-
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
|
+
):
|
|
605
939
|
"""Set up orchestrator for nesting command."""
|
|
606
|
-
# Use first path to determine project root
|
|
607
|
-
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
608
|
-
project_root = first_path if first_path.is_dir() else first_path.parent
|
|
609
|
-
|
|
610
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)
|
|
611
948
|
|
|
612
949
|
orchestrator = Orchestrator(project_root=project_root)
|
|
613
950
|
|
|
@@ -622,14 +959,34 @@ def _apply_nesting_config_override(orchestrator, max_depth: int | None, verbose:
|
|
|
622
959
|
if max_depth is None:
|
|
623
960
|
return
|
|
624
961
|
|
|
962
|
+
# Ensure nesting config exists
|
|
625
963
|
if "nesting" not in orchestrator.config:
|
|
626
964
|
orchestrator.config["nesting"] = {}
|
|
627
|
-
|
|
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)
|
|
628
973
|
|
|
629
974
|
if verbose:
|
|
630
975
|
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
631
976
|
|
|
632
977
|
|
|
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
|
|
988
|
+
|
|
989
|
+
|
|
633
990
|
def _run_nesting_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
634
991
|
"""Execute nesting lint on files or directories."""
|
|
635
992
|
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
@@ -637,7 +994,7 @@ def _run_nesting_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
|
637
994
|
|
|
638
995
|
|
|
639
996
|
@cli.command("nesting")
|
|
640
|
-
@click.argument("paths", nargs=-1, type=click.Path(
|
|
997
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
641
998
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
642
999
|
@format_option
|
|
643
1000
|
@click.option("--max-depth", type=int, help="Override max nesting depth (default: 4)")
|
|
@@ -693,6 +1050,7 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
693
1050
|
thai-lint nesting --config .thailint.yaml src/
|
|
694
1051
|
"""
|
|
695
1052
|
verbose = ctx.obj.get("verbose", False)
|
|
1053
|
+
project_root = _get_project_root_from_context(ctx)
|
|
696
1054
|
|
|
697
1055
|
# Default to current directory if no paths provided
|
|
698
1056
|
if not paths:
|
|
@@ -701,16 +1059,19 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
701
1059
|
path_objs = [Path(p) for p in paths]
|
|
702
1060
|
|
|
703
1061
|
try:
|
|
704
|
-
_execute_nesting_lint(
|
|
1062
|
+
_execute_nesting_lint(
|
|
1063
|
+
path_objs, config_file, format, max_depth, recursive, verbose, project_root
|
|
1064
|
+
)
|
|
705
1065
|
except Exception as e:
|
|
706
1066
|
_handle_linting_error(e, verbose)
|
|
707
1067
|
|
|
708
1068
|
|
|
709
1069
|
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
710
|
-
path_objs, config_file, format, max_depth, recursive, verbose
|
|
1070
|
+
path_objs, config_file, format, max_depth, recursive, verbose, project_root=None
|
|
711
1071
|
):
|
|
712
1072
|
"""Execute nesting lint."""
|
|
713
|
-
|
|
1073
|
+
_validate_paths_exist(path_objs)
|
|
1074
|
+
orchestrator = _setup_nesting_orchestrator(path_objs, config_file, verbose, project_root)
|
|
714
1075
|
_apply_nesting_config_override(orchestrator, max_depth, verbose)
|
|
715
1076
|
nesting_violations = _run_nesting_lint(orchestrator, path_objs, recursive)
|
|
716
1077
|
|
|
@@ -721,12 +1082,18 @@ def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positi
|
|
|
721
1082
|
sys.exit(1 if nesting_violations else 0)
|
|
722
1083
|
|
|
723
1084
|
|
|
724
|
-
def _setup_srp_orchestrator(
|
|
1085
|
+
def _setup_srp_orchestrator(
|
|
1086
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
1087
|
+
):
|
|
725
1088
|
"""Set up orchestrator for SRP command."""
|
|
726
|
-
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
727
|
-
project_root = first_path if first_path.is_dir() else first_path.parent
|
|
728
|
-
|
|
729
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)
|
|
730
1097
|
|
|
731
1098
|
orchestrator = Orchestrator(project_root=project_root)
|
|
732
1099
|
|
|
@@ -773,7 +1140,7 @@ def _run_srp_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
|
773
1140
|
|
|
774
1141
|
|
|
775
1142
|
@cli.command("srp")
|
|
776
|
-
@click.argument("paths", nargs=-1, type=click.Path(
|
|
1143
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
777
1144
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
778
1145
|
@format_option
|
|
779
1146
|
@click.option("--max-methods", type=int, help="Override max methods per class (default: 7)")
|
|
@@ -829,6 +1196,7 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
829
1196
|
thai-lint srp --config .thailint.yaml src/
|
|
830
1197
|
"""
|
|
831
1198
|
verbose = ctx.obj.get("verbose", False)
|
|
1199
|
+
project_root = _get_project_root_from_context(ctx)
|
|
832
1200
|
|
|
833
1201
|
if not paths:
|
|
834
1202
|
paths = (".",)
|
|
@@ -836,16 +1204,19 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
836
1204
|
path_objs = [Path(p) for p in paths]
|
|
837
1205
|
|
|
838
1206
|
try:
|
|
839
|
-
_execute_srp_lint(
|
|
1207
|
+
_execute_srp_lint(
|
|
1208
|
+
path_objs, config_file, format, max_methods, max_loc, recursive, verbose, project_root
|
|
1209
|
+
)
|
|
840
1210
|
except Exception as e:
|
|
841
1211
|
_handle_linting_error(e, verbose)
|
|
842
1212
|
|
|
843
1213
|
|
|
844
1214
|
def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
845
|
-
path_objs, config_file, format, max_methods, max_loc, recursive, verbose
|
|
1215
|
+
path_objs, config_file, format, max_methods, max_loc, recursive, verbose, project_root=None
|
|
846
1216
|
):
|
|
847
1217
|
"""Execute SRP lint."""
|
|
848
|
-
|
|
1218
|
+
_validate_paths_exist(path_objs)
|
|
1219
|
+
orchestrator = _setup_srp_orchestrator(path_objs, config_file, verbose, project_root)
|
|
849
1220
|
_apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
|
|
850
1221
|
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive)
|
|
851
1222
|
|
|
@@ -857,7 +1228,7 @@ def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional
|
|
|
857
1228
|
|
|
858
1229
|
|
|
859
1230
|
@cli.command("dry")
|
|
860
|
-
@click.argument("paths", nargs=-1, type=click.Path(
|
|
1231
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
861
1232
|
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
862
1233
|
@format_option
|
|
863
1234
|
@click.option("--min-lines", type=int, help="Override min duplicate lines threshold")
|
|
@@ -925,6 +1296,7 @@ def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
925
1296
|
thai-lint dry --format json .
|
|
926
1297
|
"""
|
|
927
1298
|
verbose = ctx.obj.get("verbose", False)
|
|
1299
|
+
project_root = _get_project_root_from_context(ctx)
|
|
928
1300
|
|
|
929
1301
|
if not paths:
|
|
930
1302
|
paths = (".",)
|
|
@@ -933,17 +1305,34 @@ def dry( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
933
1305
|
|
|
934
1306
|
try:
|
|
935
1307
|
_execute_dry_lint(
|
|
936
|
-
path_objs,
|
|
1308
|
+
path_objs,
|
|
1309
|
+
config_file,
|
|
1310
|
+
format,
|
|
1311
|
+
min_lines,
|
|
1312
|
+
no_cache,
|
|
1313
|
+
clear_cache,
|
|
1314
|
+
recursive,
|
|
1315
|
+
verbose,
|
|
1316
|
+
project_root,
|
|
937
1317
|
)
|
|
938
1318
|
except Exception as e:
|
|
939
1319
|
_handle_linting_error(e, verbose)
|
|
940
1320
|
|
|
941
1321
|
|
|
942
1322
|
def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
943
|
-
path_objs,
|
|
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,
|
|
944
1332
|
):
|
|
945
1333
|
"""Execute DRY linting."""
|
|
946
|
-
|
|
1334
|
+
_validate_paths_exist(path_objs)
|
|
1335
|
+
orchestrator = _setup_dry_orchestrator(path_objs, config_file, verbose, project_root)
|
|
947
1336
|
_apply_dry_config_override(orchestrator, min_lines, no_cache, verbose)
|
|
948
1337
|
|
|
949
1338
|
if clear_cache:
|
|
@@ -958,14 +1347,16 @@ def _execute_dry_lint( # pylint: disable=too-many-arguments,too-many-positional
|
|
|
958
1347
|
sys.exit(1 if dry_violations else 0)
|
|
959
1348
|
|
|
960
1349
|
|
|
961
|
-
def _setup_dry_orchestrator(path_objs, config_file, verbose):
|
|
1350
|
+
def _setup_dry_orchestrator(path_objs, config_file, verbose, project_root=None):
|
|
962
1351
|
"""Set up orchestrator for DRY linting."""
|
|
963
1352
|
from src.orchestrator.core import Orchestrator
|
|
964
1353
|
from src.utils.project_root import get_project_root
|
|
965
1354
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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)
|
|
969
1360
|
|
|
970
1361
|
orchestrator = Orchestrator(project_root=project_root)
|
|
971
1362
|
|
|
@@ -1051,5 +1442,224 @@ def _run_dry_lint(orchestrator, path_objs, recursive):
|
|
|
1051
1442
|
return dry_violations
|
|
1052
1443
|
|
|
1053
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
|
+
|
|
1054
1664
|
if __name__ == "__main__":
|
|
1055
1665
|
cli()
|