thailint 0.15.8__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/config.py +4 -12
- src/cli/linters/__init__.py +13 -3
- src/cli/linters/code_patterns.py +42 -38
- src/cli/linters/code_smells.py +8 -17
- src/cli/linters/documentation.py +3 -6
- src/cli/linters/performance.py +4 -10
- src/cli/linters/rust.py +177 -0
- src/cli/linters/shared.py +2 -7
- src/cli/linters/structure.py +4 -11
- src/cli/linters/structure_quality.py +4 -11
- src/cli/main.py +9 -12
- src/cli/utils.py +7 -16
- src/core/__init__.py +14 -0
- src/core/base.py +30 -0
- src/core/constants.py +1 -0
- src/core/linter_utils.py +42 -1
- src/core/rule_aliases.py +84 -0
- src/linter_config/rule_matcher.py +53 -8
- 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/print_statements/__init__.py +23 -11
- src/linters/print_statements/conditional_verbose_analyzer.py +200 -0
- src/linters/print_statements/conditional_verbose_rule.py +254 -0
- src/linters/print_statements/linter.py +2 -2
- 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.15.8.dist-info → thailint-0.17.0.dist-info}/METADATA +7 -3
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/RECORD +52 -30
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
- {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
src/cli/linters/structure.py
CHANGED
|
@@ -25,12 +25,12 @@ Suppressions:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
|
-
import logging
|
|
29
28
|
import sys
|
|
30
29
|
from pathlib import Path
|
|
31
30
|
from typing import TYPE_CHECKING, Any, NoReturn
|
|
32
31
|
|
|
33
32
|
import click
|
|
33
|
+
from loguru import logger
|
|
34
34
|
|
|
35
35
|
from src.cli.linters.shared import (
|
|
36
36
|
ensure_config_section,
|
|
@@ -54,10 +54,6 @@ from src.core.types import Violation
|
|
|
54
54
|
if TYPE_CHECKING:
|
|
55
55
|
from src.orchestrator.core import Orchestrator
|
|
56
56
|
|
|
57
|
-
# Configure module logger
|
|
58
|
-
logger = logging.getLogger(__name__)
|
|
59
|
-
|
|
60
|
-
|
|
61
57
|
# =============================================================================
|
|
62
58
|
# File Placement Command
|
|
63
59
|
# =============================================================================
|
|
@@ -93,8 +89,7 @@ def _apply_inline_rules(orchestrator: "Orchestrator", rules: str, verbose: bool)
|
|
|
93
89
|
"""Parse and apply inline JSON rules."""
|
|
94
90
|
rules_config = _parse_json_rules(rules)
|
|
95
91
|
orchestrator.config.update(rules_config)
|
|
96
|
-
|
|
97
|
-
logger.debug(f"Applied inline rules: {rules_config}")
|
|
92
|
+
logger.debug(f"Applied inline rules: {rules_config}")
|
|
98
93
|
|
|
99
94
|
|
|
100
95
|
def _parse_json_rules(rules: str) -> dict[str, Any]:
|
|
@@ -195,8 +190,7 @@ def _execute_file_placement_lint( # pylint: disable=too-many-arguments,too-many
|
|
|
195
190
|
# Filter to only file-placement violations
|
|
196
191
|
violations = [v for v in all_violations if v.rule_id.startswith("file-placement")]
|
|
197
192
|
|
|
198
|
-
|
|
199
|
-
logger.info(f"Found {len(violations)} violation(s)")
|
|
193
|
+
logger.debug(f"Found {len(violations)} violation(s)")
|
|
200
194
|
|
|
201
195
|
format_violations(violations, format)
|
|
202
196
|
sys.exit(1 if violations else 0)
|
|
@@ -320,8 +314,7 @@ def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-posit
|
|
|
320
314
|
_apply_pipeline_config_override(orchestrator, min_continues, verbose)
|
|
321
315
|
pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive, parallel)
|
|
322
316
|
|
|
323
|
-
|
|
324
|
-
logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
|
|
317
|
+
logger.debug(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
|
|
325
318
|
|
|
326
319
|
format_violations(pipeline_violations, format)
|
|
327
320
|
sys.exit(1 if pipeline_violations else 0)
|
|
@@ -24,13 +24,13 @@ Suppressions:
|
|
|
24
24
|
- too-many-arguments,too-many-positional-arguments: Click commands require many parameters by framework design
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
import logging
|
|
28
27
|
import sys
|
|
29
28
|
from contextlib import suppress
|
|
30
29
|
from pathlib import Path
|
|
31
30
|
from typing import TYPE_CHECKING, NoReturn
|
|
32
31
|
|
|
33
32
|
import click
|
|
33
|
+
from loguru import logger
|
|
34
34
|
|
|
35
35
|
from src.cli.linters.shared import (
|
|
36
36
|
ensure_config_section,
|
|
@@ -52,10 +52,6 @@ from src.core.types import Violation
|
|
|
52
52
|
if TYPE_CHECKING:
|
|
53
53
|
from src.orchestrator.core import Orchestrator
|
|
54
54
|
|
|
55
|
-
# Configure module logger
|
|
56
|
-
logger = logging.getLogger(__name__)
|
|
57
|
-
|
|
58
|
-
|
|
59
55
|
# =============================================================================
|
|
60
56
|
# Nesting Command
|
|
61
57
|
# =============================================================================
|
|
@@ -79,8 +75,7 @@ def _apply_nesting_config_override(
|
|
|
79
75
|
nesting_config["max_nesting_depth"] = max_depth
|
|
80
76
|
_apply_nesting_to_languages(nesting_config, max_depth)
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
78
|
+
logger.debug(f"Overriding max_nesting_depth to {max_depth}")
|
|
84
79
|
|
|
85
80
|
|
|
86
81
|
def _apply_nesting_to_languages(nesting_config: dict, max_depth: int) -> None:
|
|
@@ -189,8 +184,7 @@ def _execute_nesting_lint( # pylint: disable=too-many-arguments,too-many-positi
|
|
|
189
184
|
_apply_nesting_config_override(orchestrator, max_depth, verbose)
|
|
190
185
|
nesting_violations = _run_nesting_lint(orchestrator, path_objs, recursive, parallel)
|
|
191
186
|
|
|
192
|
-
|
|
193
|
-
logger.info(f"Found {len(nesting_violations)} nesting violation(s)")
|
|
187
|
+
logger.debug(f"Found {len(nesting_violations)} nesting violation(s)")
|
|
194
188
|
|
|
195
189
|
format_violations(nesting_violations, format)
|
|
196
190
|
sys.exit(1 if nesting_violations else 0)
|
|
@@ -321,8 +315,7 @@ def _execute_srp_lint( # pylint: disable=too-many-arguments,too-many-positional
|
|
|
321
315
|
_apply_srp_config_override(orchestrator, max_methods, max_loc, verbose)
|
|
322
316
|
srp_violations = _run_srp_lint(orchestrator, path_objs, recursive, parallel)
|
|
323
317
|
|
|
324
|
-
|
|
325
|
-
logger.info(f"Found {len(srp_violations)} SRP violation(s)")
|
|
318
|
+
logger.debug(f"Found {len(srp_violations)} SRP violation(s)")
|
|
326
319
|
|
|
327
320
|
format_violations(srp_violations, format)
|
|
328
321
|
sys.exit(1 if srp_violations else 0)
|
src/cli/main.py
CHANGED
|
@@ -21,32 +21,29 @@ Implementation: Uses Click decorators for group definition, stores parsed option
|
|
|
21
21
|
in test environments.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
import logging
|
|
25
24
|
import sys
|
|
26
25
|
from pathlib import Path
|
|
27
26
|
|
|
28
27
|
import click
|
|
28
|
+
from loguru import logger
|
|
29
29
|
|
|
30
30
|
from src import __version__
|
|
31
31
|
from src.config import ConfigError, load_config
|
|
32
32
|
|
|
33
|
-
# Configure module logger
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
33
|
|
|
37
34
|
def setup_logging(verbose: bool = False) -> None:
|
|
38
|
-
"""Configure
|
|
35
|
+
"""Configure loguru for the CLI application.
|
|
39
36
|
|
|
40
37
|
Args:
|
|
41
|
-
verbose: Enable DEBUG level logging if True,
|
|
38
|
+
verbose: Enable DEBUG level logging if True, WARNING otherwise.
|
|
42
39
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
logger.remove() # Remove default handler
|
|
41
|
+
level = "DEBUG" if verbose else "WARNING"
|
|
42
|
+
logger.add(
|
|
43
|
+
sys.stderr,
|
|
46
44
|
level=level,
|
|
47
|
-
format="
|
|
48
|
-
|
|
49
|
-
stream=sys.stdout,
|
|
45
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level:<8}</level> | <level>{message}</level>",
|
|
46
|
+
colorize=True,
|
|
50
47
|
)
|
|
51
48
|
|
|
52
49
|
|
src/cli/utils.py
CHANGED
|
@@ -21,7 +21,6 @@ Implementation: Uses Click decorators for option definitions, deferred imports f
|
|
|
21
21
|
to support test environments, caches project root in context for efficiency
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
import logging
|
|
25
24
|
import sys
|
|
26
25
|
from collections.abc import Callable
|
|
27
26
|
from contextlib import suppress
|
|
@@ -29,13 +28,11 @@ from pathlib import Path
|
|
|
29
28
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
30
29
|
|
|
31
30
|
import click
|
|
31
|
+
from loguru import logger
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from src.orchestrator.core import Orchestrator
|
|
35
35
|
|
|
36
|
-
# Configure module logger
|
|
37
|
-
logger = logging.getLogger(__name__)
|
|
38
|
-
|
|
39
36
|
|
|
40
37
|
# =============================================================================
|
|
41
38
|
# Common Option Decorators
|
|
@@ -132,8 +129,7 @@ def _resolve_explicit_project_root(explicit_root: str, verbose: bool) -> Path:
|
|
|
132
129
|
# Now resolve after validation
|
|
133
130
|
root = root.resolve()
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
logger.debug(f"Using explicit project root: {root}")
|
|
132
|
+
logger.debug(f"Using explicit project root: {root}")
|
|
137
133
|
return root
|
|
138
134
|
|
|
139
135
|
|
|
@@ -150,8 +146,7 @@ def _infer_root_from_config(config_path: str, verbose: bool) -> Path:
|
|
|
150
146
|
config_file = Path(config_path).resolve()
|
|
151
147
|
inferred_root = config_file.parent
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
logger.debug(f"Inferred project root from config path: {inferred_root}")
|
|
149
|
+
logger.debug(f"Inferred project root from config path: {inferred_root}")
|
|
155
150
|
return inferred_root
|
|
156
151
|
|
|
157
152
|
|
|
@@ -168,8 +163,7 @@ def _autodetect_project_root(
|
|
|
168
163
|
Auto-detected project root
|
|
169
164
|
"""
|
|
170
165
|
auto_root = get_project_root(None)
|
|
171
|
-
|
|
172
|
-
logger.debug(f"Auto-detected project root: {auto_root}")
|
|
166
|
+
logger.debug(f"Auto-detected project root: {auto_root}")
|
|
173
167
|
return auto_root
|
|
174
168
|
|
|
175
169
|
|
|
@@ -217,8 +211,7 @@ def _determine_project_root_for_context(ctx: click.Context) -> Path | None:
|
|
|
217
211
|
return _infer_root_from_config(config_path, verbose)
|
|
218
212
|
|
|
219
213
|
# No explicit root - return None for auto-detection from target paths
|
|
220
|
-
|
|
221
|
-
logger.debug("No explicit project root, will auto-detect from target paths")
|
|
214
|
+
logger.debug("No explicit project root, will auto-detect from target paths")
|
|
222
215
|
return None
|
|
223
216
|
|
|
224
217
|
|
|
@@ -262,8 +255,7 @@ def handle_linting_error(error: Exception, verbose: bool) -> None:
|
|
|
262
255
|
verbose: Whether verbose logging is enabled
|
|
263
256
|
"""
|
|
264
257
|
click.echo(f"Error during linting: {error}", err=True)
|
|
265
|
-
|
|
266
|
-
logger.exception("Linting failed with exception")
|
|
258
|
+
logger.exception("Linting failed with exception")
|
|
267
259
|
sys.exit(2)
|
|
268
260
|
|
|
269
261
|
|
|
@@ -334,8 +326,7 @@ def load_config_file(orchestrator: "Orchestrator", config_file: str, verbose: bo
|
|
|
334
326
|
# Load config into orchestrator
|
|
335
327
|
orchestrator.config = orchestrator.config_loader.load(config_path)
|
|
336
328
|
|
|
337
|
-
|
|
338
|
-
logger.debug(f"Loaded config from: {config_file}")
|
|
329
|
+
logger.debug(f"Loaded config from: {config_file}")
|
|
339
330
|
|
|
340
331
|
|
|
341
332
|
# =============================================================================
|
src/core/__init__.py
CHANGED
|
@@ -6,12 +6,26 @@ power the plugin architecture.
|
|
|
6
6
|
|
|
7
7
|
from .base import BaseLintContext, BaseLintRule
|
|
8
8
|
from .registry import RuleRegistry
|
|
9
|
+
from .rule_aliases import (
|
|
10
|
+
LINTER_ALIASES,
|
|
11
|
+
RULE_ID_ALIASES,
|
|
12
|
+
is_deprecated_linter,
|
|
13
|
+
is_deprecated_rule_id,
|
|
14
|
+
resolve_linter_name,
|
|
15
|
+
resolve_rule_id,
|
|
16
|
+
)
|
|
9
17
|
from .types import Severity, Violation
|
|
10
18
|
|
|
11
19
|
__all__ = [
|
|
12
20
|
"BaseLintContext",
|
|
13
21
|
"BaseLintRule",
|
|
22
|
+
"LINTER_ALIASES",
|
|
23
|
+
"RULE_ID_ALIASES",
|
|
14
24
|
"RuleRegistry",
|
|
15
25
|
"Severity",
|
|
16
26
|
"Violation",
|
|
27
|
+
"is_deprecated_linter",
|
|
28
|
+
"is_deprecated_rule_id",
|
|
29
|
+
"resolve_linter_name",
|
|
30
|
+
"resolve_rule_id",
|
|
17
31
|
]
|
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 ""
|
src/core/rule_aliases.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rule ID aliasing system for backward compatibility during rule renaming
|
|
3
|
+
|
|
4
|
+
Scope: Maps deprecated rule IDs to canonical rule IDs for configuration and filtering
|
|
5
|
+
|
|
6
|
+
Overview: Provides a mapping system for rule IDs that allows backward compatibility when rules
|
|
7
|
+
are renamed. Users can continue using deprecated rule IDs in configuration files and ignore
|
|
8
|
+
directives, which are transparently resolved to their canonical forms. Supports both
|
|
9
|
+
direct rule ID mapping and linter-level command aliasing. Used by configuration parsing,
|
|
10
|
+
violation filtering, and ignore directive processing.
|
|
11
|
+
|
|
12
|
+
Dependencies: None (pure Python module)
|
|
13
|
+
|
|
14
|
+
Exports: RULE_ID_ALIASES dict, LINTER_ALIASES dict, resolve_rule_id function,
|
|
15
|
+
resolve_linter_name function
|
|
16
|
+
|
|
17
|
+
Interfaces: resolve_rule_id(rule_id) -> str, resolve_linter_name(name) -> str
|
|
18
|
+
|
|
19
|
+
Implementation: Simple dictionary-based lookup with identity fallback for unknown rule IDs
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Maps deprecated rule IDs to their canonical replacements
|
|
23
|
+
RULE_ID_ALIASES: dict[str, str] = {
|
|
24
|
+
"print-statements.detected": "improper-logging.print-statement",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Maps deprecated linter command names to their canonical replacements
|
|
28
|
+
LINTER_ALIASES: dict[str, str] = {
|
|
29
|
+
"print-statements": "improper-logging",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_rule_id(rule_id: str) -> str:
|
|
34
|
+
"""Resolve a rule ID to its canonical form.
|
|
35
|
+
|
|
36
|
+
If the rule ID has been renamed, returns the new canonical name.
|
|
37
|
+
Otherwise, returns the rule ID unchanged.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
rule_id: The rule ID to resolve (may be deprecated or canonical)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The canonical rule ID
|
|
44
|
+
"""
|
|
45
|
+
return RULE_ID_ALIASES.get(rule_id, rule_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_linter_name(name: str) -> str:
|
|
49
|
+
"""Resolve a linter command name to its canonical form.
|
|
50
|
+
|
|
51
|
+
If the linter has been renamed, returns the new canonical name.
|
|
52
|
+
Otherwise, returns the name unchanged.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: The linter command name to resolve (may be deprecated or canonical)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The canonical linter name
|
|
59
|
+
"""
|
|
60
|
+
return LINTER_ALIASES.get(name, name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_deprecated_rule_id(rule_id: str) -> bool:
|
|
64
|
+
"""Check if a rule ID is deprecated.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
rule_id: The rule ID to check
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if the rule ID is deprecated (has an alias)
|
|
71
|
+
"""
|
|
72
|
+
return rule_id in RULE_ID_ALIASES
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_deprecated_linter(name: str) -> bool:
|
|
76
|
+
"""Check if a linter name is deprecated.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name: The linter name to check
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if the linter name is deprecated (has an alias)
|
|
83
|
+
"""
|
|
84
|
+
return name in LINTER_ALIASES
|
|
@@ -4,32 +4,46 @@ Purpose: Rule ID matching utilities for ignore directive processing
|
|
|
4
4
|
Scope: Pattern matching between rule IDs and ignore patterns
|
|
5
5
|
|
|
6
6
|
Overview: Provides functions for matching rule IDs against ignore patterns. Supports
|
|
7
|
-
exact matching, wildcard matching (*.suffix),
|
|
8
|
-
category.specific)
|
|
9
|
-
rule ID formatting.
|
|
7
|
+
exact matching, wildcard matching (*.suffix), prefix matching (category matches
|
|
8
|
+
category.specific), and alias resolution for backward compatibility with renamed
|
|
9
|
+
rules. All comparisons are case-insensitive to handle variations in rule ID formatting.
|
|
10
10
|
|
|
11
|
-
Dependencies: re for regex operations
|
|
11
|
+
Dependencies: re for regex operations, src.core.rule_aliases for alias resolution
|
|
12
12
|
|
|
13
13
|
Exports: rule_matches, check_bracket_rules, check_space_separated_rules
|
|
14
14
|
|
|
15
15
|
Interfaces: rule_matches(rule_id, pattern) -> bool for checking if rule matches pattern
|
|
16
16
|
|
|
17
|
-
Implementation: String-based pattern matching with wildcard and
|
|
17
|
+
Implementation: String-based pattern matching with wildcard, prefix, and alias support
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
21
|
|
|
22
|
+
from src.core.rule_aliases import RULE_ID_ALIASES
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
def rule_matches(rule_id: str, pattern: str) -> bool:
|
|
24
|
-
"""Check if rule ID matches pattern (supports wildcards and
|
|
26
|
+
"""Check if rule ID matches pattern (supports wildcards, prefixes, and aliases).
|
|
27
|
+
|
|
28
|
+
Supports backward compatibility through alias resolution:
|
|
29
|
+
- Pattern "print-statements" matches rule_id "improper-logging.print-statement"
|
|
30
|
+
- Pattern "print-statements.*" matches rule_id "improper-logging.print-statement"
|
|
25
31
|
|
|
26
32
|
Args:
|
|
27
|
-
rule_id: Rule ID to check (e.g., "
|
|
28
|
-
pattern: Pattern with optional wildcard (e.g., "nesting.*" or "
|
|
33
|
+
rule_id: Rule ID to check (e.g., "improper-logging.print-statement").
|
|
34
|
+
pattern: Pattern with optional wildcard (e.g., "nesting.*" or "print-statements").
|
|
29
35
|
|
|
30
36
|
Returns:
|
|
31
37
|
True if rule matches pattern.
|
|
32
38
|
"""
|
|
39
|
+
if _matches_pattern_directly(rule_id, pattern):
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
return _matches_via_alias(rule_id, pattern)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _matches_pattern_directly(rule_id: str, pattern: str) -> bool:
|
|
46
|
+
"""Check if rule ID matches pattern without alias resolution."""
|
|
33
47
|
rule_id_lower = rule_id.lower()
|
|
34
48
|
pattern_lower = pattern.lower()
|
|
35
49
|
|
|
@@ -46,6 +60,37 @@ def rule_matches(rule_id: str, pattern: str) -> bool:
|
|
|
46
60
|
return False
|
|
47
61
|
|
|
48
62
|
|
|
63
|
+
def _matches_via_alias(rule_id: str, pattern: str) -> bool:
|
|
64
|
+
"""Check if rule ID matches pattern through alias resolution."""
|
|
65
|
+
pattern_lower = pattern.lower()
|
|
66
|
+
rule_id_lower = rule_id.lower()
|
|
67
|
+
|
|
68
|
+
# Find deprecated IDs that alias to our rule_id and check pattern match
|
|
69
|
+
return any(
|
|
70
|
+
_pattern_matches_deprecated_id(pattern_lower, deprecated_id)
|
|
71
|
+
for deprecated_id, canonical_id in RULE_ID_ALIASES.items()
|
|
72
|
+
if canonical_id.lower() == rule_id_lower
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _pattern_matches_deprecated_id(pattern_lower: str, deprecated_id: str) -> bool:
|
|
77
|
+
"""Check if pattern matches a deprecated rule ID."""
|
|
78
|
+
deprecated_id_lower = deprecated_id.lower()
|
|
79
|
+
|
|
80
|
+
# Pattern exactly matches deprecated ID
|
|
81
|
+
if pattern_lower == deprecated_id_lower:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Pattern is prefix/wildcard that matches deprecated ID's category
|
|
85
|
+
deprecated_category = deprecated_id.split(".", maxsplit=1)[0].lower()
|
|
86
|
+
if pattern_lower == deprecated_category:
|
|
87
|
+
return True
|
|
88
|
+
if pattern_lower == deprecated_category + ".*":
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
49
94
|
def check_bracket_rules(rules_text: str, rule_id: str) -> bool:
|
|
50
95
|
"""Check if bracketed rules match the rule ID.
|
|
51
96
|
|
|
@@ -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
|
+
)
|