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.
Files changed (32) hide show
  1. src/cli/linters/__init__.py +6 -0
  2. src/cli/linters/code_patterns.py +75 -333
  3. src/cli/linters/code_smells.py +47 -168
  4. src/cli/linters/documentation.py +21 -98
  5. src/cli/linters/performance.py +274 -0
  6. src/cli/linters/shared.py +232 -6
  7. src/cli/linters/structure.py +23 -21
  8. src/cli/linters/structure_quality.py +25 -21
  9. src/core/linter_utils.py +91 -6
  10. src/linters/file_header/atemporal_detector.py +54 -40
  11. src/linters/file_header/config.py +14 -0
  12. src/linters/lazy_ignores/python_analyzer.py +5 -1
  13. src/linters/lazy_ignores/types.py +2 -0
  14. src/linters/method_property/config.py +0 -1
  15. src/linters/method_property/linter.py +0 -6
  16. src/linters/nesting/linter.py +11 -6
  17. src/linters/nesting/violation_builder.py +1 -0
  18. src/linters/performance/__init__.py +91 -0
  19. src/linters/performance/config.py +43 -0
  20. src/linters/performance/constants.py +49 -0
  21. src/linters/performance/linter.py +149 -0
  22. src/linters/performance/python_analyzer.py +365 -0
  23. src/linters/performance/regex_analyzer.py +312 -0
  24. src/linters/performance/regex_linter.py +139 -0
  25. src/linters/performance/typescript_analyzer.py +236 -0
  26. src/linters/performance/violation_builder.py +160 -0
  27. src/templates/thailint_config_template.yaml +30 -0
  28. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/METADATA +3 -2
  29. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/RECORD +32 -22
  30. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
  31. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
  32. {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 require many parameters by framework design
26
- too-many-positional-arguments: Click positional params match CLI arg structure
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 ensure_config_section, set_config_value
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
- @cli.command("magic-numbers")
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(path_objs, config_file, verbose, project_root)
341
- magic_numbers_violations = _run_magic_numbers_lint(orchestrator, path_objs, recursive)
342
-
343
- if verbose:
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
- @cli.command("stringly-typed")
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(path_objs, config_file, verbose, project_root)
448
- stringly_violations = _run_stringly_typed_lint(orchestrator, path_objs, recursive)
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
+ )
@@ -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 click
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
- @cli.command("file-header")
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(path_objs, config_file, verbose, project_root)
152
- file_header_violations = _run_file_header_lint(orchestrator, path_objs, recursive)
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)