thailint 0.16.0__py3-none-any.whl → 0.17.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 +8 -1
- src/cli/linters/rust.py +177 -0
- src/core/base.py +30 -0
- src/core/constants.py +1 -0
- src/core/linter_utils.py +42 -1
- src/linters/blocking_async/__init__.py +31 -0
- src/linters/blocking_async/config.py +67 -0
- src/linters/blocking_async/linter.py +183 -0
- src/linters/blocking_async/rust_analyzer.py +419 -0
- src/linters/blocking_async/violation_builder.py +97 -0
- src/linters/clone_abuse/__init__.py +31 -0
- src/linters/clone_abuse/config.py +65 -0
- src/linters/clone_abuse/linter.py +183 -0
- src/linters/clone_abuse/rust_analyzer.py +356 -0
- src/linters/clone_abuse/violation_builder.py +94 -0
- src/linters/magic_numbers/linter.py +92 -0
- src/linters/magic_numbers/rust_analyzer.py +148 -0
- src/linters/magic_numbers/violation_builder.py +31 -0
- src/linters/nesting/linter.py +50 -0
- src/linters/nesting/rust_analyzer.py +118 -0
- src/linters/nesting/violation_builder.py +32 -0
- src/linters/srp/class_analyzer.py +49 -0
- src/linters/srp/linter.py +22 -0
- src/linters/srp/rust_analyzer.py +206 -0
- src/linters/unwrap_abuse/__init__.py +30 -0
- src/linters/unwrap_abuse/config.py +59 -0
- src/linters/unwrap_abuse/linter.py +166 -0
- src/linters/unwrap_abuse/rust_analyzer.py +118 -0
- src/linters/unwrap_abuse/violation_builder.py +89 -0
- src/templates/thailint_config_template.yaml +88 -0
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/METADATA +5 -2
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/RECORD +35 -16
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.16.0.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/__init__.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Purpose: CLI linters package that registers all linter commands to the main CLI group
|
|
3
3
|
|
|
4
|
-
Scope: Export and registration of all linter CLI commands (nesting, srp, dry, magic-numbers,
|
|
4
|
+
Scope: Export and registration of all linter CLI commands (nesting, srp, dry, magic-numbers,
|
|
5
|
+
unwrap-abuse, clone-abuse, blocking-async, etc.)
|
|
5
6
|
|
|
6
7
|
Overview: Package initialization that imports all linter command modules to trigger their registration
|
|
7
8
|
with the main CLI group via Click decorators. Each submodule defines commands using @cli.command()
|
|
@@ -29,6 +30,7 @@ from src.cli.linters import ( # noqa: F401
|
|
|
29
30
|
code_smells,
|
|
30
31
|
documentation,
|
|
31
32
|
performance,
|
|
33
|
+
rust,
|
|
32
34
|
structure,
|
|
33
35
|
structure_quality,
|
|
34
36
|
)
|
|
@@ -43,6 +45,7 @@ from src.cli.linters.code_patterns import (
|
|
|
43
45
|
from src.cli.linters.code_smells import dry, magic_numbers
|
|
44
46
|
from src.cli.linters.documentation import file_header
|
|
45
47
|
from src.cli.linters.performance import perf, regex_in_loop, string_concat_loop
|
|
48
|
+
from src.cli.linters.rust import blocking_async, clone_abuse, unwrap_abuse
|
|
46
49
|
from src.cli.linters.structure import file_placement, pipeline
|
|
47
50
|
from src.cli.linters.structure_quality import nesting, srp
|
|
48
51
|
|
|
@@ -67,4 +70,8 @@ __all__ = [
|
|
|
67
70
|
"perf",
|
|
68
71
|
"string_concat_loop",
|
|
69
72
|
"regex_in_loop",
|
|
73
|
+
# Rust commands
|
|
74
|
+
"unwrap_abuse",
|
|
75
|
+
"clone_abuse",
|
|
76
|
+
"blocking_async",
|
|
70
77
|
]
|
src/cli/linters/rust.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI commands for Rust-specific linters (unwrap-abuse, clone-abuse, blocking-async)
|
|
3
|
+
|
|
4
|
+
Scope: Commands that detect Rust-specific anti-patterns and code smells
|
|
5
|
+
|
|
6
|
+
Overview: Provides CLI commands for Rust code linting: unwrap-abuse detects .unwrap() and
|
|
7
|
+
.expect() calls that may panic at runtime and suggests safer alternatives; clone-abuse
|
|
8
|
+
detects .clone() abuse patterns including clone in loops, chained clones, and unnecessary
|
|
9
|
+
clones; blocking-async detects blocking operations (std::fs, std::thread::sleep, std::net)
|
|
10
|
+
inside async functions and suggests tokio equivalents. Each command supports standard
|
|
11
|
+
options (config, format, recursive) and integrates with the orchestrator for execution.
|
|
12
|
+
|
|
13
|
+
Dependencies: click for CLI framework, src.cli.main for CLI group, src.cli.utils for shared utilities
|
|
14
|
+
|
|
15
|
+
Exports: unwrap_abuse command, clone_abuse command, blocking_async 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
|
+
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
25
|
+
|
|
26
|
+
from loguru import logger
|
|
27
|
+
|
|
28
|
+
from src.cli.linters.shared import ExecuteParams, create_linter_command
|
|
29
|
+
from src.cli.utils import execute_linting_on_paths, setup_base_orchestrator, validate_paths_exist
|
|
30
|
+
from src.core.cli_utils import format_violations
|
|
31
|
+
from src.core.types import Violation
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from src.orchestrator.core import Orchestrator
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Unwrap Abuse Command
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _setup_unwrap_abuse_orchestrator(
|
|
43
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
44
|
+
) -> "Orchestrator":
|
|
45
|
+
"""Set up orchestrator for unwrap-abuse command."""
|
|
46
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run_unwrap_abuse_lint(
|
|
50
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
51
|
+
) -> list[Violation]:
|
|
52
|
+
"""Execute unwrap-abuse lint on files or directories."""
|
|
53
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
54
|
+
return [v for v in all_violations if v.rule_id.startswith("unwrap-abuse")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _execute_unwrap_abuse_lint(params: ExecuteParams) -> NoReturn:
|
|
58
|
+
"""Execute unwrap-abuse lint."""
|
|
59
|
+
validate_paths_exist(params.path_objs)
|
|
60
|
+
orchestrator = _setup_unwrap_abuse_orchestrator(
|
|
61
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
62
|
+
)
|
|
63
|
+
unwrap_abuse_violations = _run_unwrap_abuse_lint(
|
|
64
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
logger.debug(f"Found {len(unwrap_abuse_violations)} unwrap abuse violation(s)")
|
|
68
|
+
|
|
69
|
+
format_violations(unwrap_abuse_violations, params.format)
|
|
70
|
+
sys.exit(1 if unwrap_abuse_violations else 0)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
unwrap_abuse = create_linter_command(
|
|
74
|
+
"unwrap-abuse",
|
|
75
|
+
_execute_unwrap_abuse_lint,
|
|
76
|
+
"Check for .unwrap() and .expect() abuse in Rust code.",
|
|
77
|
+
"Detects .unwrap() and .expect() calls in Rust code that may panic at runtime.\n"
|
|
78
|
+
" Suggests safer alternatives like the ? operator, unwrap_or(),\n"
|
|
79
|
+
" unwrap_or_default(), or match/if-let expressions.\n"
|
|
80
|
+
" Ignores calls in #[test] functions and #[cfg(test)] modules by default.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Clone Abuse Command
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _setup_clone_abuse_orchestrator(
|
|
90
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
91
|
+
) -> "Orchestrator":
|
|
92
|
+
"""Set up orchestrator for clone-abuse command."""
|
|
93
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_clone_abuse_lint(
|
|
97
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
98
|
+
) -> list[Violation]:
|
|
99
|
+
"""Execute clone-abuse lint on files or directories."""
|
|
100
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
101
|
+
return [v for v in all_violations if v.rule_id.startswith("clone-abuse")]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _execute_clone_abuse_lint(params: ExecuteParams) -> NoReturn:
|
|
105
|
+
"""Execute clone-abuse lint."""
|
|
106
|
+
validate_paths_exist(params.path_objs)
|
|
107
|
+
orchestrator = _setup_clone_abuse_orchestrator(
|
|
108
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
109
|
+
)
|
|
110
|
+
clone_abuse_violations = _run_clone_abuse_lint(
|
|
111
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logger.debug(f"Found {len(clone_abuse_violations)} clone abuse violation(s)")
|
|
115
|
+
|
|
116
|
+
format_violations(clone_abuse_violations, params.format)
|
|
117
|
+
sys.exit(1 if clone_abuse_violations else 0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
clone_abuse = create_linter_command(
|
|
121
|
+
"clone-abuse",
|
|
122
|
+
_execute_clone_abuse_lint,
|
|
123
|
+
"Check for .clone() abuse patterns in Rust code.",
|
|
124
|
+
"Detects .clone() abuse patterns in Rust code: clone in loops,\n"
|
|
125
|
+
" chained .clone().clone() calls, and unnecessary clones where\n"
|
|
126
|
+
" the original is not used after cloning.\n"
|
|
127
|
+
" Suggests borrowing, Rc/Arc, or Cow patterns as alternatives.\n"
|
|
128
|
+
" Ignores calls in #[test] functions and #[cfg(test)] modules by default.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# Blocking Async Command
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _setup_blocking_async_orchestrator(
|
|
138
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
139
|
+
) -> "Orchestrator":
|
|
140
|
+
"""Set up orchestrator for blocking-async command."""
|
|
141
|
+
return setup_base_orchestrator(path_objs, config_file, verbose, project_root)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _run_blocking_async_lint(
|
|
145
|
+
orchestrator: "Orchestrator", path_objs: list[Path], recursive: bool, parallel: bool = False
|
|
146
|
+
) -> list[Violation]:
|
|
147
|
+
"""Execute blocking-async lint on files or directories."""
|
|
148
|
+
all_violations = execute_linting_on_paths(orchestrator, path_objs, recursive, parallel)
|
|
149
|
+
return [v for v in all_violations if v.rule_id.startswith("blocking-async")]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _execute_blocking_async_lint(params: ExecuteParams) -> NoReturn:
|
|
153
|
+
"""Execute blocking-async lint."""
|
|
154
|
+
validate_paths_exist(params.path_objs)
|
|
155
|
+
orchestrator = _setup_blocking_async_orchestrator(
|
|
156
|
+
params.path_objs, params.config_file, params.verbose, params.project_root
|
|
157
|
+
)
|
|
158
|
+
blocking_async_violations = _run_blocking_async_lint(
|
|
159
|
+
orchestrator, params.path_objs, params.recursive, params.parallel
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logger.debug(f"Found {len(blocking_async_violations)} blocking-async violation(s)")
|
|
163
|
+
|
|
164
|
+
format_violations(blocking_async_violations, params.format)
|
|
165
|
+
sys.exit(1 if blocking_async_violations else 0)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
blocking_async = create_linter_command(
|
|
169
|
+
"blocking-async",
|
|
170
|
+
_execute_blocking_async_lint,
|
|
171
|
+
"Check for blocking operations in async Rust functions.",
|
|
172
|
+
"Detects blocking operations inside async functions in Rust code:\n"
|
|
173
|
+
" std::fs I/O, std::thread::sleep, and blocking std::net calls.\n"
|
|
174
|
+
" Suggests async-compatible alternatives like tokio::fs,\n"
|
|
175
|
+
" tokio::time::sleep, and tokio::net equivalents.\n"
|
|
176
|
+
" Ignores calls in #[test] functions and #[cfg(test)] modules by default.",
|
|
177
|
+
)
|
src/core/base.py
CHANGED
|
@@ -177,12 +177,27 @@ class MultiLanguageLintRule(BaseLintRule):
|
|
|
177
177
|
if not config.enabled:
|
|
178
178
|
return []
|
|
179
179
|
|
|
180
|
+
return self._dispatch_by_language(context, config)
|
|
181
|
+
|
|
182
|
+
def _dispatch_by_language(self, context: BaseLintContext, config: Any) -> list[Violation]:
|
|
183
|
+
"""Dispatch to language-specific check method.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
context: Lint context with language information
|
|
187
|
+
config: Loaded configuration
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of violations from language-specific checker
|
|
191
|
+
"""
|
|
180
192
|
if context.language == Language.PYTHON:
|
|
181
193
|
return self._check_python(context, config)
|
|
182
194
|
|
|
183
195
|
if context.language in (Language.TYPESCRIPT, Language.JAVASCRIPT):
|
|
184
196
|
return self._check_typescript(context, config)
|
|
185
197
|
|
|
198
|
+
if context.language == Language.RUST:
|
|
199
|
+
return self._check_rust(context, config)
|
|
200
|
+
|
|
186
201
|
return []
|
|
187
202
|
|
|
188
203
|
@abstractmethod
|
|
@@ -222,3 +237,18 @@ class MultiLanguageLintRule(BaseLintRule):
|
|
|
222
237
|
List of violations found in TypeScript/JavaScript code
|
|
223
238
|
"""
|
|
224
239
|
raise NotImplementedError("Subclasses must implement _check_typescript")
|
|
240
|
+
|
|
241
|
+
def _check_rust(self, context: BaseLintContext, config: Any) -> list[Violation]:
|
|
242
|
+
"""Check Rust code for violations.
|
|
243
|
+
|
|
244
|
+
Override in subclasses to add Rust language support.
|
|
245
|
+
Returns empty list by default for linters that do not support Rust.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
context: Lint context with Rust file information
|
|
249
|
+
config: Loaded configuration
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of violations found in Rust code
|
|
253
|
+
"""
|
|
254
|
+
return []
|
src/core/constants.py
CHANGED
src/core/linter_utils.py
CHANGED
|
@@ -13,7 +13,7 @@ Overview: Provides reusable helper functions to eliminate duplication across lin
|
|
|
13
13
|
Dependencies: BaseLintContext from src.core.base, ast for Python parsing
|
|
14
14
|
|
|
15
15
|
Exports: get_metadata, get_metadata_value, load_linter_config, has_file_content, parse_python_ast,
|
|
16
|
-
with_parsed_python
|
|
16
|
+
with_parsed_python, resolve_file_path, is_ignored_path, get_line_context
|
|
17
17
|
|
|
18
18
|
Interfaces: All functions take BaseLintContext and return typed values (dict, str, bool, Any)
|
|
19
19
|
|
|
@@ -255,3 +255,44 @@ def with_parsed_python(
|
|
|
255
255
|
# tree is guaranteed non-None when errors is empty (parse_python_ast contract)
|
|
256
256
|
assert tree is not None # nosec B101
|
|
257
257
|
return on_success(tree)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def resolve_file_path(context: BaseLintContext) -> str:
|
|
261
|
+
"""Resolve file path from context.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
context: Lint context
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
File path string, or "unknown" if not available
|
|
268
|
+
"""
|
|
269
|
+
return str(context.file_path) if context.file_path else "unknown"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def is_ignored_path(file_path: str, ignore_patterns: list[str]) -> bool:
|
|
273
|
+
"""Check if file path matches any ignore pattern.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
file_path: Path to check
|
|
277
|
+
ignore_patterns: List of patterns to match against
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if the path should be ignored
|
|
281
|
+
"""
|
|
282
|
+
return any(ignored in file_path for ignored in ignore_patterns)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_line_context(code: str, line_index: int) -> str:
|
|
286
|
+
"""Get the code line at the given index for context.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
code: Full source code
|
|
290
|
+
line_index: Zero-indexed line number
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Stripped line content, or empty string if index out of range
|
|
294
|
+
"""
|
|
295
|
+
lines = code.split("\n")
|
|
296
|
+
if 0 <= line_index < len(lines):
|
|
297
|
+
return lines[line_index].strip()
|
|
298
|
+
return ""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust blocking-in-async detector package exports
|
|
3
|
+
|
|
4
|
+
Scope: Detect blocking operations inside async functions in Rust code and suggest alternatives
|
|
5
|
+
|
|
6
|
+
Overview: Package providing blocking-in-async detection for Rust code. Identifies std::fs I/O
|
|
7
|
+
operations, std::thread::sleep calls, and blocking std::net operations inside async functions.
|
|
8
|
+
Suggests async-compatible alternatives including tokio::fs, tokio::time::sleep, and tokio::net
|
|
9
|
+
equivalents. Supports configuration for allowing calls in test code, toggling individual
|
|
10
|
+
pattern detection, and ignoring specific directories. Uses tree-sitter for accurate
|
|
11
|
+
AST-based detection of async function contexts.
|
|
12
|
+
|
|
13
|
+
Dependencies: tree-sitter-rust (optional) for AST parsing, src.core for base classes
|
|
14
|
+
|
|
15
|
+
Exports: BlockingAsyncConfig, BlockingAsyncRule, RustBlockingAsyncAnalyzer, BlockingCall
|
|
16
|
+
|
|
17
|
+
Interfaces: BlockingAsyncConfig.from_dict() for YAML configuration loading
|
|
18
|
+
|
|
19
|
+
Implementation: Tree-sitter AST-based async context detection with blocking API pattern matching
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import BlockingAsyncConfig
|
|
23
|
+
from .linter import BlockingAsyncRule
|
|
24
|
+
from .rust_analyzer import BlockingCall, RustBlockingAsyncAnalyzer
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BlockingAsyncConfig",
|
|
28
|
+
"BlockingAsyncRule",
|
|
29
|
+
"RustBlockingAsyncAnalyzer",
|
|
30
|
+
"BlockingCall",
|
|
31
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for Rust blocking-in-async detector
|
|
3
|
+
|
|
4
|
+
Scope: Pattern toggles, ignore patterns, and configuration for blocking-in-async detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides BlockingAsyncConfig dataclass with toggles for controlling detection of
|
|
7
|
+
blocking operations inside async functions in Rust code. Supports toggling detection of
|
|
8
|
+
std::fs operations, std::thread::sleep, and blocking network calls independently. Includes
|
|
9
|
+
configuration for allowing calls in test code, ignoring example and benchmark directories.
|
|
10
|
+
Configuration loads from YAML with sensible defaults via from_dict() class method.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: BlockingAsyncConfig
|
|
15
|
+
|
|
16
|
+
Interfaces: BlockingAsyncConfig.from_dict() for YAML configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with factory defaults and conservative default settings
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BlockingAsyncConfig:
|
|
27
|
+
"""Configuration for blocking-in-async detection."""
|
|
28
|
+
|
|
29
|
+
enabled: bool = True
|
|
30
|
+
|
|
31
|
+
# Allow blocking calls in test functions and #[cfg(test)] modules
|
|
32
|
+
allow_in_tests: bool = True
|
|
33
|
+
|
|
34
|
+
# Toggle detection of std::fs operations in async functions
|
|
35
|
+
detect_fs_in_async: bool = True
|
|
36
|
+
|
|
37
|
+
# Toggle detection of std::thread::sleep in async functions
|
|
38
|
+
detect_sleep_in_async: bool = True
|
|
39
|
+
|
|
40
|
+
# Toggle detection of std::net blocking calls in async functions
|
|
41
|
+
detect_net_in_async: bool = True
|
|
42
|
+
|
|
43
|
+
# File path patterns to ignore (e.g., examples/, benches/)
|
|
44
|
+
ignore: list[str] = field(default_factory=lambda: ["examples/", "benches/", "tests/"])
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(
|
|
48
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
49
|
+
) -> "BlockingAsyncConfig":
|
|
50
|
+
"""Load configuration from dictionary.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Configuration dictionary from YAML
|
|
54
|
+
language: Language parameter (reserved for future use)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Configured BlockingAsyncConfig instance
|
|
58
|
+
"""
|
|
59
|
+
_ = language
|
|
60
|
+
return cls(
|
|
61
|
+
enabled=config.get("enabled", True),
|
|
62
|
+
allow_in_tests=config.get("allow_in_tests", True),
|
|
63
|
+
detect_fs_in_async=config.get("detect_fs_in_async", True),
|
|
64
|
+
detect_sleep_in_async=config.get("detect_sleep_in_async", True),
|
|
65
|
+
detect_net_in_async=config.get("detect_net_in_async", True),
|
|
66
|
+
ignore=config.get("ignore", ["examples/", "benches/", "tests/"]),
|
|
67
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Main linter rule for detecting blocking operations in async Rust code
|
|
3
|
+
|
|
4
|
+
Scope: Entry point for blocking-in-async detection implementing BaseLintRule interface
|
|
5
|
+
|
|
6
|
+
Overview: Provides BlockingAsyncRule class that implements the BaseLintRule interface for
|
|
7
|
+
detecting blocking API calls inside async functions in Rust code. Validates that files
|
|
8
|
+
are Rust with content, loads configuration, checks ignored paths, and delegates analysis
|
|
9
|
+
to RustBlockingAsyncAnalyzer. Filters detected calls based on configuration (allow_in_tests,
|
|
10
|
+
pattern toggles, ignored paths) and converts remaining calls to Violation objects via the
|
|
11
|
+
violation builder. Supports disabling via configuration.
|
|
12
|
+
|
|
13
|
+
Dependencies: BaseLintRule, RustBlockingAsyncAnalyzer, BlockingAsyncConfig, violation_builder
|
|
14
|
+
|
|
15
|
+
Exports: BlockingAsyncRule
|
|
16
|
+
|
|
17
|
+
Interfaces: check(context: BaseLintContext) -> list[Violation]
|
|
18
|
+
|
|
19
|
+
Implementation: Single-file analysis with config-driven filtering and tree-sitter-based detection
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from src.core.base import BaseLintContext, BaseLintRule
|
|
23
|
+
from src.core.linter_utils import (
|
|
24
|
+
has_file_content,
|
|
25
|
+
is_ignored_path,
|
|
26
|
+
load_linter_config,
|
|
27
|
+
resolve_file_path,
|
|
28
|
+
)
|
|
29
|
+
from src.core.types import Violation
|
|
30
|
+
|
|
31
|
+
from .config import BlockingAsyncConfig
|
|
32
|
+
from .rust_analyzer import BlockingCall, RustBlockingAsyncAnalyzer
|
|
33
|
+
from .violation_builder import (
|
|
34
|
+
build_fs_in_async_violation,
|
|
35
|
+
build_net_in_async_violation,
|
|
36
|
+
build_sleep_in_async_violation,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_PATTERN_BUILDERS = {
|
|
40
|
+
"fs-in-async": build_fs_in_async_violation,
|
|
41
|
+
"sleep-in-async": build_sleep_in_async_violation,
|
|
42
|
+
"net-in-async": build_net_in_async_violation,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_PATTERN_CONFIG_KEYS = {
|
|
46
|
+
"fs-in-async": "detect_fs_in_async",
|
|
47
|
+
"sleep-in-async": "detect_sleep_in_async",
|
|
48
|
+
"net-in-async": "detect_net_in_async",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BlockingAsyncRule(BaseLintRule):
|
|
53
|
+
"""Detects blocking operations inside async functions in Rust code."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: BlockingAsyncConfig | None = None) -> None:
|
|
56
|
+
"""Initialize blocking-in-async rule.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config: Optional configuration override for testing
|
|
60
|
+
"""
|
|
61
|
+
self._config_override = config
|
|
62
|
+
self._analyzer = RustBlockingAsyncAnalyzer()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def rule_id(self) -> str:
|
|
66
|
+
"""Unique identifier for this rule."""
|
|
67
|
+
return "blocking-async"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def rule_name(self) -> str:
|
|
71
|
+
"""Human-readable name for this rule."""
|
|
72
|
+
return "Rust Blocking in Async"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
"""Description of what this rule checks."""
|
|
77
|
+
return (
|
|
78
|
+
"Detects blocking operations inside async functions in Rust code including "
|
|
79
|
+
"std::fs I/O, std::thread::sleep, and blocking std::net calls. Suggests "
|
|
80
|
+
"async-compatible alternatives like tokio::fs, tokio::time::sleep, and tokio::net."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
84
|
+
"""Check for blocking-in-async violations in a Rust file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
context: Lint context with file content and metadata
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of violations found
|
|
91
|
+
"""
|
|
92
|
+
config = self._get_config(context)
|
|
93
|
+
if not self._should_analyze(context, config):
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
file_path = resolve_file_path(context)
|
|
97
|
+
calls = self._analyzer.find_blocking_calls(context.file_content or "")
|
|
98
|
+
return self._build_violations(calls, config, file_path)
|
|
99
|
+
|
|
100
|
+
def _should_analyze(self, context: BaseLintContext, config: BlockingAsyncConfig) -> bool:
|
|
101
|
+
"""Determine if the file should be analyzed.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
context: Lint context to check
|
|
105
|
+
config: Blocking-in-async configuration
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if the file should be analyzed
|
|
109
|
+
"""
|
|
110
|
+
if context.language != "rust":
|
|
111
|
+
return False
|
|
112
|
+
if not has_file_content(context):
|
|
113
|
+
return False
|
|
114
|
+
if not config.enabled:
|
|
115
|
+
return False
|
|
116
|
+
return not is_ignored_path(resolve_file_path(context), config.ignore)
|
|
117
|
+
|
|
118
|
+
def _get_config(self, context: BaseLintContext) -> BlockingAsyncConfig:
|
|
119
|
+
"""Load configuration from override or context metadata.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
context: Lint context with metadata
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
BlockingAsyncConfig instance
|
|
126
|
+
"""
|
|
127
|
+
if self._config_override is not None:
|
|
128
|
+
return self._config_override
|
|
129
|
+
return load_linter_config(context, "blocking-async", BlockingAsyncConfig)
|
|
130
|
+
|
|
131
|
+
def _build_violations(
|
|
132
|
+
self,
|
|
133
|
+
calls: list[BlockingCall],
|
|
134
|
+
config: BlockingAsyncConfig,
|
|
135
|
+
file_path: str,
|
|
136
|
+
) -> list[Violation]:
|
|
137
|
+
"""Convert blocking calls to violations with config filtering.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
calls: Detected blocking calls from analyzer
|
|
141
|
+
config: Configuration for filtering
|
|
142
|
+
file_path: Path of the analyzed file
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of filtered violations
|
|
146
|
+
"""
|
|
147
|
+
return [
|
|
148
|
+
_build_violation_for_call(call, file_path)
|
|
149
|
+
for call in calls
|
|
150
|
+
if not _should_skip_call(call, config)
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _should_skip_call(call: BlockingCall, config: BlockingAsyncConfig) -> bool:
|
|
155
|
+
"""Determine if a blocking call should be skipped based on config.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
call: Detected blocking call
|
|
159
|
+
config: Configuration for filtering
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if the call should be skipped
|
|
163
|
+
"""
|
|
164
|
+
if call.is_in_test and config.allow_in_tests:
|
|
165
|
+
return True
|
|
166
|
+
config_key = _PATTERN_CONFIG_KEYS.get(call.pattern)
|
|
167
|
+
if config_key and not getattr(config, config_key):
|
|
168
|
+
return True
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_violation_for_call(call: BlockingCall, file_path: str) -> Violation:
|
|
173
|
+
"""Build a violation for a specific blocking call.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
call: Detected blocking call with pattern info
|
|
177
|
+
file_path: Path of the analyzed file
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Violation instance
|
|
181
|
+
"""
|
|
182
|
+
builder = _PATTERN_BUILDERS.get(call.pattern, build_fs_in_async_violation)
|
|
183
|
+
return builder(file_path, call.line, call.column, call.context)
|