thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- 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/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
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, parallel: bool = False
|
|
78
|
+
) -> list[Violation]:
|
|
79
|
+
"""Execute string-concat-loop lint on files or directories."""
|
|
80
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
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(
|
|
88
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if params.verbose:
|
|
92
|
+
logger.info(f"Found {len(violations)} string-concat-loop violation(s)")
|
|
93
|
+
|
|
94
|
+
format_violations(violations, params.format)
|
|
95
|
+
sys.exit(1 if violations else 0)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
string_concat_loop = create_linter_command(
|
|
99
|
+
"string-concat-loop",
|
|
100
|
+
_execute_string_concat_lint,
|
|
101
|
+
"Check for string concatenation in loops.",
|
|
102
|
+
"Detects O(n^2) string building patterns using += in for/while loops.\n"
|
|
103
|
+
" This is a common performance anti-pattern in Python and TypeScript.",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# Regex In Loop Command
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _run_regex_in_loop_lint(
|
|
113
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
114
|
+
) -> list[Violation]:
|
|
115
|
+
"""Execute regex-in-loop lint on files or directories."""
|
|
116
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
117
|
+
return [v for v in all_violations if v.rule_id == "performance.regex-in-loop"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _execute_regex_in_loop_lint(params: ExecuteParams) -> NoReturn:
|
|
121
|
+
"""Execute regex-in-loop lint."""
|
|
122
|
+
orchestrator = _setup_and_validate(params)
|
|
123
|
+
violations = _run_regex_in_loop_lint(
|
|
124
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if params.verbose:
|
|
128
|
+
logger.info(f"Found {len(violations)} regex-in-loop violation(s)")
|
|
129
|
+
|
|
130
|
+
format_violations(violations, params.format)
|
|
131
|
+
sys.exit(1 if violations else 0)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
regex_in_loop = create_linter_command(
|
|
135
|
+
"regex-in-loop",
|
|
136
|
+
_execute_regex_in_loop_lint,
|
|
137
|
+
"Check for regex compilation in loops.",
|
|
138
|
+
"Detects re.match(), re.search(), re.sub(), re.findall(), re.split(), and\n"
|
|
139
|
+
" re.fullmatch() calls inside loops. These recompile the regex pattern on\n"
|
|
140
|
+
" each iteration instead of compiling once with re.compile().",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# =============================================================================
|
|
145
|
+
# Combined Perf Command
|
|
146
|
+
# =============================================================================
|
|
147
|
+
|
|
148
|
+
# Valid rule names for the --rule option
|
|
149
|
+
PERF_RULES = {
|
|
150
|
+
"string-concat": "performance.string-concat-loop",
|
|
151
|
+
"regex-loop": "performance.regex-in-loop",
|
|
152
|
+
# Also accept full rule names
|
|
153
|
+
"string-concat-loop": "performance.string-concat-loop",
|
|
154
|
+
"regex-in-loop": "performance.regex-in-loop",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _filter_by_rule(violations: list[Violation], rule: str | None) -> list[Violation]:
|
|
159
|
+
"""Filter violations by rule name if specified.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
violations: List of violations to filter
|
|
163
|
+
rule: Optional rule name (string-concat, regex-loop, or full rule names)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Filtered list of violations
|
|
167
|
+
"""
|
|
168
|
+
if not rule:
|
|
169
|
+
return violations
|
|
170
|
+
|
|
171
|
+
rule_id = PERF_RULES.get(rule)
|
|
172
|
+
if not rule_id:
|
|
173
|
+
logger.warning(f"Unknown rule '{rule}'. Valid rules: {', '.join(PERF_RULES.keys())}")
|
|
174
|
+
return violations
|
|
175
|
+
|
|
176
|
+
return [v for v in violations if v.rule_id == rule_id]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _run_all_perf_lint(
|
|
180
|
+
orchestrator: "Orchestrator",
|
|
181
|
+
path_objs: list[Path],
|
|
182
|
+
recursive: bool,
|
|
183
|
+
rule: str | None,
|
|
184
|
+
parallel: bool = False,
|
|
185
|
+
) -> list[Violation]:
|
|
186
|
+
"""Execute all performance lints on files or directories.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
orchestrator: Configured orchestrator instance
|
|
190
|
+
path_objs: List of paths to analyze
|
|
191
|
+
recursive: Whether to scan directories recursively
|
|
192
|
+
rule: Optional rule filter (string-concat, regex-loop, or full rule names)
|
|
193
|
+
parallel: Whether to use parallel processing
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of performance-related violations
|
|
197
|
+
"""
|
|
198
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
199
|
+
perf_violations = [v for v in all_violations if v.rule_id.startswith("performance.")]
|
|
200
|
+
return _filter_by_rule(perf_violations, rule)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _execute_perf_lint(params: ExecuteParams, rule: str | None) -> NoReturn:
|
|
204
|
+
"""Execute combined performance lint."""
|
|
205
|
+
orchestrator = _setup_and_validate(params)
|
|
206
|
+
violations = _run_all_perf_lint(
|
|
207
|
+
orchestrator, params.path_objs, params.recursive, rule, params.parallel
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if params.verbose:
|
|
211
|
+
logger.info(f"Found {len(violations)} performance violation(s)")
|
|
212
|
+
|
|
213
|
+
format_violations(violations, params.format)
|
|
214
|
+
sys.exit(1 if violations else 0)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@cli.command(
|
|
218
|
+
"perf",
|
|
219
|
+
help="""Check for performance anti-patterns in code.
|
|
220
|
+
|
|
221
|
+
Detects common performance issues in loops:
|
|
222
|
+
- string-concat-loop: O(n^2) string building using += in loops
|
|
223
|
+
- regex-in-loop: Regex recompilation on each loop iteration
|
|
224
|
+
|
|
225
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
|
|
229
|
+
\\b
|
|
230
|
+
# Check current directory for all performance issues
|
|
231
|
+
thai-lint perf
|
|
232
|
+
|
|
233
|
+
\\b
|
|
234
|
+
# Check specific directory
|
|
235
|
+
thai-lint perf src/
|
|
236
|
+
|
|
237
|
+
\\b
|
|
238
|
+
# Check only string concatenation issues
|
|
239
|
+
thai-lint perf --rule string-concat src/
|
|
240
|
+
|
|
241
|
+
\\b
|
|
242
|
+
# Check only regex-in-loop issues
|
|
243
|
+
thai-lint perf --rule regex-loop src/
|
|
244
|
+
|
|
245
|
+
\\b
|
|
246
|
+
# Get JSON output
|
|
247
|
+
thai-lint perf --format json .
|
|
248
|
+
|
|
249
|
+
\\b
|
|
250
|
+
# Use custom config file
|
|
251
|
+
thai-lint perf --config .thailint.yaml src/
|
|
252
|
+
""",
|
|
253
|
+
)
|
|
254
|
+
@click.option(
|
|
255
|
+
"--rule",
|
|
256
|
+
"-r",
|
|
257
|
+
"rule",
|
|
258
|
+
type=click.Choice(["string-concat", "regex-loop"]),
|
|
259
|
+
help="Run only a specific performance rule",
|
|
260
|
+
)
|
|
261
|
+
@standard_linter_options
|
|
262
|
+
def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
263
|
+
ctx: click.Context,
|
|
264
|
+
paths: tuple[str, ...],
|
|
265
|
+
config_file: str | None,
|
|
266
|
+
format: str,
|
|
267
|
+
recursive: bool,
|
|
268
|
+
parallel: bool,
|
|
269
|
+
rule: str | None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Run all performance linters.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ctx: Click context with global options
|
|
275
|
+
paths: Files or directories to lint
|
|
276
|
+
config_file: Optional path to config file
|
|
277
|
+
format: Output format (text, json, sarif)
|
|
278
|
+
recursive: Whether to scan directories recursively
|
|
279
|
+
parallel: Whether to use parallel processing
|
|
280
|
+
rule: Optional rule filter (string-concat or regex-loop)
|
|
281
|
+
"""
|
|
282
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
|
|
283
|
+
|
|
284
|
+
def execute_with_rule(p: ExecuteParams) -> None:
|
|
285
|
+
_execute_perf_lint(p, rule)
|
|
286
|
+
|
|
287
|
+
run_linter_command(execute_with_rule, params)
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Shared utilities for linter CLI commands
|
|
3
|
+
|
|
4
|
+
Scope: Common helper functions and patterns used across all linter command modules
|
|
5
|
+
|
|
6
|
+
Overview: Provides reusable utilities for linter CLI commands including config section management,
|
|
7
|
+
config value setting with logging, rule ID filtering, CLI context extraction, and help text
|
|
8
|
+
generation. Centralizes shared patterns to reduce duplication across linter command modules
|
|
9
|
+
(code_quality, code_patterns, structure, documentation, performance). All utilities are designed
|
|
10
|
+
to work with the orchestrator configuration system and Click CLI framework.
|
|
11
|
+
|
|
12
|
+
Dependencies: logging for debug output, pathlib for Path type hints, click for Context type,
|
|
13
|
+
dataclasses for CommandContext
|
|
14
|
+
|
|
15
|
+
Exports: ensure_config_section, set_config_value, filter_violations_by_prefix, CommandContext,
|
|
16
|
+
extract_command_context, make_linter_help, ExecuteParams, prepare_standard_command,
|
|
17
|
+
run_linter_command, standard_linter_options, filter_violations_by_startswith,
|
|
18
|
+
create_linter_command
|
|
19
|
+
|
|
20
|
+
Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI context extraction,
|
|
21
|
+
help text generation
|
|
22
|
+
|
|
23
|
+
Implementation: Pure helper functions with no side effects beyond config mutation and logging
|
|
24
|
+
|
|
25
|
+
Suppressions:
|
|
26
|
+
- too-many-arguments,too-many-positional-arguments: CLI helper functions require many parameters
|
|
27
|
+
by Click framework design (ctx, paths, config_file, format, recursive, parallel = 6 params)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
import click
|
|
36
|
+
|
|
37
|
+
from src.cli.utils import (
|
|
38
|
+
format_option,
|
|
39
|
+
get_project_root_from_context,
|
|
40
|
+
handle_linting_error,
|
|
41
|
+
parallel_option,
|
|
42
|
+
)
|
|
43
|
+
from src.core.types import Violation
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from collections.abc import Callable
|
|
47
|
+
|
|
48
|
+
from src.orchestrator.core import Orchestrator
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def standard_linter_options(f: Any) -> Any:
|
|
52
|
+
"""Apply standard linter CLI options to a command.
|
|
53
|
+
|
|
54
|
+
Bundles the common options used by most linter commands:
|
|
55
|
+
- paths argument (variadic)
|
|
56
|
+
- config file option
|
|
57
|
+
- format option
|
|
58
|
+
- recursive option
|
|
59
|
+
- parallel option
|
|
60
|
+
- pass_context
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
@cli.command("my-linter")
|
|
64
|
+
@standard_linter_options
|
|
65
|
+
def my_linter(ctx, paths, config_file, format, recursive, parallel):
|
|
66
|
+
...
|
|
67
|
+
"""
|
|
68
|
+
f = click.pass_context(f)
|
|
69
|
+
f = parallel_option(f)
|
|
70
|
+
f = click.option(
|
|
71
|
+
"--recursive/--no-recursive", default=True, help="Scan directories recursively"
|
|
72
|
+
)(f)
|
|
73
|
+
f = format_option(f)
|
|
74
|
+
f = click.option(
|
|
75
|
+
"--config", "-c", "config_file", type=click.Path(), help="Path to config file"
|
|
76
|
+
)(f)
|
|
77
|
+
f = click.argument("paths", nargs=-1, type=click.Path())(f)
|
|
78
|
+
return f
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class CommandContext:
|
|
83
|
+
"""Extracted context from CLI command invocation.
|
|
84
|
+
|
|
85
|
+
Consolidates common CLI command setup into a reusable structure.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
verbose: bool
|
|
89
|
+
project_root: Path | None
|
|
90
|
+
path_objs: list[Path]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class ExecuteParams:
|
|
95
|
+
"""Parameters for linter execution functions.
|
|
96
|
+
|
|
97
|
+
Bundles the common parameters passed to _execute_*_lint functions
|
|
98
|
+
to reduce function signature duplication across CLI modules.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
path_objs: list[Path]
|
|
102
|
+
config_file: str | None
|
|
103
|
+
format: str
|
|
104
|
+
recursive: bool
|
|
105
|
+
verbose: bool
|
|
106
|
+
project_root: Path | None
|
|
107
|
+
parallel: bool = False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
|
|
111
|
+
"""Extract common context values from CLI command invocation.
|
|
112
|
+
|
|
113
|
+
Consolidates the repeated pattern of extracting verbose, project_root,
|
|
114
|
+
and converting paths to Path objects with default handling.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
ctx: Click context from command invocation
|
|
118
|
+
paths: Tuple of path strings from command arguments
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
CommandContext with extracted values
|
|
122
|
+
"""
|
|
123
|
+
verbose: bool = ctx.obj.get("verbose", False)
|
|
124
|
+
project_root = get_project_root_from_context(ctx)
|
|
125
|
+
|
|
126
|
+
if not paths:
|
|
127
|
+
paths = (".",)
|
|
128
|
+
|
|
129
|
+
path_objs = [Path(p) for p in paths]
|
|
130
|
+
|
|
131
|
+
return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def prepare_standard_command( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
135
|
+
ctx: click.Context,
|
|
136
|
+
paths: tuple[str, ...],
|
|
137
|
+
config_file: str | None,
|
|
138
|
+
format: str,
|
|
139
|
+
recursive: bool,
|
|
140
|
+
parallel: bool = False,
|
|
141
|
+
) -> ExecuteParams:
|
|
142
|
+
"""Prepare standard linter command execution parameters.
|
|
143
|
+
|
|
144
|
+
Combines context extraction and ExecuteParams creation into a single call.
|
|
145
|
+
Use with commands that have the standard options (config, format, recursive, parallel).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
ctx: Click context from command invocation
|
|
149
|
+
paths: Tuple of path strings from command arguments
|
|
150
|
+
config_file: Optional config file path
|
|
151
|
+
format: Output format
|
|
152
|
+
recursive: Whether to scan recursively
|
|
153
|
+
parallel: Whether to use parallel processing
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
ExecuteParams ready for _execute_*_lint function
|
|
157
|
+
"""
|
|
158
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
159
|
+
return ExecuteParams(
|
|
160
|
+
path_objs=cmd_ctx.path_objs,
|
|
161
|
+
config_file=config_file,
|
|
162
|
+
format=format,
|
|
163
|
+
recursive=recursive,
|
|
164
|
+
verbose=cmd_ctx.verbose,
|
|
165
|
+
project_root=cmd_ctx.project_root,
|
|
166
|
+
parallel=parallel,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_linter_command(
|
|
171
|
+
execute_fn: "Callable[[ExecuteParams], None]",
|
|
172
|
+
params: ExecuteParams,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Run a linter command with standard error handling.
|
|
175
|
+
|
|
176
|
+
Wraps the try/except pattern used by all linter commands.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
execute_fn: The _execute_*_lint function to call
|
|
180
|
+
params: ExecuteParams for the execution
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
execute_fn(params)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
handle_linting_error(e, params.verbose)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Configure module logger
|
|
189
|
+
logger = logging.getLogger(__name__)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def ensure_config_section(orchestrator: "Orchestrator", section: str) -> dict[str, Any]:
|
|
193
|
+
"""Ensure a config section exists and return it.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
orchestrator: Orchestrator instance with config dict
|
|
197
|
+
section: Name of the config section to ensure exists
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The config section dict (created if it didn't exist)
|
|
201
|
+
"""
|
|
202
|
+
if section not in orchestrator.config:
|
|
203
|
+
orchestrator.config[section] = {}
|
|
204
|
+
config_section: dict[str, Any] = orchestrator.config[section]
|
|
205
|
+
return config_section
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def set_config_value(config: dict[str, Any], key: str, value: Any, verbose: bool) -> None:
|
|
209
|
+
"""Set a config value with optional debug logging.
|
|
210
|
+
|
|
211
|
+
Only sets the value if it is not None.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
config: Config dict to update
|
|
215
|
+
key: Config key to set
|
|
216
|
+
value: Value to set (skipped if None)
|
|
217
|
+
verbose: Whether to log the override
|
|
218
|
+
"""
|
|
219
|
+
if value is None:
|
|
220
|
+
return
|
|
221
|
+
config[key] = value
|
|
222
|
+
if verbose:
|
|
223
|
+
logger.debug(f"Overriding {key} to {value}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def filter_violations_by_prefix(violations: list[Violation], prefix: str) -> list[Violation]:
|
|
227
|
+
"""Filter violations to those matching a rule ID prefix.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
violations: List of violation objects with rule_id attribute
|
|
231
|
+
prefix: Prefix to match against rule_id
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Filtered list of violations where rule_id contains the prefix
|
|
235
|
+
"""
|
|
236
|
+
return [v for v in violations if prefix in v.rule_id]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def filter_violations_by_startswith(violations: list[Violation], prefix: str) -> list[Violation]:
|
|
240
|
+
"""Filter violations to those with rule_id starting with prefix.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
violations: List of violation objects with rule_id attribute
|
|
244
|
+
prefix: Prefix that rule_id must start with
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Filtered list of violations where rule_id starts with the prefix
|
|
248
|
+
"""
|
|
249
|
+
return [v for v in violations if v.rule_id.startswith(prefix)]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_linter_command(
|
|
253
|
+
name: str,
|
|
254
|
+
execute_fn: "Callable[[ExecuteParams], None]",
|
|
255
|
+
brief: str,
|
|
256
|
+
description: str,
|
|
257
|
+
) -> "Callable[..., None]":
|
|
258
|
+
"""Create a standard linter CLI command.
|
|
259
|
+
|
|
260
|
+
Factory function that generates Click commands with consistent structure,
|
|
261
|
+
eliminating boilerplate duplication across linter command modules.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: CLI command name (e.g., "magic-numbers")
|
|
265
|
+
execute_fn: The _execute_*_lint function to call
|
|
266
|
+
brief: Brief one-line description
|
|
267
|
+
description: Detailed multi-line description
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Decorated Click command function
|
|
271
|
+
"""
|
|
272
|
+
from src.cli.main import cli
|
|
273
|
+
|
|
274
|
+
@cli.command(name, help=make_linter_help(name, brief, description))
|
|
275
|
+
@standard_linter_options
|
|
276
|
+
def command( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
277
|
+
ctx: click.Context,
|
|
278
|
+
paths: tuple[str, ...],
|
|
279
|
+
config_file: str | None,
|
|
280
|
+
format: str,
|
|
281
|
+
recursive: bool,
|
|
282
|
+
parallel: bool,
|
|
283
|
+
) -> None:
|
|
284
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
|
|
285
|
+
run_linter_command(execute_fn, params)
|
|
286
|
+
|
|
287
|
+
return command
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def make_linter_help(command: str, brief: str, description: str) -> str:
|
|
291
|
+
"""Generate standardized CLI help text for linter commands.
|
|
292
|
+
|
|
293
|
+
Creates consistent help text following the established pattern with
|
|
294
|
+
examples showing common usage patterns.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
command: CLI command name (e.g., "magic-numbers")
|
|
298
|
+
brief: Brief one-line description
|
|
299
|
+
description: Detailed multi-line description
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Formatted help text string for Click command
|
|
303
|
+
"""
|
|
304
|
+
return f"""{brief}
|
|
305
|
+
|
|
306
|
+
{description}
|
|
307
|
+
|
|
308
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
309
|
+
|
|
310
|
+
Examples:
|
|
311
|
+
|
|
312
|
+
\\b
|
|
313
|
+
# Check current directory (all files recursively)
|
|
314
|
+
thai-lint {command}
|
|
315
|
+
|
|
316
|
+
\\b
|
|
317
|
+
# Check specific directory
|
|
318
|
+
thai-lint {command} src/
|
|
319
|
+
|
|
320
|
+
\\b
|
|
321
|
+
# Check single file
|
|
322
|
+
thai-lint {command} src/app.py
|
|
323
|
+
|
|
324
|
+
\\b
|
|
325
|
+
# Get JSON output
|
|
326
|
+
thai-lint {command} --format json .
|
|
327
|
+
|
|
328
|
+
\\b
|
|
329
|
+
# Use custom config file
|
|
330
|
+
thai-lint {command} --config .thailint.yaml src/
|
|
331
|
+
"""
|