thailint 0.13.0__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/cli/linters/__init__.py +6 -0
- src/cli/linters/code_patterns.py +75 -333
- src/cli/linters/code_smells.py +47 -168
- src/cli/linters/documentation.py +21 -98
- src/cli/linters/performance.py +274 -0
- src/cli/linters/shared.py +232 -6
- src/cli/linters/structure.py +23 -21
- src/cli/linters/structure_quality.py +25 -21
- src/core/linter_utils.py +91 -6
- src/linters/file_header/atemporal_detector.py +54 -40
- src/linters/file_header/config.py +14 -0
- src/linters/lazy_ignores/python_analyzer.py +5 -1
- src/linters/lazy_ignores/types.py +2 -0
- src/linters/method_property/config.py +0 -1
- src/linters/method_property/linter.py +0 -6
- src/linters/nesting/linter.py +11 -6
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/templates/thailint_config_template.yaml +30 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/METADATA +3 -2
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/RECORD +32 -22
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/WHEEL +0 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.13.0.dist-info → thailint-0.14.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/shared.py
CHANGED
|
@@ -4,27 +4,172 @@ Purpose: Shared utilities for linter CLI commands
|
|
|
4
4
|
Scope: Common helper functions and patterns used across all linter command modules
|
|
5
5
|
|
|
6
6
|
Overview: Provides reusable utilities for linter CLI commands including config section management,
|
|
7
|
-
config value setting with logging,
|
|
8
|
-
duplication across linter command modules
|
|
9
|
-
|
|
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.
|
|
10
11
|
|
|
11
|
-
Dependencies: logging for debug output, pathlib for Path type hints
|
|
12
|
+
Dependencies: logging for debug output, pathlib for Path type hints, click for Context type,
|
|
13
|
+
dataclasses for CommandContext
|
|
12
14
|
|
|
13
|
-
Exports: ensure_config_section, set_config_value, filter_violations_by_prefix
|
|
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
|
|
14
19
|
|
|
15
|
-
Interfaces: Orchestrator config dict manipulation, violation list filtering
|
|
20
|
+
Interfaces: Orchestrator config dict manipulation, violation list filtering, CLI context extraction,
|
|
21
|
+
help text generation
|
|
16
22
|
|
|
17
23
|
Implementation: Pure helper functions with no side effects beyond config mutation and logging
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
26
|
import logging
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
21
29
|
from typing import TYPE_CHECKING, Any
|
|
22
30
|
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
from src.cli.utils import format_option, get_project_root_from_context, handle_linting_error
|
|
23
34
|
from src.core.types import Violation
|
|
24
35
|
|
|
25
36
|
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
|
|
26
39
|
from src.orchestrator.core import Orchestrator
|
|
27
40
|
|
|
41
|
+
|
|
42
|
+
def standard_linter_options(f: Any) -> Any:
|
|
43
|
+
"""Apply standard linter CLI options to a command.
|
|
44
|
+
|
|
45
|
+
Bundles the common options used by most linter commands:
|
|
46
|
+
- paths argument (variadic)
|
|
47
|
+
- config file option
|
|
48
|
+
- format option
|
|
49
|
+
- recursive option
|
|
50
|
+
- pass_context
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
@cli.command("my-linter")
|
|
54
|
+
@standard_linter_options
|
|
55
|
+
def my_linter(ctx, paths, config_file, format, recursive):
|
|
56
|
+
...
|
|
57
|
+
"""
|
|
58
|
+
f = click.pass_context(f)
|
|
59
|
+
f = click.option(
|
|
60
|
+
"--recursive/--no-recursive", default=True, help="Scan directories recursively"
|
|
61
|
+
)(f)
|
|
62
|
+
f = format_option(f)
|
|
63
|
+
f = click.option(
|
|
64
|
+
"--config", "-c", "config_file", type=click.Path(), help="Path to config file"
|
|
65
|
+
)(f)
|
|
66
|
+
f = click.argument("paths", nargs=-1, type=click.Path())(f)
|
|
67
|
+
return f
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CommandContext:
|
|
72
|
+
"""Extracted context from CLI command invocation.
|
|
73
|
+
|
|
74
|
+
Consolidates common CLI command setup into a reusable structure.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
verbose: bool
|
|
78
|
+
project_root: Path | None
|
|
79
|
+
path_objs: list[Path]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ExecuteParams:
|
|
84
|
+
"""Parameters for linter execution functions.
|
|
85
|
+
|
|
86
|
+
Bundles the common parameters passed to _execute_*_lint functions
|
|
87
|
+
to reduce function signature duplication across CLI modules.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
path_objs: list[Path]
|
|
91
|
+
config_file: str | None
|
|
92
|
+
format: str
|
|
93
|
+
recursive: bool
|
|
94
|
+
verbose: bool
|
|
95
|
+
project_root: Path | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_command_context(ctx: click.Context, paths: tuple[str, ...]) -> CommandContext:
|
|
99
|
+
"""Extract common context values from CLI command invocation.
|
|
100
|
+
|
|
101
|
+
Consolidates the repeated pattern of extracting verbose, project_root,
|
|
102
|
+
and converting paths to Path objects with default handling.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
ctx: Click context from command invocation
|
|
106
|
+
paths: Tuple of path strings from command arguments
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
CommandContext with extracted values
|
|
110
|
+
"""
|
|
111
|
+
verbose: bool = ctx.obj.get("verbose", False)
|
|
112
|
+
project_root = get_project_root_from_context(ctx)
|
|
113
|
+
|
|
114
|
+
if not paths:
|
|
115
|
+
paths = (".",)
|
|
116
|
+
|
|
117
|
+
path_objs = [Path(p) for p in paths]
|
|
118
|
+
|
|
119
|
+
return CommandContext(verbose=verbose, project_root=project_root, path_objs=path_objs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def prepare_standard_command(
|
|
123
|
+
ctx: click.Context,
|
|
124
|
+
paths: tuple[str, ...],
|
|
125
|
+
config_file: str | None,
|
|
126
|
+
format: str,
|
|
127
|
+
recursive: bool,
|
|
128
|
+
) -> ExecuteParams:
|
|
129
|
+
"""Prepare standard linter command execution parameters.
|
|
130
|
+
|
|
131
|
+
Combines context extraction and ExecuteParams creation into a single call.
|
|
132
|
+
Use with commands that have the standard options (config, format, recursive).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
ctx: Click context from command invocation
|
|
136
|
+
paths: Tuple of path strings from command arguments
|
|
137
|
+
config_file: Optional config file path
|
|
138
|
+
format: Output format
|
|
139
|
+
recursive: Whether to scan recursively
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
ExecuteParams ready for _execute_*_lint function
|
|
143
|
+
"""
|
|
144
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
145
|
+
return ExecuteParams(
|
|
146
|
+
path_objs=cmd_ctx.path_objs,
|
|
147
|
+
config_file=config_file,
|
|
148
|
+
format=format,
|
|
149
|
+
recursive=recursive,
|
|
150
|
+
verbose=cmd_ctx.verbose,
|
|
151
|
+
project_root=cmd_ctx.project_root,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_linter_command(
|
|
156
|
+
execute_fn: "Callable[[ExecuteParams], None]",
|
|
157
|
+
params: ExecuteParams,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Run a linter command with standard error handling.
|
|
160
|
+
|
|
161
|
+
Wraps the try/except pattern used by all linter commands.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
execute_fn: The _execute_*_lint function to call
|
|
165
|
+
params: ExecuteParams for the execution
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
execute_fn(params)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
handle_linting_error(e, params.verbose)
|
|
171
|
+
|
|
172
|
+
|
|
28
173
|
# Configure module logger
|
|
29
174
|
logger = logging.getLogger(__name__)
|
|
30
175
|
|
|
@@ -87,3 +232,84 @@ def filter_violations_by_startswith(violations: list[Violation], prefix: str) ->
|
|
|
87
232
|
Filtered list of violations where rule_id starts with the prefix
|
|
88
233
|
"""
|
|
89
234
|
return [v for v in violations if v.rule_id.startswith(prefix)]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def create_linter_command(
|
|
238
|
+
name: str,
|
|
239
|
+
execute_fn: "Callable[[ExecuteParams], None]",
|
|
240
|
+
brief: str,
|
|
241
|
+
description: str,
|
|
242
|
+
) -> "Callable[..., None]":
|
|
243
|
+
"""Create a standard linter CLI command.
|
|
244
|
+
|
|
245
|
+
Factory function that generates Click commands with consistent structure,
|
|
246
|
+
eliminating boilerplate duplication across linter command modules.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
name: CLI command name (e.g., "magic-numbers")
|
|
250
|
+
execute_fn: The _execute_*_lint function to call
|
|
251
|
+
brief: Brief one-line description
|
|
252
|
+
description: Detailed multi-line description
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Decorated Click command function
|
|
256
|
+
"""
|
|
257
|
+
from src.cli.main import cli
|
|
258
|
+
|
|
259
|
+
@cli.command(name, help=make_linter_help(name, brief, description))
|
|
260
|
+
@standard_linter_options
|
|
261
|
+
def command(
|
|
262
|
+
ctx: click.Context,
|
|
263
|
+
paths: tuple[str, ...],
|
|
264
|
+
config_file: str | None,
|
|
265
|
+
format: str,
|
|
266
|
+
recursive: bool,
|
|
267
|
+
) -> None:
|
|
268
|
+
params = prepare_standard_command(ctx, paths, config_file, format, recursive)
|
|
269
|
+
run_linter_command(execute_fn, params)
|
|
270
|
+
|
|
271
|
+
return command
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def make_linter_help(command: str, brief: str, description: str) -> str:
|
|
275
|
+
"""Generate standardized CLI help text for linter commands.
|
|
276
|
+
|
|
277
|
+
Creates consistent help text following the established pattern with
|
|
278
|
+
examples showing common usage patterns.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
command: CLI command name (e.g., "magic-numbers")
|
|
282
|
+
brief: Brief one-line description
|
|
283
|
+
description: Detailed multi-line description
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Formatted help text string for Click command
|
|
287
|
+
"""
|
|
288
|
+
return f"""{brief}
|
|
289
|
+
|
|
290
|
+
{description}
|
|
291
|
+
|
|
292
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
|
|
296
|
+
\\b
|
|
297
|
+
# Check current directory (all files recursively)
|
|
298
|
+
thai-lint {command}
|
|
299
|
+
|
|
300
|
+
\\b
|
|
301
|
+
# Check specific directory
|
|
302
|
+
thai-lint {command} src/
|
|
303
|
+
|
|
304
|
+
\\b
|
|
305
|
+
# Check single file
|
|
306
|
+
thai-lint {command} src/app.py
|
|
307
|
+
|
|
308
|
+
\\b
|
|
309
|
+
# Get JSON output
|
|
310
|
+
thai-lint {command} --format json .
|
|
311
|
+
|
|
312
|
+
\\b
|
|
313
|
+
# Use custom config file
|
|
314
|
+
thai-lint {command} --config .thailint.yaml src/
|
|
315
|
+
"""
|
src/cli/linters/structure.py
CHANGED
|
@@ -23,7 +23,6 @@ SRP Exception: CLI command modules follow Click framework patterns requiring sim
|
|
|
23
23
|
Suppressions:
|
|
24
24
|
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
25
|
"""
|
|
26
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
27
26
|
|
|
28
27
|
import json
|
|
29
28
|
import logging
|
|
@@ -33,13 +32,16 @@ from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
33
32
|
|
|
34
33
|
import click
|
|
35
34
|
|
|
36
|
-
from src.cli.linters.shared import
|
|
35
|
+
from src.cli.linters.shared import (
|
|
36
|
+
ensure_config_section,
|
|
37
|
+
extract_command_context,
|
|
38
|
+
set_config_value,
|
|
39
|
+
)
|
|
37
40
|
from src.cli.main import cli
|
|
38
41
|
from src.cli.utils import (
|
|
39
42
|
execute_linting_on_paths,
|
|
40
43
|
format_option,
|
|
41
44
|
get_or_detect_project_root,
|
|
42
|
-
get_project_root_from_context,
|
|
43
45
|
handle_linting_error,
|
|
44
46
|
load_config_file,
|
|
45
47
|
setup_base_orchestrator,
|
|
@@ -155,20 +157,20 @@ def file_placement( # pylint: disable=too-many-arguments,too-many-positional-ar
|
|
|
155
157
|
# Inline JSON rules
|
|
156
158
|
thai-lint file-placement --rules '{"allow": [".*\\.py$"]}' .
|
|
157
159
|
"""
|
|
158
|
-
|
|
159
|
-
project_root = get_project_root_from_context(ctx)
|
|
160
|
-
|
|
161
|
-
if not paths:
|
|
162
|
-
paths = (".",)
|
|
163
|
-
|
|
164
|
-
path_objs = [Path(p) for p in paths]
|
|
160
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
165
161
|
|
|
166
162
|
try:
|
|
167
163
|
_execute_file_placement_lint(
|
|
168
|
-
path_objs,
|
|
164
|
+
cmd_ctx.path_objs,
|
|
165
|
+
config_file,
|
|
166
|
+
rules,
|
|
167
|
+
format,
|
|
168
|
+
recursive,
|
|
169
|
+
cmd_ctx.verbose,
|
|
170
|
+
cmd_ctx.project_root,
|
|
169
171
|
)
|
|
170
172
|
except Exception as e:
|
|
171
|
-
handle_linting_error(e, verbose)
|
|
173
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
172
174
|
|
|
173
175
|
|
|
174
176
|
def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -278,20 +280,20 @@ def pipeline( # pylint: disable=too-many-arguments,too-many-positional-argument
|
|
|
278
280
|
# Use custom config file
|
|
279
281
|
thai-lint pipeline --config .thailint.yaml src/
|
|
280
282
|
"""
|
|
281
|
-
|
|
282
|
-
project_root = get_project_root_from_context(ctx)
|
|
283
|
-
|
|
284
|
-
if not paths:
|
|
285
|
-
paths = (".",)
|
|
286
|
-
|
|
287
|
-
path_objs = [Path(p) for p in paths]
|
|
283
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
288
284
|
|
|
289
285
|
try:
|
|
290
286
|
_execute_pipeline_lint(
|
|
291
|
-
path_objs,
|
|
287
|
+
cmd_ctx.path_objs,
|
|
288
|
+
config_file,
|
|
289
|
+
format,
|
|
290
|
+
min_continues,
|
|
291
|
+
recursive,
|
|
292
|
+
cmd_ctx.verbose,
|
|
293
|
+
cmd_ctx.project_root,
|
|
292
294
|
)
|
|
293
295
|
except Exception as e:
|
|
294
|
-
handle_linting_error(e, verbose)
|
|
296
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
295
297
|
|
|
296
298
|
|
|
297
299
|
def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -23,7 +23,6 @@ SRP Exception: CLI command modules follow Click framework patterns requiring sim
|
|
|
23
23
|
Suppressions:
|
|
24
24
|
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
25
|
"""
|
|
26
|
-
# dry: ignore-block - CLI commands follow Click framework pattern with intentional repetition
|
|
27
26
|
|
|
28
27
|
import logging
|
|
29
28
|
import sys
|
|
@@ -32,12 +31,15 @@ from typing import TYPE_CHECKING, NoReturn
|
|
|
32
31
|
|
|
33
32
|
import click
|
|
34
33
|
|
|
35
|
-
from src.cli.linters.shared import
|
|
34
|
+
from src.cli.linters.shared import (
|
|
35
|
+
ensure_config_section,
|
|
36
|
+
extract_command_context,
|
|
37
|
+
set_config_value,
|
|
38
|
+
)
|
|
36
39
|
from src.cli.main import cli
|
|
37
40
|
from src.cli.utils import (
|
|
38
41
|
execute_linting_on_paths,
|
|
39
42
|
format_option,
|
|
40
|
-
get_project_root_from_context,
|
|
41
43
|
handle_linting_error,
|
|
42
44
|
parallel_option,
|
|
43
45
|
setup_base_orchestrator,
|
|
@@ -153,20 +155,21 @@ def nesting( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
153
155
|
# Use custom config file
|
|
154
156
|
thai-lint nesting --config .thailint.yaml src/
|
|
155
157
|
"""
|
|
156
|
-
|
|
157
|
-
project_root = get_project_root_from_context(ctx)
|
|
158
|
-
|
|
159
|
-
if not paths:
|
|
160
|
-
paths = (".",)
|
|
161
|
-
|
|
162
|
-
path_objs = [Path(p) for p in paths]
|
|
158
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
163
159
|
|
|
164
160
|
try:
|
|
165
161
|
_execute_nesting_lint(
|
|
166
|
-
path_objs,
|
|
162
|
+
cmd_ctx.path_objs,
|
|
163
|
+
config_file,
|
|
164
|
+
format,
|
|
165
|
+
max_depth,
|
|
166
|
+
recursive,
|
|
167
|
+
parallel,
|
|
168
|
+
cmd_ctx.verbose,
|
|
169
|
+
cmd_ctx.project_root,
|
|
167
170
|
)
|
|
168
171
|
except Exception as e:
|
|
169
|
-
handle_linting_error(e, verbose)
|
|
172
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
170
173
|
|
|
171
174
|
|
|
172
175
|
def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
@@ -280,20 +283,21 @@ def srp( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
280
283
|
# Use custom config file
|
|
281
284
|
thai-lint srp --config .thailint.yaml src/
|
|
282
285
|
"""
|
|
283
|
-
|
|
284
|
-
project_root = get_project_root_from_context(ctx)
|
|
285
|
-
|
|
286
|
-
if not paths:
|
|
287
|
-
paths = (".",)
|
|
288
|
-
|
|
289
|
-
path_objs = [Path(p) for p in paths]
|
|
286
|
+
cmd_ctx = extract_command_context(ctx, paths)
|
|
290
287
|
|
|
291
288
|
try:
|
|
292
289
|
_execute_srp_lint(
|
|
293
|
-
path_objs,
|
|
290
|
+
cmd_ctx.path_objs,
|
|
291
|
+
config_file,
|
|
292
|
+
format,
|
|
293
|
+
max_methods,
|
|
294
|
+
max_loc,
|
|
295
|
+
recursive,
|
|
296
|
+
cmd_ctx.verbose,
|
|
297
|
+
cmd_ctx.project_root,
|
|
294
298
|
)
|
|
295
299
|
except Exception as e:
|
|
296
|
-
handle_linting_error(e, verbose)
|
|
300
|
+
handle_linting_error(e, cmd_ctx.verbose)
|
|
297
301
|
|
|
298
302
|
|
|
299
303
|
def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
src/core/linter_utils.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: Shared utility functions for linter framework patterns
|
|
3
3
|
|
|
4
|
-
Scope: Common config loading, metadata access,
|
|
4
|
+
Scope: Common config loading, metadata access, context validation, and AST parsing utilities
|
|
5
5
|
|
|
6
6
|
Overview: Provides reusable helper functions to eliminate duplication across linter implementations.
|
|
7
7
|
Includes utilities for loading configuration from context metadata with language-specific overrides,
|
|
8
|
-
extracting metadata fields safely with type validation,
|
|
9
|
-
common patterns used by srp, nesting, dry,
|
|
10
|
-
while maintaining type safety
|
|
8
|
+
extracting metadata fields safely with type validation, validating context state, and parsing
|
|
9
|
+
Python AST with syntax error handling. Standardizes common patterns used by srp, nesting, dry,
|
|
10
|
+
performance, and file_placement linters. Reduces boilerplate code while maintaining type safety
|
|
11
|
+
and proper error handling.
|
|
11
12
|
|
|
12
|
-
Dependencies: BaseLintContext from src.core.base
|
|
13
|
+
Dependencies: BaseLintContext from src.core.base, ast for Python parsing
|
|
13
14
|
|
|
14
|
-
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content
|
|
15
|
+
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content, parse_python_ast,
|
|
16
|
+
with_parsed_python
|
|
15
17
|
|
|
16
18
|
Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
|
|
17
19
|
|
|
@@ -20,11 +22,27 @@ Implementation: Type-safe metadata access with fallbacks, generic config loading
|
|
|
20
22
|
Suppressions:
|
|
21
23
|
- invalid-name: T type variable follows Python generic naming convention
|
|
22
24
|
- type:ignore[return-value]: Generic config factory with runtime type checking
|
|
25
|
+
- unnecessary-ellipsis: Protocol method bodies use ellipsis per PEP 544
|
|
26
|
+
- B101: Assert used to narrow type after parse_python_ast returns non-None tree
|
|
23
27
|
"""
|
|
24
28
|
|
|
29
|
+
import ast
|
|
30
|
+
from collections.abc import Callable
|
|
25
31
|
from typing import Any, Protocol, TypeVar
|
|
26
32
|
|
|
27
33
|
from src.core.base import BaseLintContext
|
|
34
|
+
from src.core.types import Violation
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Protocol for violation builders that support syntax error handling
|
|
38
|
+
class SyntaxErrorViolationBuilder(Protocol):
|
|
39
|
+
"""Protocol for violation builders that can create syntax error violations."""
|
|
40
|
+
|
|
41
|
+
def create_syntax_error_violation(
|
|
42
|
+
self, error: SyntaxError, context: BaseLintContext
|
|
43
|
+
) -> Violation:
|
|
44
|
+
"""Create a violation for a syntax error."""
|
|
45
|
+
... # pylint: disable=unnecessary-ellipsis
|
|
28
46
|
|
|
29
47
|
|
|
30
48
|
# Protocol for config classes that support from_dict
|
|
@@ -170,3 +188,70 @@ def should_process_file(context: BaseLintContext) -> bool:
|
|
|
170
188
|
True if file has both content and path available
|
|
171
189
|
"""
|
|
172
190
|
return has_file_content(context) and has_file_path(context)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def parse_python_ast(
|
|
194
|
+
context: BaseLintContext,
|
|
195
|
+
violation_builder: SyntaxErrorViolationBuilder,
|
|
196
|
+
) -> tuple[ast.Module | None, list[Violation]]:
|
|
197
|
+
"""Parse Python AST from context, handling syntax errors gracefully.
|
|
198
|
+
|
|
199
|
+
Provides a standard pattern for Python linters to parse AST and handle
|
|
200
|
+
syntax errors by returning a violation instead of crashing.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
context: Lint context containing file content
|
|
204
|
+
violation_builder: Builder to create syntax error violations
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (ast_tree, violations):
|
|
208
|
+
- On success: (ast.Module, [])
|
|
209
|
+
- On syntax error: (None, [syntax_error_violation])
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
tree, errors = parse_python_ast(context, self._violation_builder)
|
|
213
|
+
if errors:
|
|
214
|
+
return errors
|
|
215
|
+
# ... use tree for analysis
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
tree = ast.parse(context.file_content or "")
|
|
219
|
+
return tree, []
|
|
220
|
+
except SyntaxError as e:
|
|
221
|
+
violation = violation_builder.create_syntax_error_violation(e, context)
|
|
222
|
+
return None, [violation]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def with_parsed_python(
|
|
226
|
+
context: BaseLintContext,
|
|
227
|
+
violation_builder: SyntaxErrorViolationBuilder,
|
|
228
|
+
on_success: Callable[[ast.Module], list[Violation]],
|
|
229
|
+
) -> list[Violation]:
|
|
230
|
+
"""Parse Python and call on_success with the AST, or return parse errors.
|
|
231
|
+
|
|
232
|
+
Eliminates the repeated parse-check-assert pattern across Python linters.
|
|
233
|
+
On parse success, calls on_success with a guaranteed non-None AST tree.
|
|
234
|
+
On parse failure, returns syntax error violations.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
context: Lint context containing file content
|
|
238
|
+
violation_builder: Builder to create syntax error violations
|
|
239
|
+
on_success: Callback receiving the parsed AST tree, returns violations
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Violations from on_success callback, or syntax error violations
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
def _check_python(self, context, config):
|
|
246
|
+
return with_parsed_python(
|
|
247
|
+
context,
|
|
248
|
+
self._violation_builder,
|
|
249
|
+
lambda tree: self._analyze_tree(tree, config, context),
|
|
250
|
+
)
|
|
251
|
+
"""
|
|
252
|
+
tree, errors = parse_python_ast(context, violation_builder)
|
|
253
|
+
if errors:
|
|
254
|
+
return errors
|
|
255
|
+
# tree is guaranteed non-None when errors is empty (parse_python_ast contract)
|
|
256
|
+
assert tree is not None # nosec B101
|
|
257
|
+
return on_success(tree)
|
|
@@ -15,7 +15,7 @@ Exports: AtemporalDetector class with detect_violations method
|
|
|
15
15
|
|
|
16
16
|
Interfaces: detect_violations(text) -> list[tuple[str, str, int]] returns pattern matches with line numbers
|
|
17
17
|
|
|
18
|
-
Implementation: Regex-based pattern matching with
|
|
18
|
+
Implementation: Regex-based pattern matching with pre-compiled patterns organized by category
|
|
19
19
|
|
|
20
20
|
Suppressions:
|
|
21
21
|
- nesting: detect_violations iterates over pattern categories and their patterns.
|
|
@@ -23,46 +23,60 @@ Suppressions:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import re
|
|
26
|
+
from re import Pattern
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compile_patterns(patterns: list[tuple[str, str]]) -> list[tuple[Pattern[str], str]]:
|
|
30
|
+
"""Compile regex patterns for efficient reuse."""
|
|
31
|
+
return [(re.compile(pattern, re.IGNORECASE), desc) for pattern, desc in patterns]
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
class AtemporalDetector:
|
|
29
35
|
"""Detects temporal language patterns in text."""
|
|
30
36
|
|
|
31
|
-
#
|
|
32
|
-
DATE_PATTERNS =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
37
|
+
# Pre-compiled date patterns
|
|
38
|
+
DATE_PATTERNS = _compile_patterns(
|
|
39
|
+
[
|
|
40
|
+
(r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
|
|
41
|
+
(
|
|
42
|
+
r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
|
|
43
|
+
"Month Year format",
|
|
44
|
+
),
|
|
45
|
+
(r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Pre-compiled temporal qualifiers
|
|
50
|
+
TEMPORAL_QUALIFIERS = _compile_patterns(
|
|
51
|
+
[
|
|
52
|
+
(r"\bcurrently\b", 'temporal qualifier "currently"'),
|
|
53
|
+
(r"\bnow\b", 'temporal qualifier "now"'),
|
|
54
|
+
(r"\brecently\b", 'temporal qualifier "recently"'),
|
|
55
|
+
(r"\bsoon\b", 'temporal qualifier "soon"'),
|
|
56
|
+
(r"\bfor now\b", 'temporal qualifier "for now"'),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Pre-compiled state change language
|
|
61
|
+
STATE_CHANGE = _compile_patterns(
|
|
62
|
+
[
|
|
63
|
+
(r"\breplaces?\b", 'state change "replaces"'),
|
|
64
|
+
(r"\bmigrated from\b", 'state change "migrated from"'),
|
|
65
|
+
(r"\bformerly\b", 'state change "formerly"'),
|
|
66
|
+
(r"\bold implementation\b", 'state change "old"'),
|
|
67
|
+
(r"\bnew implementation\b", 'state change "new"'),
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Pre-compiled future references
|
|
72
|
+
FUTURE_REFS = _compile_patterns(
|
|
73
|
+
[
|
|
74
|
+
(r"\bwill be\b", 'future reference "will be"'),
|
|
75
|
+
(r"\bplanned\b", 'future reference "planned"'),
|
|
76
|
+
(r"\bto be added\b", 'future reference "to be added"'),
|
|
77
|
+
(r"\bcoming soon\b", 'future reference "coming soon"'),
|
|
78
|
+
]
|
|
79
|
+
)
|
|
66
80
|
|
|
67
81
|
def detect_violations( # thailint: ignore[nesting]
|
|
68
82
|
self, text: str
|
|
@@ -77,15 +91,15 @@ class AtemporalDetector:
|
|
|
77
91
|
"""
|
|
78
92
|
violations = []
|
|
79
93
|
|
|
80
|
-
# Check all pattern categories
|
|
94
|
+
# Check all pattern categories (patterns are pre-compiled)
|
|
81
95
|
all_patterns = (
|
|
82
96
|
self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
|
|
83
97
|
)
|
|
84
98
|
|
|
85
99
|
lines = text.split("\n")
|
|
86
100
|
for line_num, line in enumerate(lines, start=1):
|
|
87
|
-
for
|
|
88
|
-
if
|
|
89
|
-
violations.append((pattern, description, line_num))
|
|
101
|
+
for compiled_pattern, description in all_patterns:
|
|
102
|
+
if compiled_pattern.search(line):
|
|
103
|
+
violations.append((compiled_pattern.pattern, description, line_num))
|
|
90
104
|
|
|
91
105
|
return violations
|