thailint 0.1.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/.ai/layout.yaml +48 -0
- src/__init__.py +49 -0
- src/api.py +118 -0
- src/cli.py +698 -0
- src/config.py +386 -0
- src/core/__init__.py +17 -0
- src/core/base.py +122 -0
- src/core/registry.py +170 -0
- src/core/types.py +83 -0
- src/linter_config/__init__.py +13 -0
- src/linter_config/ignore.py +403 -0
- src/linter_config/loader.py +77 -0
- src/linters/__init__.py +4 -0
- src/linters/file_placement/__init__.py +31 -0
- src/linters/file_placement/linter.py +621 -0
- src/linters/nesting/__init__.py +87 -0
- src/linters/nesting/config.py +50 -0
- src/linters/nesting/linter.py +257 -0
- src/linters/nesting/python_analyzer.py +89 -0
- src/linters/nesting/typescript_analyzer.py +180 -0
- src/orchestrator/__init__.py +9 -0
- src/orchestrator/core.py +188 -0
- src/orchestrator/language_detector.py +81 -0
- thailint-0.1.0.dist-info/LICENSE +21 -0
- thailint-0.1.0.dist-info/METADATA +601 -0
- thailint-0.1.0.dist-info/RECORD +28 -0
- thailint-0.1.0.dist-info/WHEEL +4 -0
- thailint-0.1.0.dist-info/entry_points.txt +4 -0
src/cli.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main CLI entrypoint with Click framework for command-line interface
|
|
3
|
+
|
|
4
|
+
Scope: CLI command definitions, option parsing, and command execution coordination
|
|
5
|
+
|
|
6
|
+
Overview: Provides the main CLI application using Click decorators for command definition, option
|
|
7
|
+
parsing, and help text generation. Includes example commands (hello, config management) that
|
|
8
|
+
demonstrate best practices for CLI design including error handling, logging configuration,
|
|
9
|
+
context management, and user-friendly output. Serves as the entry point for the installed
|
|
10
|
+
CLI tool and coordinates between user input and application logic.
|
|
11
|
+
|
|
12
|
+
Dependencies: click for CLI framework, logging for structured output, pathlib for file paths
|
|
13
|
+
|
|
14
|
+
Exports: cli (main command group), hello command, config command group, file_placement command
|
|
15
|
+
|
|
16
|
+
Interfaces: Click CLI commands, configuration context via Click ctx, logging integration
|
|
17
|
+
|
|
18
|
+
Implementation: Click decorators for commands, context passing for shared state, comprehensive help text
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from src import __version__
|
|
28
|
+
from src.config import ConfigError, load_config, save_config, validate_config
|
|
29
|
+
|
|
30
|
+
# Configure module logger
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def setup_logging(verbose: bool = False):
|
|
35
|
+
"""
|
|
36
|
+
Configure logging for the CLI application.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
verbose: Enable DEBUG level logging if True, INFO otherwise.
|
|
40
|
+
"""
|
|
41
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
42
|
+
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
level=level,
|
|
45
|
+
format="%(asctime)s | %(levelname)-8s | %(message)s",
|
|
46
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
47
|
+
stream=sys.stdout,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group()
|
|
52
|
+
@click.version_option(version=__version__)
|
|
53
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
54
|
+
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def cli(ctx, verbose: bool, config: str | None):
|
|
57
|
+
"""
|
|
58
|
+
thai-lint - AI code linter and governance tool
|
|
59
|
+
|
|
60
|
+
Lint and governance for AI-generated code across multiple languages.
|
|
61
|
+
Identifies common mistakes, anti-patterns, and security issues.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
|
|
65
|
+
\b
|
|
66
|
+
# Lint current directory for file placement issues
|
|
67
|
+
thai-lint file-placement .
|
|
68
|
+
|
|
69
|
+
\b
|
|
70
|
+
# Lint with custom config
|
|
71
|
+
thai-lint file-placement --config .thailint.yaml src/
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
# Get JSON output
|
|
75
|
+
thai-lint file-placement --format json .
|
|
76
|
+
|
|
77
|
+
\b
|
|
78
|
+
# Show help
|
|
79
|
+
thai-lint --help
|
|
80
|
+
"""
|
|
81
|
+
# Ensure context object exists
|
|
82
|
+
ctx.ensure_object(dict)
|
|
83
|
+
|
|
84
|
+
# Setup logging
|
|
85
|
+
setup_logging(verbose)
|
|
86
|
+
|
|
87
|
+
# Load configuration
|
|
88
|
+
try:
|
|
89
|
+
if config:
|
|
90
|
+
ctx.obj["config"] = load_config(Path(config))
|
|
91
|
+
ctx.obj["config_path"] = Path(config)
|
|
92
|
+
else:
|
|
93
|
+
ctx.obj["config"] = load_config()
|
|
94
|
+
ctx.obj["config_path"] = None
|
|
95
|
+
|
|
96
|
+
logger.debug("Configuration loaded successfully")
|
|
97
|
+
except ConfigError as e:
|
|
98
|
+
click.echo(f"Error loading configuration: {e}", err=True)
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
|
|
101
|
+
ctx.obj["verbose"] = verbose
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command()
|
|
105
|
+
@click.option("--name", "-n", default="World", help="Name to greet")
|
|
106
|
+
@click.option("--uppercase", "-u", is_flag=True, help="Convert greeting to uppercase")
|
|
107
|
+
@click.pass_context
|
|
108
|
+
def hello(ctx, name: str, uppercase: bool):
|
|
109
|
+
"""
|
|
110
|
+
Print a greeting message.
|
|
111
|
+
|
|
112
|
+
This is a simple example command demonstrating CLI basics.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
\b
|
|
117
|
+
# Basic greeting
|
|
118
|
+
thai-lint hello
|
|
119
|
+
|
|
120
|
+
\b
|
|
121
|
+
# Custom name
|
|
122
|
+
thai-lint hello --name Alice
|
|
123
|
+
|
|
124
|
+
\b
|
|
125
|
+
# Uppercase output
|
|
126
|
+
thai-lint hello --name Bob --uppercase
|
|
127
|
+
"""
|
|
128
|
+
config = ctx.obj["config"]
|
|
129
|
+
verbose = ctx.obj.get("verbose", False)
|
|
130
|
+
|
|
131
|
+
# Get greeting from config or use default
|
|
132
|
+
greeting_template = config.get("greeting", "Hello")
|
|
133
|
+
|
|
134
|
+
# Build greeting message
|
|
135
|
+
message = f"{greeting_template}, {name}!"
|
|
136
|
+
|
|
137
|
+
if uppercase:
|
|
138
|
+
message = message.upper()
|
|
139
|
+
|
|
140
|
+
# Output greeting
|
|
141
|
+
click.echo(message)
|
|
142
|
+
|
|
143
|
+
if verbose:
|
|
144
|
+
logger.info(f"Greeted {name} with template '{greeting_template}'")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cli.group()
|
|
148
|
+
def config():
|
|
149
|
+
"""Configuration management commands."""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@config.command("show")
|
|
154
|
+
@click.option(
|
|
155
|
+
"--format",
|
|
156
|
+
"-f",
|
|
157
|
+
type=click.Choice(["text", "json", "yaml"]),
|
|
158
|
+
default="text",
|
|
159
|
+
help="Output format",
|
|
160
|
+
)
|
|
161
|
+
@click.pass_context
|
|
162
|
+
def config_show(ctx, format: str):
|
|
163
|
+
"""
|
|
164
|
+
Display current configuration.
|
|
165
|
+
|
|
166
|
+
Shows all configuration values in the specified format.
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
|
|
170
|
+
\b
|
|
171
|
+
# Show as text
|
|
172
|
+
thai-lint config show
|
|
173
|
+
|
|
174
|
+
\b
|
|
175
|
+
# Show as JSON
|
|
176
|
+
thai-lint config show --format json
|
|
177
|
+
|
|
178
|
+
\b
|
|
179
|
+
# Show as YAML
|
|
180
|
+
thai-lint config show --format yaml
|
|
181
|
+
"""
|
|
182
|
+
cfg = ctx.obj["config"]
|
|
183
|
+
|
|
184
|
+
formatters = {
|
|
185
|
+
"json": _format_config_json,
|
|
186
|
+
"yaml": _format_config_yaml,
|
|
187
|
+
"text": _format_config_text,
|
|
188
|
+
}
|
|
189
|
+
formatters[format](cfg)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _format_config_json(cfg: dict) -> None:
|
|
193
|
+
"""Format configuration as JSON."""
|
|
194
|
+
import json
|
|
195
|
+
|
|
196
|
+
click.echo(json.dumps(cfg, indent=2))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _format_config_yaml(cfg: dict) -> None:
|
|
200
|
+
"""Format configuration as YAML."""
|
|
201
|
+
import yaml
|
|
202
|
+
|
|
203
|
+
click.echo(yaml.dump(cfg, default_flow_style=False, sort_keys=False))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _format_config_text(cfg: dict) -> None:
|
|
207
|
+
"""Format configuration as text."""
|
|
208
|
+
click.echo("Current Configuration:")
|
|
209
|
+
click.echo("-" * 40)
|
|
210
|
+
for key, value in cfg.items():
|
|
211
|
+
click.echo(f"{key:20} : {value}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@config.command("get")
|
|
215
|
+
@click.argument("key")
|
|
216
|
+
@click.pass_context
|
|
217
|
+
def config_get(ctx, key: str):
|
|
218
|
+
"""
|
|
219
|
+
Get specific configuration value.
|
|
220
|
+
|
|
221
|
+
KEY: Configuration key to retrieve
|
|
222
|
+
|
|
223
|
+
Examples:
|
|
224
|
+
|
|
225
|
+
\b
|
|
226
|
+
# Get log level
|
|
227
|
+
thai-lint config get log_level
|
|
228
|
+
|
|
229
|
+
\b
|
|
230
|
+
# Get greeting template
|
|
231
|
+
thai-lint config get greeting
|
|
232
|
+
"""
|
|
233
|
+
cfg = ctx.obj["config"]
|
|
234
|
+
|
|
235
|
+
if key not in cfg:
|
|
236
|
+
click.echo(f"Configuration key not found: {key}", err=True)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
click.echo(cfg[key])
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _convert_value_type(value: str):
|
|
243
|
+
"""Convert string value to appropriate type."""
|
|
244
|
+
if value.lower() in ["true", "false"]:
|
|
245
|
+
return value.lower() == "true"
|
|
246
|
+
if value.isdigit():
|
|
247
|
+
return int(value)
|
|
248
|
+
if value.replace(".", "", 1).isdigit() and value.count(".") == 1:
|
|
249
|
+
return float(value)
|
|
250
|
+
return value
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _validate_and_report_errors(cfg: dict):
|
|
254
|
+
"""Validate configuration and report errors."""
|
|
255
|
+
is_valid, errors = validate_config(cfg)
|
|
256
|
+
if not is_valid:
|
|
257
|
+
click.echo("Invalid configuration:", err=True)
|
|
258
|
+
for error in errors:
|
|
259
|
+
click.echo(f" - {error}", err=True)
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _save_and_report_success(cfg: dict, key: str, value, config_path, verbose: bool):
|
|
264
|
+
"""Save configuration and report success."""
|
|
265
|
+
save_config(cfg, config_path)
|
|
266
|
+
click.echo(f"✓ Set {key} = {value}")
|
|
267
|
+
if verbose:
|
|
268
|
+
logger.info(f"Configuration updated: {key}={value}")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@config.command("set")
|
|
272
|
+
@click.argument("key")
|
|
273
|
+
@click.argument("value")
|
|
274
|
+
@click.pass_context
|
|
275
|
+
def config_set(ctx, key: str, value: str):
|
|
276
|
+
"""
|
|
277
|
+
Set configuration value.
|
|
278
|
+
|
|
279
|
+
KEY: Configuration key to set
|
|
280
|
+
|
|
281
|
+
VALUE: New value for the key
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
\b
|
|
286
|
+
# Set log level
|
|
287
|
+
thai-lint config set log_level DEBUG
|
|
288
|
+
|
|
289
|
+
\b
|
|
290
|
+
# Set greeting template
|
|
291
|
+
thai-lint config set greeting "Hi"
|
|
292
|
+
|
|
293
|
+
\b
|
|
294
|
+
# Set numeric value
|
|
295
|
+
thai-lint config set max_retries 5
|
|
296
|
+
"""
|
|
297
|
+
cfg = ctx.obj["config"]
|
|
298
|
+
converted_value = _convert_value_type(value)
|
|
299
|
+
cfg[key] = converted_value
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
_validate_and_report_errors(cfg)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
click.echo(f"Validation error: {e}", err=True)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
config_path = ctx.obj.get("config_path")
|
|
309
|
+
verbose = ctx.obj.get("verbose", False)
|
|
310
|
+
_save_and_report_success(cfg, key, converted_value, config_path, verbose)
|
|
311
|
+
except ConfigError as e:
|
|
312
|
+
click.echo(f"Error saving configuration: {e}", err=True)
|
|
313
|
+
sys.exit(1)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@config.command("reset")
|
|
317
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
318
|
+
@click.pass_context
|
|
319
|
+
def config_reset(ctx, yes: bool):
|
|
320
|
+
"""
|
|
321
|
+
Reset configuration to defaults.
|
|
322
|
+
|
|
323
|
+
Examples:
|
|
324
|
+
|
|
325
|
+
\b
|
|
326
|
+
# Reset with confirmation
|
|
327
|
+
thai-lint config reset
|
|
328
|
+
|
|
329
|
+
\b
|
|
330
|
+
# Reset without confirmation
|
|
331
|
+
thai-lint config reset --yes
|
|
332
|
+
"""
|
|
333
|
+
if not yes:
|
|
334
|
+
click.confirm("Reset configuration to defaults?", abort=True)
|
|
335
|
+
|
|
336
|
+
from src.config import DEFAULT_CONFIG
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
config_path = ctx.obj.get("config_path")
|
|
340
|
+
save_config(DEFAULT_CONFIG.copy(), config_path)
|
|
341
|
+
click.echo("✓ Configuration reset to defaults")
|
|
342
|
+
|
|
343
|
+
if ctx.obj.get("verbose"):
|
|
344
|
+
logger.info("Configuration reset to defaults")
|
|
345
|
+
except ConfigError as e:
|
|
346
|
+
click.echo(f"Error resetting configuration: {e}", err=True)
|
|
347
|
+
sys.exit(1)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@cli.command("file-placement")
|
|
351
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
352
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
353
|
+
@click.option("--rules", "-r", help="Inline JSON rules configuration")
|
|
354
|
+
@click.option(
|
|
355
|
+
"--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
|
|
356
|
+
)
|
|
357
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
358
|
+
@click.pass_context
|
|
359
|
+
def file_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-statements
|
|
360
|
+
ctx, path: str, config_file: str | None, rules: str | None, format: str, recursive: bool
|
|
361
|
+
):
|
|
362
|
+
# Justification for Pylint disables:
|
|
363
|
+
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 4 options = 6 params
|
|
364
|
+
# - too-many-locals/statements: Complex CLI logic for config, linting, and output formatting
|
|
365
|
+
# All parameters and logic are necessary for flexible CLI usage.
|
|
366
|
+
"""
|
|
367
|
+
Lint files for proper file placement.
|
|
368
|
+
|
|
369
|
+
Checks that files are placed in appropriate directories according to
|
|
370
|
+
configured rules and patterns.
|
|
371
|
+
|
|
372
|
+
PATH: File or directory to lint (defaults to current directory)
|
|
373
|
+
|
|
374
|
+
Examples:
|
|
375
|
+
|
|
376
|
+
\b
|
|
377
|
+
# Lint current directory
|
|
378
|
+
thai-lint file-placement
|
|
379
|
+
|
|
380
|
+
\b
|
|
381
|
+
# Lint specific directory
|
|
382
|
+
thai-lint file-placement src/
|
|
383
|
+
|
|
384
|
+
\b
|
|
385
|
+
# Use custom config
|
|
386
|
+
thai-lint file-placement --config rules.json .
|
|
387
|
+
|
|
388
|
+
\b
|
|
389
|
+
# Inline JSON rules
|
|
390
|
+
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
391
|
+
"""
|
|
392
|
+
verbose = ctx.obj.get("verbose", False)
|
|
393
|
+
path_obj = Path(path)
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
_execute_file_placement_lint(path_obj, config_file, rules, format, recursive, verbose)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
_handle_linting_error(e, verbose)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
402
|
+
path_obj, config_file, rules, format, recursive, verbose
|
|
403
|
+
):
|
|
404
|
+
"""Execute file placement linting."""
|
|
405
|
+
orchestrator = _setup_orchestrator(path_obj, config_file, rules, verbose)
|
|
406
|
+
violations = _execute_linting(orchestrator, path_obj, recursive)
|
|
407
|
+
|
|
408
|
+
if verbose:
|
|
409
|
+
logger.info(f"Found {len(violations)} violation(s)")
|
|
410
|
+
|
|
411
|
+
_output_violations(violations, format)
|
|
412
|
+
sys.exit(1 if violations else 0)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _handle_linting_error(error: Exception, verbose: bool) -> None:
|
|
416
|
+
"""Handle linting errors."""
|
|
417
|
+
click.echo(f"Error during linting: {error}", err=True)
|
|
418
|
+
if verbose:
|
|
419
|
+
logger.exception("Linting failed with exception")
|
|
420
|
+
sys.exit(2)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _setup_orchestrator(path_obj, config_file, rules, verbose):
|
|
424
|
+
"""Set up and configure the orchestrator."""
|
|
425
|
+
from src.orchestrator.core import Orchestrator
|
|
426
|
+
|
|
427
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
428
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
429
|
+
|
|
430
|
+
if rules:
|
|
431
|
+
_apply_inline_rules(orchestrator, rules, verbose)
|
|
432
|
+
elif config_file:
|
|
433
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
434
|
+
|
|
435
|
+
return orchestrator
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _apply_inline_rules(orchestrator, rules, verbose):
|
|
439
|
+
"""Parse and apply inline JSON rules."""
|
|
440
|
+
rules_config = _parse_json_rules(rules)
|
|
441
|
+
orchestrator.config.update(rules_config)
|
|
442
|
+
_write_layout_config(orchestrator, rules_config, verbose)
|
|
443
|
+
_log_applied_rules(rules_config, verbose)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _parse_json_rules(rules: str) -> dict:
|
|
447
|
+
"""Parse JSON rules string, exit on error."""
|
|
448
|
+
import json
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
return json.loads(rules)
|
|
452
|
+
except json.JSONDecodeError as e:
|
|
453
|
+
click.echo(f"Error: Invalid JSON in --rules: {e}", err=True)
|
|
454
|
+
sys.exit(2)
|
|
455
|
+
|
|
456
|
+
|
|
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
|
+
def _log_applied_rules(rules_config: dict, verbose: bool) -> None:
|
|
492
|
+
"""Log applied rules if verbose."""
|
|
493
|
+
if verbose:
|
|
494
|
+
logger.debug(f"Applied inline rules: {rules_config}")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _load_config_file(orchestrator, config_file, verbose):
|
|
498
|
+
"""Load configuration from external file."""
|
|
499
|
+
config_path = Path(config_file)
|
|
500
|
+
if not config_path.exists():
|
|
501
|
+
click.echo(f"Error: Config file not found: {config_file}", err=True)
|
|
502
|
+
sys.exit(2)
|
|
503
|
+
|
|
504
|
+
# Load config into orchestrator
|
|
505
|
+
orchestrator.config = orchestrator.config_loader.load(config_path)
|
|
506
|
+
|
|
507
|
+
# Also copy to .ai/layout.yaml for file-placement linter
|
|
508
|
+
_write_loaded_config_to_layout(orchestrator, config_file, verbose)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _write_loaded_config_to_layout(orchestrator, config_file: str, verbose: bool) -> None:
|
|
512
|
+
"""Write loaded config to .ai/layout.yaml if possible."""
|
|
513
|
+
ai_dir = orchestrator.project_root / ".ai"
|
|
514
|
+
layout_file = ai_dir / "layout.yaml"
|
|
515
|
+
|
|
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
|
+
|
|
522
|
+
|
|
523
|
+
def _write_config_yaml(ai_dir, layout_file, config):
|
|
524
|
+
"""Write config to YAML file."""
|
|
525
|
+
import yaml
|
|
526
|
+
|
|
527
|
+
ai_dir.mkdir(exist_ok=True)
|
|
528
|
+
with layout_file.open("w", encoding="utf-8") as f:
|
|
529
|
+
yaml.dump(config, f)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _log_config_loaded(config_file, layout_file, verbose):
|
|
533
|
+
"""Log config loaded and written."""
|
|
534
|
+
if verbose:
|
|
535
|
+
logger.debug(f"Loaded config from: {config_file}")
|
|
536
|
+
logger.debug(f"Written layout config to: {layout_file}")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _execute_linting(orchestrator, path_obj, recursive):
|
|
540
|
+
"""Execute linting on file or directory."""
|
|
541
|
+
if path_obj.is_file():
|
|
542
|
+
return orchestrator.lint_file(path_obj)
|
|
543
|
+
return orchestrator.lint_directory(path_obj, recursive=recursive)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _output_violations(violations, format):
|
|
547
|
+
"""Format and output violations."""
|
|
548
|
+
if format == "json":
|
|
549
|
+
_output_json(violations)
|
|
550
|
+
else:
|
|
551
|
+
_output_text(violations)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _output_json(violations):
|
|
555
|
+
"""Output violations in JSON format."""
|
|
556
|
+
import json
|
|
557
|
+
|
|
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
|
+
|
|
574
|
+
|
|
575
|
+
def _output_text(violations):
|
|
576
|
+
"""Output violations in text format."""
|
|
577
|
+
if not violations:
|
|
578
|
+
click.echo("✓ No violations found")
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
click.echo(f"Found {len(violations)} violation(s):\n")
|
|
582
|
+
for v in violations:
|
|
583
|
+
_print_violation(v)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _print_violation(v) -> None:
|
|
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()
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _setup_nesting_orchestrator(path_obj: Path, config_file: str | None, verbose: bool):
|
|
597
|
+
"""Set up orchestrator for nesting command."""
|
|
598
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
599
|
+
from src.orchestrator.core import Orchestrator
|
|
600
|
+
|
|
601
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
602
|
+
|
|
603
|
+
if config_file:
|
|
604
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
605
|
+
|
|
606
|
+
return orchestrator
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _apply_nesting_config_override(orchestrator, max_depth: int | None, verbose: bool):
|
|
610
|
+
"""Apply max_depth override to orchestrator config."""
|
|
611
|
+
if max_depth is None:
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
if "nesting" not in orchestrator.config:
|
|
615
|
+
orchestrator.config["nesting"] = {}
|
|
616
|
+
orchestrator.config["nesting"]["max_nesting_depth"] = max_depth
|
|
617
|
+
|
|
618
|
+
if verbose:
|
|
619
|
+
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _run_nesting_lint(orchestrator, path_obj: Path, recursive: bool):
|
|
623
|
+
"""Execute nesting lint on file or directory."""
|
|
624
|
+
if path_obj.is_file():
|
|
625
|
+
violations = orchestrator.lint_file(path_obj)
|
|
626
|
+
else:
|
|
627
|
+
violations = orchestrator.lint_directory(path_obj, recursive=recursive)
|
|
628
|
+
|
|
629
|
+
return [v for v in violations if "nesting" in v.rule_id]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@cli.command("nesting")
|
|
633
|
+
@click.argument("path", type=click.Path(exists=True), default=".")
|
|
634
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
635
|
+
@click.option(
|
|
636
|
+
"--format", "-f", type=click.Choice(["text", "json"]), default="text", help="Output format"
|
|
637
|
+
)
|
|
638
|
+
@click.option("--max-depth", type=int, help="Override max nesting depth (default: 4)")
|
|
639
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
640
|
+
@click.pass_context
|
|
641
|
+
def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
642
|
+
ctx, path: str, config_file: str | None, format: str, max_depth: int | None, recursive: bool
|
|
643
|
+
):
|
|
644
|
+
"""Check for excessive nesting depth in code.
|
|
645
|
+
|
|
646
|
+
Analyzes Python and TypeScript files for deeply nested code structures
|
|
647
|
+
(if/for/while/try statements) and reports violations.
|
|
648
|
+
|
|
649
|
+
PATH: File or directory to lint (defaults to current directory)
|
|
650
|
+
|
|
651
|
+
Examples:
|
|
652
|
+
|
|
653
|
+
\b
|
|
654
|
+
# Check current directory
|
|
655
|
+
thai-lint nesting
|
|
656
|
+
|
|
657
|
+
\b
|
|
658
|
+
# Check specific directory
|
|
659
|
+
thai-lint nesting src/
|
|
660
|
+
|
|
661
|
+
\b
|
|
662
|
+
# Use custom max depth
|
|
663
|
+
thai-lint nesting --max-depth 3 src/
|
|
664
|
+
|
|
665
|
+
\b
|
|
666
|
+
# Get JSON output
|
|
667
|
+
thai-lint nesting --format json .
|
|
668
|
+
|
|
669
|
+
\b
|
|
670
|
+
# Use custom config file
|
|
671
|
+
thai-lint nesting --config .thailint.yaml src/
|
|
672
|
+
"""
|
|
673
|
+
verbose = ctx.obj.get("verbose", False)
|
|
674
|
+
path_obj = Path(path)
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
_execute_nesting_lint(path_obj, config_file, format, max_depth, recursive, verbose)
|
|
678
|
+
except Exception as e:
|
|
679
|
+
_handle_linting_error(e, verbose)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
683
|
+
path_obj, config_file, format, max_depth, recursive, verbose
|
|
684
|
+
):
|
|
685
|
+
"""Execute nesting lint."""
|
|
686
|
+
orchestrator = _setup_nesting_orchestrator(path_obj, config_file, verbose)
|
|
687
|
+
_apply_nesting_config_override(orchestrator, max_depth, verbose)
|
|
688
|
+
nesting_violations = _run_nesting_lint(orchestrator, path_obj, recursive)
|
|
689
|
+
|
|
690
|
+
if verbose:
|
|
691
|
+
logger.info(f"Found {len(nesting_violations)} nesting violation(s)")
|
|
692
|
+
|
|
693
|
+
_output_violations(nesting_violations, format)
|
|
694
|
+
sys.exit(1 if nesting_violations else 0)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
if __name__ == "__main__":
|
|
698
|
+
cli()
|