thailint 0.13.0__py3-none-any.whl → 0.14.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/linters/__init__.py +6 -0
- src/cli/linters/code_patterns.py +75 -333
- src/cli/linters/code_smells.py +47 -168
- src/cli/linters/documentation.py +21 -98
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +23 -21
- src/cli/linters/structure_quality.py +25 -21
- src/core/linter_utils.py +91 -6
- src/linters/file_header/atemporal_detector.py +54 -40
- src/linters/file_header/config.py +14 -0
- src/linters/lazy_ignores/python_analyzer.py +5 -1
- src/linters/lazy_ignores/types.py +2 -0
- src/linters/method_property/config.py +0 -1
- src/linters/method_property/linter.py +0 -6
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/templates/thailint_config_template.yaml +30 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/METADATA +3 -2
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/RECORD +32 -22
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/code_smells.py
CHANGED
|
@@ -18,14 +18,10 @@ Interfaces: Click CLI commands registered to main CLI group
|
|
|
18
18
|
|
|
19
19
|
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
20
20
|
|
|
21
|
-
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
22
|
-
structure across all linter commands. This is intentional design for consistency.
|
|
23
|
-
|
|
24
21
|
Suppressions:
|
|
25
|
-
too-many-arguments: Click commands
|
|
26
|
-
|
|
22
|
+
- too-many-arguments,too-many-positional-arguments: Click commands with custom options require
|
|
23
|
+
many parameters by framework design (dry command has 8 params for extra options)
|
|
27
24
|
"""
|
|
28
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
29
25
|
|
|
30
26
|
import logging
|
|
31
27
|
import sys
|
|
@@ -35,7 +31,12 @@ from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
35
31
|
import click
|
|
36
32
|
import yaml
|
|
37
33
|
|
|
38
|
-
from src.cli.linters.shared import
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ExecuteParams,
|
|
36
|
+
create_linter_command,
|
|
37
|
+
ensure_config_section,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
39
40
|
from src.cli.main import cli
|
|
40
41
|
from src.cli.utils import (
|
|
41
42
|
execute_linting_on_paths,
|
|
@@ -56,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
# =============================================================================
|
|
59
|
-
# DRY Command
|
|
60
|
+
# DRY Command (custom options - cannot use create_linter_command)
|
|
60
61
|
# =============================================================================
|
|
61
62
|
|
|
62
63
|
|
|
@@ -261,92 +262,32 @@ def _run_magic_numbers_lint(
|
|
|
261
262
|
return [v for v in all_violations if "magic-number" in v.rule_id]
|
|
262
263
|
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
266
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
267
|
-
@format_option
|
|
268
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
269
|
-
@click.pass_context
|
|
270
|
-
def magic_numbers( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
271
|
-
ctx: click.Context,
|
|
272
|
-
paths: tuple[str, ...],
|
|
273
|
-
config_file: str | None,
|
|
274
|
-
format: str,
|
|
275
|
-
recursive: bool,
|
|
276
|
-
) -> None:
|
|
277
|
-
"""Check for magic numbers in code.
|
|
278
|
-
|
|
279
|
-
Detects unnamed numeric literals in Python and TypeScript/JavaScript code
|
|
280
|
-
that should be extracted as named constants for better readability.
|
|
281
|
-
|
|
282
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
283
|
-
|
|
284
|
-
Examples:
|
|
285
|
-
|
|
286
|
-
\b
|
|
287
|
-
# Check current directory (all files recursively)
|
|
288
|
-
thai-lint magic-numbers
|
|
289
|
-
|
|
290
|
-
\b
|
|
291
|
-
# Check specific directory
|
|
292
|
-
thai-lint magic-numbers src/
|
|
293
|
-
|
|
294
|
-
\b
|
|
295
|
-
# Check single file
|
|
296
|
-
thai-lint magic-numbers src/app.py
|
|
297
|
-
|
|
298
|
-
\b
|
|
299
|
-
# Check multiple files
|
|
300
|
-
thai-lint magic-numbers src/app.py src/utils.py tests/test_app.py
|
|
301
|
-
|
|
302
|
-
\b
|
|
303
|
-
# Check mix of files and directories
|
|
304
|
-
thai-lint magic-numbers src/app.py tests/
|
|
305
|
-
|
|
306
|
-
\b
|
|
307
|
-
# Get JSON output
|
|
308
|
-
thai-lint magic-numbers --format json .
|
|
309
|
-
|
|
310
|
-
\b
|
|
311
|
-
# Use custom config file
|
|
312
|
-
thai-lint magic-numbers --config .thailint.yaml src/
|
|
313
|
-
"""
|
|
314
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
315
|
-
project_root = get_project_root_from_context(ctx)
|
|
316
|
-
|
|
317
|
-
if not paths:
|
|
318
|
-
paths = (".",)
|
|
319
|
-
|
|
320
|
-
path_objs = [Path(p) for p in paths]
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
_execute_magic_numbers_lint(
|
|
324
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
325
|
-
)
|
|
326
|
-
except Exception as e:
|
|
327
|
-
handle_linting_error(e, verbose)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def _execute_magic_numbers_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
331
|
-
path_objs: list[Path],
|
|
332
|
-
config_file: str | None,
|
|
333
|
-
format: str,
|
|
334
|
-
recursive: bool,
|
|
335
|
-
verbose: bool,
|
|
336
|
-
project_root: Path | None = None,
|
|
337
|
-
) -> NoReturn:
|
|
265
|
+
def _execute_magic_numbers_lint(params: ExecuteParams) -> NoReturn:
|
|
338
266
|
"""Execute magic-numbers lint."""
|
|
339
|
-
validate_paths_exist(path_objs)
|
|
340
|
-
orchestrator = _setup_magic_numbers_orchestrator(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
267
|
+
validate_paths_exist(params.path_objs)
|
|
268
|
+
orchestrator = _setup_magic_numbers_orchestrator(
|
|
269
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
270
|
+
)
|
|
271
|
+
magic_numbers_violations = _run_magic_numbers_lint(
|
|
272
|
+
orchestrator, params.path_objs, params.recursive
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if params.verbose:
|
|
344
276
|
logger.info(f"Found {len(magic_numbers_violations)} magic number violation(s)")
|
|
345
277
|
|
|
346
|
-
format_violations(magic_numbers_violations, format)
|
|
278
|
+
format_violations(magic_numbers_violations, params.format)
|
|
347
279
|
sys.exit(1 if magic_numbers_violations else 0)
|
|
348
280
|
|
|
349
281
|
|
|
282
|
+
magic_numbers = create_linter_command(
|
|
283
|
+
"magic-numbers",
|
|
284
|
+
_execute_magic_numbers_lint,
|
|
285
|
+
"Check for magic numbers in code.",
|
|
286
|
+
"Detects unnamed numeric literals in Python and TypeScript/JavaScript code\n"
|
|
287
|
+
" that should be extracted as named constants for better readability.",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
350
291
|
# =============================================================================
|
|
351
292
|
# Stringly-Typed Command
|
|
352
293
|
# =============================================================================
|
|
@@ -367,88 +308,26 @@ def _run_stringly_typed_lint(
|
|
|
367
308
|
return [v for v in all_violations if "stringly-typed" in v.rule_id]
|
|
368
309
|
|
|
369
310
|
|
|
370
|
-
|
|
371
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
372
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
373
|
-
@format_option
|
|
374
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
375
|
-
@click.pass_context
|
|
376
|
-
def stringly_typed( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
377
|
-
ctx: click.Context,
|
|
378
|
-
paths: tuple[str, ...],
|
|
379
|
-
config_file: str | None,
|
|
380
|
-
format: str,
|
|
381
|
-
recursive: bool,
|
|
382
|
-
) -> None:
|
|
383
|
-
"""Check for stringly-typed patterns in code.
|
|
384
|
-
|
|
385
|
-
Detects string patterns in Python and TypeScript/JavaScript code that should
|
|
386
|
-
use enums or typed alternatives. Finds membership validation, equality chains,
|
|
387
|
-
and function calls with limited string values across multiple files.
|
|
388
|
-
|
|
389
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
390
|
-
|
|
391
|
-
Examples:
|
|
392
|
-
|
|
393
|
-
\b
|
|
394
|
-
# Check current directory (all files recursively)
|
|
395
|
-
thai-lint stringly-typed
|
|
396
|
-
|
|
397
|
-
\b
|
|
398
|
-
# Check specific directory
|
|
399
|
-
thai-lint stringly-typed src/
|
|
400
|
-
|
|
401
|
-
\b
|
|
402
|
-
# Check single file
|
|
403
|
-
thai-lint stringly-typed src/handlers.py
|
|
404
|
-
|
|
405
|
-
\b
|
|
406
|
-
# Check multiple files
|
|
407
|
-
thai-lint stringly-typed src/handlers.py src/services.py
|
|
408
|
-
|
|
409
|
-
\b
|
|
410
|
-
# Get JSON output
|
|
411
|
-
thai-lint stringly-typed --format json .
|
|
412
|
-
|
|
413
|
-
\b
|
|
414
|
-
# Get SARIF output for IDE integration
|
|
415
|
-
thai-lint stringly-typed --format sarif .
|
|
416
|
-
|
|
417
|
-
\b
|
|
418
|
-
# Use custom config file
|
|
419
|
-
thai-lint stringly-typed --config .thailint.yaml src/
|
|
420
|
-
"""
|
|
421
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
422
|
-
project_root = get_project_root_from_context(ctx)
|
|
423
|
-
|
|
424
|
-
if not paths:
|
|
425
|
-
paths = (".",)
|
|
426
|
-
|
|
427
|
-
path_objs = [Path(p) for p in paths]
|
|
428
|
-
|
|
429
|
-
try:
|
|
430
|
-
_execute_stringly_typed_lint(
|
|
431
|
-
path_objs, config_file, format, recursive, verbose, project_root
|
|
432
|
-
)
|
|
433
|
-
except Exception as e:
|
|
434
|
-
handle_linting_error(e, verbose)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def _execute_stringly_typed_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
438
|
-
path_objs: list[Path],
|
|
439
|
-
config_file: str | None,
|
|
440
|
-
format: str,
|
|
441
|
-
recursive: bool,
|
|
442
|
-
verbose: bool,
|
|
443
|
-
project_root: Path | None = None,
|
|
444
|
-
) -> NoReturn:
|
|
311
|
+
def _execute_stringly_typed_lint(params: ExecuteParams) -> NoReturn:
|
|
445
312
|
"""Execute stringly-typed lint."""
|
|
446
|
-
validate_paths_exist(path_objs)
|
|
447
|
-
orchestrator = _setup_stringly_typed_orchestrator(
|
|
448
|
-
|
|
313
|
+
validate_paths_exist(params.path_objs)
|
|
314
|
+
orchestrator = _setup_stringly_typed_orchestrator(
|
|
315
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
316
|
+
)
|
|
317
|
+
stringly_violations = _run_stringly_typed_lint(orchestrator, params.path_objs, params.recursive)
|
|
449
318
|
|
|
450
|
-
if verbose:
|
|
319
|
+
if params.verbose:
|
|
451
320
|
logger.info(f"Found {len(stringly_violations)} stringly-typed violation(s)")
|
|
452
321
|
|
|
453
|
-
format_violations(stringly_violations, format)
|
|
322
|
+
format_violations(stringly_violations, params.format)
|
|
454
323
|
sys.exit(1 if stringly_violations else 0)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
stringly_typed = create_linter_command(
|
|
327
|
+
"stringly-typed",
|
|
328
|
+
_execute_stringly_typed_lint,
|
|
329
|
+
"Check for stringly-typed patterns in code.",
|
|
330
|
+
"Detects string patterns in Python and TypeScript/JavaScript code that should\n"
|
|
331
|
+
" use enums or typed alternatives. Finds membership validation, equality chains,\n"
|
|
332
|
+
" and function calls with limited string values across multiple files.",
|
|
333
|
+
)
|
src/cli/linters/documentation.py
CHANGED
|
@@ -16,31 +16,15 @@ Exports: file_header command
|
|
|
16
16
|
Interfaces: Click CLI commands registered to main CLI group
|
|
17
17
|
|
|
18
18
|
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
19
|
-
|
|
20
|
-
SRP Exception: CLI command modules follow Click framework patterns requiring similar command
|
|
21
|
-
structure across all linter commands. This is intentional design for consistency.
|
|
22
|
-
|
|
23
|
-
Suppressions:
|
|
24
|
-
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
19
|
"""
|
|
26
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
27
20
|
|
|
28
21
|
import logging
|
|
29
22
|
import sys
|
|
30
23
|
from pathlib import Path
|
|
31
24
|
from typing import TYPE_CHECKING, NoReturn
|
|
32
25
|
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
from src.cli.main import cli
|
|
36
|
-
from src.cli.utils import (
|
|
37
|
-
execute_linting_on_paths,
|
|
38
|
-
format_option,
|
|
39
|
-
get_project_root_from_context,
|
|
40
|
-
handle_linting_error,
|
|
41
|
-
setup_base_orchestrator,
|
|
42
|
-
validate_paths_exist,
|
|
43
|
-
)
|
|
26
|
+
from src.cli.linters.shared import ExecuteParams, create_linter_command
|
|
27
|
+
from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
|
|
44
28
|
from src.core.cli_utils import format_violations
|
|
45
29
|
from src.core.types import Violation
|
|
46
30
|
|
|
@@ -71,88 +55,27 @@ def _run_file_header_lint(
|
|
|
71
55
|
return [v for v in all_violations if "file-header" in v.rule_id]
|
|
72
56
|
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
@click.argument("paths", nargs=-1, type=click.Path())
|
|
76
|
-
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
77
|
-
@format_option
|
|
78
|
-
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
79
|
-
@click.pass_context
|
|
80
|
-
def file_header(
|
|
81
|
-
ctx: click.Context,
|
|
82
|
-
paths: tuple[str, ...],
|
|
83
|
-
config_file: str | None,
|
|
84
|
-
format: str,
|
|
85
|
-
recursive: bool,
|
|
86
|
-
) -> None:
|
|
87
|
-
"""Check file headers for mandatory fields and atemporal language.
|
|
88
|
-
|
|
89
|
-
Validates that source files have proper documentation headers containing
|
|
90
|
-
required fields (Purpose, Scope, Overview, etc.) and don't use temporal
|
|
91
|
-
language (dates, "currently", "now", etc.).
|
|
92
|
-
|
|
93
|
-
Supports Python, TypeScript, JavaScript, Bash, Markdown, and CSS files.
|
|
94
|
-
|
|
95
|
-
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
96
|
-
|
|
97
|
-
Examples:
|
|
98
|
-
|
|
99
|
-
\b
|
|
100
|
-
# Check current directory (all files recursively)
|
|
101
|
-
thai-lint file-header
|
|
102
|
-
|
|
103
|
-
\b
|
|
104
|
-
# Check specific directory
|
|
105
|
-
thai-lint file-header src/
|
|
106
|
-
|
|
107
|
-
\b
|
|
108
|
-
# Check single file
|
|
109
|
-
thai-lint file-header src/cli.py
|
|
110
|
-
|
|
111
|
-
\b
|
|
112
|
-
# Check multiple files
|
|
113
|
-
thai-lint file-header src/cli.py src/api.py tests/
|
|
114
|
-
|
|
115
|
-
\b
|
|
116
|
-
# Get JSON output
|
|
117
|
-
thai-lint file-header --format json .
|
|
118
|
-
|
|
119
|
-
\b
|
|
120
|
-
# Get SARIF output for CI/CD integration
|
|
121
|
-
thai-lint file-header --format sarif src/
|
|
122
|
-
|
|
123
|
-
\b
|
|
124
|
-
# Use custom config file
|
|
125
|
-
thai-lint file-header --config .thailint.yaml src/
|
|
126
|
-
"""
|
|
127
|
-
verbose: bool = ctx.obj.get("verbose", False)
|
|
128
|
-
project_root = get_project_root_from_context(ctx)
|
|
129
|
-
|
|
130
|
-
if not paths:
|
|
131
|
-
paths = (".",)
|
|
132
|
-
|
|
133
|
-
path_objs = [Path(p) for p in paths]
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
_execute_file_header_lint(path_objs, config_file, format, recursive, verbose, project_root)
|
|
137
|
-
except Exception as e:
|
|
138
|
-
handle_linting_error(e, verbose)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _execute_file_header_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
142
|
-
path_objs: list[Path],
|
|
143
|
-
config_file: str | None,
|
|
144
|
-
format: str,
|
|
145
|
-
recursive: bool,
|
|
146
|
-
verbose: bool,
|
|
147
|
-
project_root: Path | None = None,
|
|
148
|
-
) -> NoReturn:
|
|
58
|
+
def _execute_file_header_lint(params: ExecuteParams) -> NoReturn:
|
|
149
59
|
"""Execute file-header lint."""
|
|
150
|
-
validate_paths_exist(path_objs)
|
|
151
|
-
orchestrator = _setup_file_header_orchestrator(
|
|
152
|
-
|
|
60
|
+
validate_paths_exist(params.path_objs)
|
|
61
|
+
orchestrator = _setup_file_header_orchestrator(
|
|
62
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
63
|
+
)
|
|
64
|
+
file_header_violations = _run_file_header_lint(orchestrator, params.path_objs, params.recursive)
|
|
153
65
|
|
|
154
|
-
if verbose:
|
|
66
|
+
if params.verbose:
|
|
155
67
|
logger.info(f"Found {len(file_header_violations)} file header violation(s)")
|
|
156
68
|
|
|
157
|
-
format_violations(file_header_violations, format)
|
|
69
|
+
format_violations(file_header_violations, params.format)
|
|
158
70
|
sys.exit(1 if file_header_violations else 0)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
file_header = create_linter_command(
|
|
74
|
+
"file-header",
|
|
75
|
+
_execute_file_header_lint,
|
|
76
|
+
"Check file headers for mandatory fields and atemporal language.",
|
|
77
|
+
"Validates that source files have proper documentation headers containing\n"
|
|
78
|
+
" required fields (Purpose, Scope, Overview, etc.) and don't use temporal\n"
|
|
79
|
+
" language (dates, 'currently', 'now', etc.). Supports Python, TypeScript,\n"
|
|
80
|
+
" JavaScript, Bash, Markdown, and CSS files.",
|
|
81
|
+
)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for performance linters (string-concat-loop, regex-in-loop, perf)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect performance anti-patterns in loops
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for performance anti-pattern detection: string-concat-loop
|
|
7
|
+
finds O(n^2) string concatenation using += in loops, regex-in-loop detects repeated
|
|
8
|
+
regex compilation inside loops. The `perf` command runs all performance rules together
|
|
9
|
+
with optional --rule flag to select specific rules. Each command supports standard options
|
|
10
|
+
(config, format, recursive) and integrates with the orchestrator for execution.
|
|
11
|
+
|
|
12
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities,
|
|
13
|
+
src.cli.linters.shared for linter-specific helpers
|
|
14
|
+
|
|
15
|
+
Exports: string_concat_loop command, regex_in_loop command, perf command
|
|
16
|
+
|
|
17
|
+
Interfaces: Click CLI commands registered to main CLI group
|
|
18
|
+
|
|
19
|
+
Implementation: Click decorators for command definition, orchestrator-based linting execution
|
|
20
|
+
|
|
21
|
+
Suppressions:
|
|
22
|
+
- too-many-arguments,too-many-positional-arguments: Click commands with custom options require
|
|
23
|
+
additional parameters beyond the standard 5 (ctx + 4 standard options). The perf command adds
|
|
24
|
+
--rule option for 6 total parameters - framework design requirement for CLI extensibility.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ExecuteParams,
|
|
36
|
+
create_linter_command,
|
|
37
|
+
prepare_standard_command,
|
|
38
|
+
run_linter_command,
|
|
39
|
+
standard_linter_options,
|
|
40
|
+
)
|
|
41
|
+
from src.cli.main import cli
|
|
42
|
+
from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
|
|
43
|
+
from src.core.cli_utils import format_violations
|
|
44
|
+
from src.core.types import Violation
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from src.orchestrator.core import Orchestrator
|
|
48
|
+
|
|
49
|
+
# Configure module logger
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# String Concat Loop Command
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _setup_performance_orchestrator(
|
|
59
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
60
|
+
) -> "Orchestrator":
|
|
61
|
+
"""Set up orchestrator for performance linting."""
|
|
62
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _setup_and_validate(params: ExecuteParams) -> "Orchestrator":
|
|
66
|
+
"""Validate paths and set up orchestrator for linting.
|
|
67
|
+
|
|
68
|
+
Common setup code extracted to avoid DRY violations across execute functions.
|
|
69
|
+
"""
|
|
70
|
+
validate_paths_exist(params.path_objs)
|
|
71
|
+
return _setup_performance_orchestrator(
|
|
72
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run_string_concat_lint(
|
|
77
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
78
|
+
) -> list[Violation]:
|
|
79
|
+
"""Execute string-concat-loop lint on files or directories."""
|
|
80
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
81
|
+
return [v for v in all_violations if v.rule_id == "performance.string-concat-loop"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _execute_string_concat_lint(params: ExecuteParams) -> NoReturn:
|
|
85
|
+
"""Execute string-concat-loop lint."""
|
|
86
|
+
orchestrator = _setup_and_validate(params)
|
|
87
|
+
violations = _run_string_concat_lint(orchestrator, params.path_objs, params.recursive)
|
|
88
|
+
|
|
89
|
+
if params.verbose:
|
|
90
|
+
logger.info(f"Found {len(violations)} string-concat-loop violation(s)")
|
|
91
|
+
|
|
92
|
+
format_violations(violations, params.format)
|
|
93
|
+
sys.exit(1 if violations else 0)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
string_concat_loop = create_linter_command(
|
|
97
|
+
"string-concat-loop",
|
|
98
|
+
_execute_string_concat_lint,
|
|
99
|
+
"Check for string concatenation in loops.",
|
|
100
|
+
"Detects O(n^2) string building patterns using += in for/while loops.\n"
|
|
101
|
+
" This is a common performance anti-pattern in Python and TypeScript.",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# Regex In Loop Command
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run_regex_in_loop_lint(
|
|
111
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
112
|
+
) -> list[Violation]:
|
|
113
|
+
"""Execute regex-in-loop lint on files or directories."""
|
|
114
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
115
|
+
return [v for v in all_violations if v.rule_id == "performance.regex-in-loop"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _execute_regex_in_loop_lint(params: ExecuteParams) -> NoReturn:
|
|
119
|
+
"""Execute regex-in-loop lint."""
|
|
120
|
+
orchestrator = _setup_and_validate(params)
|
|
121
|
+
violations = _run_regex_in_loop_lint(orchestrator, params.path_objs, params.recursive)
|
|
122
|
+
|
|
123
|
+
if params.verbose:
|
|
124
|
+
logger.info(f"Found {len(violations)} regex-in-loop violation(s)")
|
|
125
|
+
|
|
126
|
+
format_violations(violations, params.format)
|
|
127
|
+
sys.exit(1 if violations else 0)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
regex_in_loop = create_linter_command(
|
|
131
|
+
"regex-in-loop",
|
|
132
|
+
_execute_regex_in_loop_lint,
|
|
133
|
+
"Check for regex compilation in loops.",
|
|
134
|
+
"Detects re.match(), re.search(), re.sub(), re.findall(), re.split(), and\n"
|
|
135
|
+
" re.fullmatch() calls inside loops. These recompile the regex pattern on\n"
|
|
136
|
+
" each iteration instead of compiling once with re.compile().",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# Combined Perf Command
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
# Valid rule names for the --rule option
|
|
145
|
+
PERF_RULES = {
|
|
146
|
+
"string-concat": "performance.string-concat-loop",
|
|
147
|
+
"regex-loop": "performance.regex-in-loop",
|
|
148
|
+
# Also accept full rule names
|
|
149
|
+
"string-concat-loop": "performance.string-concat-loop",
|
|
150
|
+
"regex-in-loop": "performance.regex-in-loop",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _filter_by_rule(violations: list[Violation], rule: str | None) -> list[Violation]:
|
|
155
|
+
"""Filter violations by rule name if specified.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
violations: List of violations to filter
|
|
159
|
+
rule: Optional rule name (string-concat, regex-loop, or full rule names)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Filtered list of violations
|
|
163
|
+
"""
|
|
164
|
+
if not rule:
|
|
165
|
+
return violations
|
|
166
|
+
|
|
167
|
+
rule_id = PERF_RULES.get(rule)
|
|
168
|
+
if not rule_id:
|
|
169
|
+
logger.warning(f"Unknown rule '{rule}'. Valid rules: {', '.join(PERF_RULES.keys())}")
|
|
170
|
+
return violations
|
|
171
|
+
|
|
172
|
+
return [v for v in violations if v.rule_id == rule_id]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run_all_perf_lint(
|
|
176
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, rule: str | None
|
|
177
|
+
) -> list[Violation]:
|
|
178
|
+
"""Execute all performance lints on files or directories.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
orchestrator: Configured orchestrator instance
|
|
182
|
+
path_objs: List of paths to analyze
|
|
183
|
+
recursive: Whether to scan directories recursively
|
|
184
|
+
rule: Optional rule filter (string-concat, regex-loop, or full rule names)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of performance-related violations
|
|
188
|
+
"""
|
|
189
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
190
|
+
perf_violations = [v for v in all_violations if v.rule_id.startswith("performance.")]
|
|
191
|
+
return _filter_by_rule(perf_violations, rule)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _execute_perf_lint(params: ExecuteParams, rule: str | None) -> NoReturn:
|
|
195
|
+
"""Execute combined performance lint."""
|
|
196
|
+
orchestrator = _setup_and_validate(params)
|
|
197
|
+
violations = _run_all_perf_lint(orchestrator, params.path_objs, params.recursive, rule)
|
|
198
|
+
|
|
199
|
+
if params.verbose:
|
|
200
|
+
logger.info(f"Found {len(violations)} performance violation(s)")
|
|
201
|
+
|
|
202
|
+
format_violations(violations, params.format)
|
|
203
|
+
sys.exit(1 if violations else 0)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@cli.command(
|
|
207
|
+
"perf",
|
|
208
|
+
help="""Check for performance anti-patterns in code.
|
|
209
|
+
|
|
210
|
+
Detects common performance issues in loops:
|
|
211
|
+
- string-concat-loop: O(n^2) string building using += in loops
|
|
212
|
+
- regex-in-loop: Regex recompilation on each loop iteration
|
|
213
|
+
|
|
214
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
|
|
218
|
+
\\b
|
|
219
|
+
# Check current directory for all performance issues
|
|
220
|
+
thai-lint perf
|
|
221
|
+
|
|
222
|
+
\\b
|
|
223
|
+
# Check specific directory
|
|
224
|
+
thai-lint perf src/
|
|
225
|
+
|
|
226
|
+
\\b
|
|
227
|
+
# Check only string concatenation issues
|
|
228
|
+
thai-lint perf --rule string-concat src/
|
|
229
|
+
|
|
230
|
+
\\b
|
|
231
|
+
# Check only regex-in-loop issues
|
|
232
|
+
thai-lint perf --rule regex-loop src/
|
|
233
|
+
|
|
234
|
+
\\b
|
|
235
|
+
# Get JSON output
|
|
236
|
+
thai-lint perf --format json .
|
|
237
|
+
|
|
238
|
+
\\b
|
|
239
|
+
# Use custom config file
|
|
240
|
+
thai-lint perf --config .thailint.yaml src/
|
|
241
|
+
""",
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--rule",
|
|
245
|
+
"-r",
|
|
246
|
+
"rule",
|
|
247
|
+
type=click.Choice(["string-concat", "regex-loop"]),
|
|
248
|
+
help="Run only a specific performance rule",
|
|
249
|
+
)
|
|
250
|
+
@standard_linter_options
|
|
251
|
+
def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
252
|
+
ctx: click.Context,
|
|
253
|
+
paths: tuple[str, ...],
|
|
254
|
+
config_file: str | None,
|
|
255
|
+
format: str,
|
|
256
|
+
recursive: bool,
|
|
257
|
+
rule: str | None,
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Run all performance linters.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
ctx: Click context with global options
|
|
263
|
+
paths: Files or directories to lint
|
|
264
|
+
config_file: Optional path to config file
|
|
265
|
+
format: Output format (text, json, sarif)
|
|
266
|
+
recursive: Whether to scan directories recursively
|
|
267
|
+
rule: Optional rule filter (string-concat or regex-loop)
|
|
268
|
+
"""
|
|
269
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive)
|
|
270
|
+
|
|
271
|
+
def execute_with_rule(p: ExecuteParams) -> None:
|
|
272
|
+
_execute_perf_lint(p, rule)
|
|
273
|
+
|
|
274
|
+
run_linter_command(execute_with_rule, params)
|