thailint 0.15.0__py3-none-any.whl → 0.15.1__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/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/cli/config.py +6 -4
- src/cli/linters/code_patterns.py +64 -16
- src/cli/linters/code_smells.py +23 -14
- src/cli/linters/documentation.py +5 -3
- src/cli/linters/performance.py +23 -10
- src/cli/linters/shared.py +22 -6
- src/cli/linters/structure.py +13 -4
- src/cli/linters/structure_quality.py +9 -4
- src/cli/utils.py +4 -4
- src/config.py +34 -21
- src/core/python_lint_rule.py +101 -0
- src/linter_config/ignore.py +2 -1
- 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/typescript_value_extractor.py +2 -1
- src/linters/file_header/linter.py +2 -1
- src/linters/file_placement/linter.py +6 -6
- src/linters/file_placement/pattern_validator.py +6 -5
- src/linters/file_placement/rule_checker.py +10 -5
- src/linters/lazy_ignores/config.py +5 -3
- src/linters/lazy_ignores/python_analyzer.py +5 -1
- src/linters/lazy_ignores/types.py +2 -1
- src/linters/lbyl/__init__.py +3 -1
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +30 -2
- src/linters/lbyl/pattern_detectors/base.py +24 -7
- 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/stringly_typed/ignore_checker.py +4 -6
- src/orchestrator/language_detector.py +5 -3
- {thailint-0.15.0.dist-info → thailint-0.15.1.dist-info}/METADATA +4 -2
- {thailint-0.15.0.dist-info → thailint-0.15.1.dist-info}/RECORD +56 -29
- {thailint-0.15.0.dist-info → thailint-0.15.1.dist-info}/WHEEL +0 -0
- {thailint-0.15.0.dist-info → thailint-0.15.1.dist-info}/entry_points.txt +0 -0
- {thailint-0.15.0.dist-info → thailint-0.15.1.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/performance.py
CHANGED
|
@@ -74,17 +74,19 @@ def _setup_and_validate(params: ExecuteParams) -> "Orchestrator":
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def _run_string_concat_lint(
|
|
77
|
-
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
77
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
78
78
|
) -> list[Violation]:
|
|
79
79
|
"""Execute string-concat-loop lint on files or directories."""
|
|
80
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
80
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
81
81
|
return [v for v in all_violations if v.rule_id == "performance.string-concat-loop"]
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _execute_string_concat_lint(params: ExecuteParams) -> NoReturn:
|
|
85
85
|
"""Execute string-concat-loop lint."""
|
|
86
86
|
orchestrator = _setup_and_validate(params)
|
|
87
|
-
violations = _run_string_concat_lint(
|
|
87
|
+
violations = _run_string_concat_lint(
|
|
88
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
89
|
+
)
|
|
88
90
|
|
|
89
91
|
if params.verbose:
|
|
90
92
|
logger.info(f"Found {len(violations)} string-concat-loop violation(s)")
|
|
@@ -108,17 +110,19 @@ string_concat_loop = create_linter_command(
|
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
def _run_regex_in_loop_lint(
|
|
111
|
-
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
113
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
112
114
|
) -> list[Violation]:
|
|
113
115
|
"""Execute regex-in-loop lint on files or directories."""
|
|
114
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
116
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
115
117
|
return [v for v in all_violations if v.rule_id == "performance.regex-in-loop"]
|
|
116
118
|
|
|
117
119
|
|
|
118
120
|
def _execute_regex_in_loop_lint(params: ExecuteParams) -> NoReturn:
|
|
119
121
|
"""Execute regex-in-loop lint."""
|
|
120
122
|
orchestrator = _setup_and_validate(params)
|
|
121
|
-
violations = _run_regex_in_loop_lint(
|
|
123
|
+
violations = _run_regex_in_loop_lint(
|
|
124
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
125
|
+
)
|
|
122
126
|
|
|
123
127
|
if params.verbose:
|
|
124
128
|
logger.info(f"Found {len(violations)} regex-in-loop violation(s)")
|
|
@@ -173,7 +177,11 @@ def _filter_by_rule(violations: list[Violation], rule: str | None) -> list[Viola
|
|
|
173
177
|
|
|
174
178
|
|
|
175
179
|
def _run_all_perf_lint(
|
|
176
|
-
orchestrator: "Orchestrator",
|
|
180
|
+
orchestrator: "Orchestrator",
|
|
181
|
+
path_objs: list[Path],
|
|
182
|
+
recursive: bool,
|
|
183
|
+
rule: str | None,
|
|
184
|
+
parallel: bool = False,
|
|
177
185
|
) -> list[Violation]:
|
|
178
186
|
"""Execute all performance lints on files or directories.
|
|
179
187
|
|
|
@@ -182,11 +190,12 @@ def _run_all_perf_lint(
|
|
|
182
190
|
path_objs: List of paths to analyze
|
|
183
191
|
recursive: Whether to scan directories recursively
|
|
184
192
|
rule: Optional rule filter (string-concat, regex-loop, or full rule names)
|
|
193
|
+
parallel: Whether to use parallel processing
|
|
185
194
|
|
|
186
195
|
Returns:
|
|
187
196
|
List of performance-related violations
|
|
188
197
|
"""
|
|
189
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
198
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
190
199
|
perf_violations = [v for v in all_violations if v.rule_id.startswith("performance.")]
|
|
191
200
|
return _filter_by_rule(perf_violations, rule)
|
|
192
201
|
|
|
@@ -194,7 +203,9 @@ def _run_all_perf_lint(
|
|
|
194
203
|
def _execute_perf_lint(params: ExecuteParams, rule: str | None) -> NoReturn:
|
|
195
204
|
"""Execute combined performance lint."""
|
|
196
205
|
orchestrator = _setup_and_validate(params)
|
|
197
|
-
violations = _run_all_perf_lint(
|
|
206
|
+
violations = _run_all_perf_lint(
|
|
207
|
+
orchestrator, params.path_objs, params.recursive, rule, params.parallel
|
|
208
|
+
)
|
|
198
209
|
|
|
199
210
|
if params.verbose:
|
|
200
211
|
logger.info(f"Found {len(violations)} performance violation(s)")
|
|
@@ -254,6 +265,7 @@ def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
254
265
|
config_file: str | None,
|
|
255
266
|
format: str,
|
|
256
267
|
recursive: bool,
|
|
268
|
+
parallel: bool,
|
|
257
269
|
rule: str | None,
|
|
258
270
|
) -> None:
|
|
259
271
|
"""Run all performance linters.
|
|
@@ -264,9 +276,10 @@ def perf( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
264
276
|
config_file: Optional path to config file
|
|
265
277
|
format: Output format (text, json, sarif)
|
|
266
278
|
recursive: Whether to scan directories recursively
|
|
279
|
+
parallel: Whether to use parallel processing
|
|
267
280
|
rule: Optional rule filter (string-concat or regex-loop)
|
|
268
281
|
"""
|
|
269
|
-
params = prepare_standard_command(ctx, paths, config_file, format, recursive)
|
|
282
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
|
|
270
283
|
|
|
271
284
|
def execute_with_rule(p: ExecuteParams) -> None:
|
|
272
285
|
_execute_perf_lint(p, rule)
|
src/cli/linters/shared.py
CHANGED
|
@@ -21,6 +21,10 @@ Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI
|
|
|
21
21
|
help text generation
|
|
22
22
|
|
|
23
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)
|
|
24
28
|
"""
|
|
25
29
|
|
|
26
30
|
import logging
|
|
@@ -30,7 +34,12 @@ from typing import TYPE_CHECKING, Any
|
|
|
30
34
|
|
|
31
35
|
import click
|
|
32
36
|
|
|
33
|
-
from src.cli.utils import
|
|
37
|
+
from src.cli.utils import (
|
|
38
|
+
format_option,
|
|
39
|
+
get_project_root_from_context,
|
|
40
|
+
handle_linting_error,
|
|
41
|
+
parallel_option,
|
|
42
|
+
)
|
|
34
43
|
from src.core.types import Violation
|
|
35
44
|
|
|
36
45
|
if TYPE_CHECKING:
|
|
@@ -47,15 +56,17 @@ def standard_linter_options(f: Any) -> Any:
|
|
|
47
56
|
- config file option
|
|
48
57
|
- format option
|
|
49
58
|
- recursive option
|
|
59
|
+
- parallel option
|
|
50
60
|
- pass_context
|
|
51
61
|
|
|
52
62
|
Usage:
|
|
53
63
|
@cli.command("my-linter")
|
|
54
64
|
@standard_linter_options
|
|
55
|
-
def my_linter(ctx, paths, config_file, format, recursive):
|
|
65
|
+
def my_linter(ctx, paths, config_file, format, recursive, parallel):
|
|
56
66
|
...
|
|
57
67
|
"""
|
|
58
68
|
f = click.pass_context(f)
|
|
69
|
+
f = parallel_option(f)
|
|
59
70
|
f = click.option(
|
|
60
71
|
"--recursive/--no-recursive", default=True, help="Scan directories recursively"
|
|
61
72
|
)(f)
|
|
@@ -93,6 +104,7 @@ class ExecuteParams:
|
|
|
93
104
|
recursive: bool
|
|
94
105
|
verbose: bool
|
|
95
106
|
project_root: Path | None
|
|
107
|
+
parallel: bool = False
|
|
96
108
|
|
|
97
109
|
|
|
98
110
|
def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
|
|
@@ -119,17 +131,18 @@ def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> Comma
|
|
|
119
131
|
return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
|
|
120
132
|
|
|
121
133
|
|
|
122
|
-
def prepare_standard_command(
|
|
134
|
+
def prepare_standard_command( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
123
135
|
ctx: click.Context,
|
|
124
136
|
paths: tuple[str, ...],
|
|
125
137
|
config_file: str | None,
|
|
126
138
|
format: str,
|
|
127
139
|
recursive: bool,
|
|
140
|
+
parallel: bool = False,
|
|
128
141
|
) -> ExecuteParams:
|
|
129
142
|
"""Prepare standard linter command execution parameters.
|
|
130
143
|
|
|
131
144
|
Combines context extraction and ExecuteParams creation into a single call.
|
|
132
|
-
Use with commands that have the standard options (config, format, recursive).
|
|
145
|
+
Use with commands that have the standard options (config, format, recursive, parallel).
|
|
133
146
|
|
|
134
147
|
Args:
|
|
135
148
|
ctx: Click context from command invocation
|
|
@@ -137,6 +150,7 @@ def prepare_standard_command(
|
|
|
137
150
|
config_file: Optional config file path
|
|
138
151
|
format: Output format
|
|
139
152
|
recursive: Whether to scan recursively
|
|
153
|
+
parallel: Whether to use parallel processing
|
|
140
154
|
|
|
141
155
|
Returns:
|
|
142
156
|
ExecuteParams ready for _execute_*_lint function
|
|
@@ -149,6 +163,7 @@ def prepare_standard_command(
|
|
|
149
163
|
recursive=recursive,
|
|
150
164
|
verbose=cmd_ctx.verbose,
|
|
151
165
|
project_root=cmd_ctx.project_root,
|
|
166
|
+
parallel=parallel,
|
|
152
167
|
)
|
|
153
168
|
|
|
154
169
|
|
|
@@ -258,14 +273,15 @@ def create_linter_command(
|
|
|
258
273
|
|
|
259
274
|
@cli.command(name, help=make_linter_help(name, brief, description))
|
|
260
275
|
@standard_linter_options
|
|
261
|
-
def command(
|
|
276
|
+
def command( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
262
277
|
ctx: click.Context,
|
|
263
278
|
paths: tuple[str, ...],
|
|
264
279
|
config_file: str | None,
|
|
265
280
|
format: str,
|
|
266
281
|
recursive: bool,
|
|
282
|
+
parallel: bool,
|
|
267
283
|
) -> None:
|
|
268
|
-
params = prepare_standard_command(ctx, paths, config_file, format, recursive)
|
|
284
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive, parallel)
|
|
269
285
|
run_linter_command(execute_fn, params)
|
|
270
286
|
|
|
271
287
|
return command
|
src/cli/linters/structure.py
CHANGED
|
@@ -44,6 +44,7 @@ from src.cli.utils import (
|
|
|
44
44
|
get_or_detect_project_root,
|
|
45
45
|
handle_linting_error,
|
|
46
46
|
load_config_file,
|
|
47
|
+
parallel_option,
|
|
47
48
|
setup_base_orchestrator,
|
|
48
49
|
validate_paths_exist,
|
|
49
50
|
)
|
|
@@ -112,6 +113,7 @@ def _parse_json_rules(rules: str) -> dict[str, Any]:
|
|
|
112
113
|
@click.option("--rules", "-r", help="Inline JSON rules configuration")
|
|
113
114
|
@format_option
|
|
114
115
|
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
116
|
+
@parallel_option
|
|
115
117
|
@click.pass_context
|
|
116
118
|
def file_placement( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
117
119
|
ctx: click.Context,
|
|
@@ -120,6 +122,7 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
120
122
|
rules: str | None,
|
|
121
123
|
format: str,
|
|
122
124
|
recursive: bool,
|
|
125
|
+
parallel: bool,
|
|
123
126
|
) -> None:
|
|
124
127
|
# Justification for Pylint disables:
|
|
125
128
|
# - too-many-arguments/positional: CLI requires 1 ctx + 1 arg + 4 options = 6 params
|
|
@@ -166,6 +169,7 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
166
169
|
rules,
|
|
167
170
|
format,
|
|
168
171
|
recursive,
|
|
172
|
+
parallel,
|
|
169
173
|
cmd_ctx.verbose,
|
|
170
174
|
cmd_ctx.project_root,
|
|
171
175
|
)
|
|
@@ -179,13 +183,14 @@ def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many
|
|
|
179
183
|
rules: str | None,
|
|
180
184
|
format: str,
|
|
181
185
|
recursive: bool,
|
|
186
|
+
parallel: bool,
|
|
182
187
|
verbose: bool,
|
|
183
188
|
project_root: Path | None = None,
|
|
184
189
|
) -> NoReturn:
|
|
185
190
|
"""Execute file placement linting."""
|
|
186
191
|
validate_paths_exist(path_objs)
|
|
187
192
|
orchestrator = _setup_orchestrator(path_objs, config_file, rules, verbose, project_root)
|
|
188
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
193
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
189
194
|
|
|
190
195
|
# Filter to only file-placement violations
|
|
191
196
|
violations = [v for v in all_violations if v.rule_id.startswith("file-placement")]
|
|
@@ -221,10 +226,10 @@ def _apply_pipeline_config_override(
|
|
|
221
226
|
|
|
222
227
|
|
|
223
228
|
def _run_pipeline_lint(
|
|
224
|
-
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
229
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
225
230
|
) -> list[Violation]:
|
|
226
231
|
"""Execute collection-pipeline lint on files or directories."""
|
|
227
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
232
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
228
233
|
return [v for v in all_violations if "collection-pipeline" in v.rule_id]
|
|
229
234
|
|
|
230
235
|
|
|
@@ -234,6 +239,7 @@ def _run_pipeline_lint(
|
|
|
234
239
|
@format_option
|
|
235
240
|
@click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
|
|
236
241
|
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
242
|
+
@parallel_option
|
|
237
243
|
@click.pass_context
|
|
238
244
|
def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
239
245
|
ctx: click.Context,
|
|
@@ -242,6 +248,7 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
|
|
|
242
248
|
format: str,
|
|
243
249
|
min_continues: int | None,
|
|
244
250
|
recursive: bool,
|
|
251
|
+
parallel: bool,
|
|
245
252
|
) -> None:
|
|
246
253
|
"""Check for collection pipeline anti-patterns in code.
|
|
247
254
|
|
|
@@ -289,6 +296,7 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
|
|
|
289
296
|
format,
|
|
290
297
|
min_continues,
|
|
291
298
|
recursive,
|
|
299
|
+
parallel,
|
|
292
300
|
cmd_ctx.verbose,
|
|
293
301
|
cmd_ctx.project_root,
|
|
294
302
|
)
|
|
@@ -302,6 +310,7 @@ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-posit
|
|
|
302
310
|
format: str,
|
|
303
311
|
min_continues: int | None,
|
|
304
312
|
recursive: bool,
|
|
313
|
+
parallel: bool,
|
|
305
314
|
verbose: bool,
|
|
306
315
|
project_root: Path | None = None,
|
|
307
316
|
) -> NoReturn:
|
|
@@ -309,7 +318,7 @@ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-posit
|
|
|
309
318
|
validate_paths_exist(path_objs)
|
|
310
319
|
orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
|
|
311
320
|
_apply_pipeline_config_override(orchestrator, min_continues, verbose)
|
|
312
|
-
pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive)
|
|
321
|
+
pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive, parallel)
|
|
313
322
|
|
|
314
323
|
if verbose:
|
|
315
324
|
logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
|
|
@@ -26,6 +26,7 @@ Suppressions:
|
|
|
26
26
|
|
|
27
27
|
import logging
|
|
28
28
|
import sys
|
|
29
|
+
from contextlib import suppress
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from typing import TYPE_CHECKING, NoReturn
|
|
31
32
|
|
|
@@ -85,7 +86,7 @@ def _apply_nesting_config_override(
|
|
|
85
86
|
def _apply_nesting_to_languages(nesting_config: dict, max_depth: int) -> None:
|
|
86
87
|
"""Apply max_depth to language-specific configs."""
|
|
87
88
|
for lang in ["python", "typescript", "javascript"]:
|
|
88
|
-
|
|
89
|
+
with suppress(KeyError):
|
|
89
90
|
nesting_config[lang]["max_nesting_depth"] = max_depth
|
|
90
91
|
|
|
91
92
|
|
|
@@ -220,10 +221,10 @@ def _apply_srp_config_override(
|
|
|
220
221
|
|
|
221
222
|
|
|
222
223
|
def _run_srp_lint(
|
|
223
|
-
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool
|
|
224
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
224
225
|
) -> list[Violation]:
|
|
225
226
|
"""Execute SRP lint on files or directories."""
|
|
226
|
-
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
227
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
227
228
|
return [v for v in all_violations if "srp" in v.rule_id]
|
|
228
229
|
|
|
229
230
|
|
|
@@ -234,6 +235,7 @@ def _run_srp_lint(
|
|
|
234
235
|
@click.option("--max-methods", type=int, help="Override max methods per class (default: 7)")
|
|
235
236
|
@click.option("--max-loc", type=int, help="Override max lines of code per class (default: 200)")
|
|
236
237
|
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
238
|
+
@parallel_option
|
|
237
239
|
@click.pass_context
|
|
238
240
|
def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
239
241
|
ctx: click.Context,
|
|
@@ -243,6 +245,7 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
243
245
|
max_methods: int | None,
|
|
244
246
|
max_loc: int | None,
|
|
245
247
|
recursive: bool,
|
|
248
|
+
parallel: bool,
|
|
246
249
|
) -> None:
|
|
247
250
|
"""Check for Single Responsibility Principle violations.
|
|
248
251
|
|
|
@@ -293,6 +296,7 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
293
296
|
max_methods,
|
|
294
297
|
max_loc,
|
|
295
298
|
recursive,
|
|
299
|
+
parallel,
|
|
296
300
|
cmd_ctx.verbose,
|
|
297
301
|
cmd_ctx.project_root,
|
|
298
302
|
)
|
|
@@ -307,6 +311,7 @@ def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional
|
|
|
307
311
|
max_methods: int | None,
|
|
308
312
|
max_loc: int | None,
|
|
309
313
|
recursive: bool,
|
|
314
|
+
parallel: bool,
|
|
310
315
|
verbose: bool,
|
|
311
316
|
project_root: Path | None = None,
|
|
312
317
|
) -> NoReturn:
|
|
@@ -314,7 +319,7 @@ def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional
|
|
|
314
319
|
validate_paths_exist(path_objs)
|
|
315
320
|
orchestrator = _setup_srp_orchestrator(path_objs, config_file, verbose, project_root)
|
|
316
321
|
_apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
|
|
317
|
-
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive)
|
|
322
|
+
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive, parallel)
|
|
318
323
|
|
|
319
324
|
if verbose:
|
|
320
325
|
logger.info(f"Found {len(srp_violations)} SRP violation(s)")
|
src/cli/utils.py
CHANGED
|
@@ -24,8 +24,9 @@ Implementation: Uses Click decorators for option definitions, deferred imports f
|
|
|
24
24
|
import logging
|
|
25
25
|
import sys
|
|
26
26
|
from collections.abc import Callable
|
|
27
|
+
from contextlib import suppress
|
|
27
28
|
from pathlib import Path
|
|
28
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
29
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
29
30
|
|
|
30
31
|
import click
|
|
31
32
|
|
|
@@ -188,9 +189,8 @@ def get_project_root_from_context(ctx: click.Context) -> Path | None:
|
|
|
188
189
|
Path to determined project root, or None for auto-detection from target paths
|
|
189
190
|
"""
|
|
190
191
|
# Check if already determined and cached
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return cached
|
|
192
|
+
with suppress(KeyError):
|
|
193
|
+
return cast(Path | None, ctx.obj["project_root"])
|
|
194
194
|
|
|
195
195
|
project_root = _determine_project_root_for_context(ctx)
|
|
196
196
|
ctx.obj["project_root"] = project_root
|
src/config.py
CHANGED
|
@@ -237,37 +237,47 @@ def _validate_required_keys(config: dict[str, Any], errors: list[str]) -> None:
|
|
|
237
237
|
def _validate_log_level(config: dict[str, Any], errors: list[str]) -> None:
|
|
238
238
|
"""Validate log level is a valid value."""
|
|
239
239
|
valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
try:
|
|
241
|
+
log_level = config["log_level"]
|
|
242
|
+
except KeyError:
|
|
243
|
+
return # Optional key not present
|
|
244
|
+
if log_level not in valid_log_levels:
|
|
245
|
+
errors.append(
|
|
246
|
+
f"Invalid log_level: {log_level}. Must be one of: {', '.join(valid_log_levels)}"
|
|
247
|
+
)
|
|
246
248
|
|
|
247
249
|
|
|
248
250
|
def _validate_output_format(config: dict[str, Any], errors: list[str]) -> None:
|
|
249
251
|
"""Validate output format is a valid value."""
|
|
250
252
|
valid_formats = ["text", "json", "yaml"]
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
try:
|
|
254
|
+
output_format = config["output_format"]
|
|
255
|
+
except KeyError:
|
|
256
|
+
return # Optional key not present
|
|
257
|
+
if output_format not in valid_formats:
|
|
258
|
+
errors.append(
|
|
259
|
+
f"Invalid output_format: {output_format}. Must be one of: {', '.join(valid_formats)}"
|
|
260
|
+
)
|
|
257
261
|
|
|
258
262
|
|
|
259
263
|
def _validate_max_retries(config: dict[str, Any], errors: list[str]) -> None:
|
|
260
264
|
"""Validate max_retries configuration value."""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
try:
|
|
266
|
+
max_retries = config["max_retries"]
|
|
267
|
+
except KeyError:
|
|
268
|
+
return # Optional key not present
|
|
269
|
+
if not isinstance(max_retries, int) or max_retries < 0:
|
|
270
|
+
errors.append("max_retries must be a non-negative integer")
|
|
264
271
|
|
|
265
272
|
|
|
266
273
|
def _validate_timeout(config: dict[str, Any], errors: list[str]) -> None:
|
|
267
274
|
"""Validate timeout configuration value."""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
try:
|
|
276
|
+
timeout = config["timeout"]
|
|
277
|
+
except KeyError:
|
|
278
|
+
return # Optional key not present
|
|
279
|
+
if not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
280
|
+
errors.append("timeout must be a positive number")
|
|
271
281
|
|
|
272
282
|
|
|
273
283
|
def _validate_numeric_values(config: dict[str, Any], errors: list[str]) -> None:
|
|
@@ -278,9 +288,12 @@ def _validate_numeric_values(config: dict[str, Any], errors: list[str]) -> None:
|
|
|
278
288
|
|
|
279
289
|
def _validate_string_values(config: dict[str, Any], errors: list[str]) -> None:
|
|
280
290
|
"""Validate string configuration values."""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
try:
|
|
292
|
+
app_name = config["app_name"]
|
|
293
|
+
except KeyError:
|
|
294
|
+
return # Optional key not present
|
|
295
|
+
if not isinstance(app_name, str) or not app_name.strip():
|
|
296
|
+
errors.append("app_name must be a non-empty string")
|
|
284
297
|
|
|
285
298
|
|
|
286
299
|
def validate_config(config: dict[str, Any]) -> tuple[bool, list[str]]:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Base class for Python-only linters with common boilerplate
|
|
3
|
+
|
|
4
|
+
Scope: Shared infrastructure for Python-only lint rules
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonOnlyLintRule abstract base class that handles common boilerplate
|
|
7
|
+
for Python-only linters. Subclasses implement the abstract properties and analysis
|
|
8
|
+
method while the base class handles language checking, config loading, and enabled
|
|
9
|
+
checking. This eliminates duplicate code across Python-only linters like CQS and LBYL.
|
|
10
|
+
|
|
11
|
+
Dependencies: BaseLintRule, BaseLintContext, Language, load_linter_config, has_file_content
|
|
12
|
+
|
|
13
|
+
Exports: PythonOnlyLintRule
|
|
14
|
+
|
|
15
|
+
Interfaces: Subclasses implement _config_key, _config_class, _analyze, and rule metadata
|
|
16
|
+
|
|
17
|
+
Implementation: Template method pattern for Python linter boilerplate
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import abstractmethod
|
|
21
|
+
from typing import Any, Generic
|
|
22
|
+
|
|
23
|
+
from .base import BaseLintContext, BaseLintRule
|
|
24
|
+
from .constants import Language
|
|
25
|
+
from .linter_utils import ConfigType, has_file_content, load_linter_config
|
|
26
|
+
from .types import Violation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PythonOnlyLintRule(BaseLintRule, Generic[ConfigType]):
|
|
30
|
+
"""Base class for Python-only linters with common boilerplate.
|
|
31
|
+
|
|
32
|
+
Handles language checking, config loading, and enabled checking.
|
|
33
|
+
Subclasses provide the config key, config class, and analysis logic.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: ConfigType | None = None) -> None:
|
|
37
|
+
"""Initialize with optional config override.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Optional configuration override for testing
|
|
41
|
+
"""
|
|
42
|
+
self._config_override = config
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _config_key(self) -> str:
|
|
47
|
+
"""Configuration key in metadata (e.g., 'cqs', 'lbyl')."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def _config_class(self) -> type[ConfigType]:
|
|
53
|
+
"""Configuration class type."""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _analyze(self, code: str, file_path: str, config: ConfigType) -> list[Violation]:
|
|
58
|
+
"""Perform linter-specific analysis.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
code: Python source code
|
|
62
|
+
file_path: Path to the file
|
|
63
|
+
config: Loaded configuration
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of violations found
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
71
|
+
"""Check for violations in the given context.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
context: The lint context containing file information.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of violations found.
|
|
78
|
+
"""
|
|
79
|
+
if not self._should_analyze(context):
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
config = self._get_config(context)
|
|
83
|
+
if not self._is_enabled(config):
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
file_path = str(context.file_path) if context.file_path else "unknown"
|
|
87
|
+
return self._analyze(context.file_content or "", file_path, config)
|
|
88
|
+
|
|
89
|
+
def _should_analyze(self, context: BaseLintContext) -> bool:
|
|
90
|
+
"""Check if context should be analyzed."""
|
|
91
|
+
return context.language == Language.PYTHON and has_file_content(context)
|
|
92
|
+
|
|
93
|
+
def _get_config(self, context: BaseLintContext) -> ConfigType:
|
|
94
|
+
"""Get configuration, using override if provided."""
|
|
95
|
+
if self._config_override is not None:
|
|
96
|
+
return self._config_override
|
|
97
|
+
return load_linter_config(context, self._config_key, self._config_class)
|
|
98
|
+
|
|
99
|
+
def _is_enabled(self, config: Any) -> bool:
|
|
100
|
+
"""Check if linter is enabled in config."""
|
|
101
|
+
return getattr(config, "enabled", True)
|
src/linter_config/ignore.py
CHANGED
|
@@ -27,6 +27,7 @@ Suppressions:
|
|
|
27
27
|
|
|
28
28
|
import logging
|
|
29
29
|
import re
|
|
30
|
+
from contextlib import suppress
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import TYPE_CHECKING
|
|
32
33
|
|
|
@@ -66,7 +67,7 @@ class IgnoreDirectiveParser:
|
|
|
66
67
|
def is_ignored(self, file_path: Path) -> bool:
|
|
67
68
|
"""Check if file matches repository-level ignore patterns (cached)."""
|
|
68
69
|
path_str = str(file_path)
|
|
69
|
-
|
|
70
|
+
with suppress(KeyError):
|
|
70
71
|
return self._ignore_cache[path_str]
|
|
71
72
|
try:
|
|
72
73
|
check_path = str(file_path.relative_to(self.project_root))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CQS (Command-Query Separation) linter package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect CQS violations in Python and TypeScript code
|
|
5
|
+
|
|
6
|
+
Overview: Package providing CQS violation detection for Python and TypeScript code.
|
|
7
|
+
Identifies functions that mix INPUT operations (queries that return values captured
|
|
8
|
+
in variables) and OUTPUT operations (commands that perform side effects without
|
|
9
|
+
capturing return values). Functions should either query state and return a value,
|
|
10
|
+
or command a change and return nothing. Mixing these violates CQS principles and
|
|
11
|
+
makes code harder to reason about.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for Python parsing, tree-sitter for TypeScript parsing
|
|
14
|
+
|
|
15
|
+
Exports: CQSConfig, CQSPattern, CQSRule, FunctionAnalyzer, InputOperation, OutputOperation,
|
|
16
|
+
PythonCQSAnalyzer, TypeScriptCQSAnalyzer, TypeScriptFunctionAnalyzer,
|
|
17
|
+
TypeScriptInputDetector, TypeScriptOutputDetector, build_cqs_violation
|
|
18
|
+
|
|
19
|
+
Interfaces: CQSConfig.from_dict() for YAML configuration loading,
|
|
20
|
+
CQSRule.check() for BaseLintRule interface
|
|
21
|
+
|
|
22
|
+
Implementation: AST-based pattern detection for Python, tree-sitter for TypeScript,
|
|
23
|
+
with configurable ignore rules
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .config import CQSConfig
|
|
27
|
+
from .function_analyzer import FunctionAnalyzer
|
|
28
|
+
from .input_detector import InputDetector
|
|
29
|
+
from .linter import CQSRule
|
|
30
|
+
from .output_detector import OutputDetector
|
|
31
|
+
from .python_analyzer import PythonCQSAnalyzer
|
|
32
|
+
from .types import CQSPattern, InputOperation, OutputOperation
|
|
33
|
+
from .typescript_cqs_analyzer import TypeScriptCQSAnalyzer
|
|
34
|
+
from .typescript_function_analyzer import TypeScriptFunctionAnalyzer
|
|
35
|
+
from .typescript_input_detector import TypeScriptInputDetector
|
|
36
|
+
from .typescript_output_detector import TypeScriptOutputDetector
|
|
37
|
+
from .violation_builder import build_cqs_violation
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"CQSConfig",
|
|
41
|
+
"CQSPattern",
|
|
42
|
+
"CQSRule",
|
|
43
|
+
"FunctionAnalyzer",
|
|
44
|
+
"InputDetector",
|
|
45
|
+
"InputOperation",
|
|
46
|
+
"OutputDetector",
|
|
47
|
+
"OutputOperation",
|
|
48
|
+
"PythonCQSAnalyzer",
|
|
49
|
+
"TypeScriptCQSAnalyzer",
|
|
50
|
+
"TypeScriptFunctionAnalyzer",
|
|
51
|
+
"TypeScriptInputDetector",
|
|
52
|
+
"TypeScriptOutputDetector",
|
|
53
|
+
"build_cqs_violation",
|
|
54
|
+
]
|