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.
Files changed (52) hide show
  1. src/cli/config.py +4 -12
  2. src/cli/linters/__init__.py +13 -3
  3. src/cli/linters/code_patterns.py +42 -38
  4. src/cli/linters/code_smells.py +8 -17
  5. src/cli/linters/documentation.py +3 -6
  6. src/cli/linters/performance.py +4 -10
  7. src/cli/linters/rust.py +177 -0
  8. src/cli/linters/shared.py +2 -7
  9. src/cli/linters/structure.py +4 -11
  10. src/cli/linters/structure_quality.py +4 -11
  11. src/cli/main.py +9 -12
  12. src/cli/utils.py +7 -16
  13. src/core/__init__.py +14 -0
  14. src/core/base.py +30 -0
  15. src/core/constants.py +1 -0
  16. src/core/linter_utils.py +42 -1
  17. src/core/rule_aliases.py +84 -0
  18. src/linter_config/rule_matcher.py +53 -8
  19. src/linters/blocking_async/__init__.py +31 -0
  20. src/linters/blocking_async/config.py +67 -0
  21. src/linters/blocking_async/linter.py +183 -0
  22. src/linters/blocking_async/rust_analyzer.py +419 -0
  23. src/linters/blocking_async/violation_builder.py +97 -0
  24. src/linters/clone_abuse/__init__.py +31 -0
  25. src/linters/clone_abuse/config.py +65 -0
  26. src/linters/clone_abuse/linter.py +183 -0
  27. src/linters/clone_abuse/rust_analyzer.py +356 -0
  28. src/linters/clone_abuse/violation_builder.py +94 -0
  29. src/linters/magic_numbers/linter.py +92 -0
  30. src/linters/magic_numbers/rust_analyzer.py +148 -0
  31. src/linters/magic_numbers/violation_builder.py +31 -0
  32. src/linters/nesting/linter.py +50 -0
  33. src/linters/nesting/rust_analyzer.py +118 -0
  34. src/linters/nesting/violation_builder.py +32 -0
  35. src/linters/print_statements/__init__.py +23 -11
  36. src/linters/print_statements/conditional_verbose_analyzer.py +200 -0
  37. src/linters/print_statements/conditional_verbose_rule.py +254 -0
  38. src/linters/print_statements/linter.py +2 -2
  39. src/linters/srp/class_analyzer.py +49 -0
  40. src/linters/srp/linter.py +22 -0
  41. src/linters/srp/rust_analyzer.py +206 -0
  42. src/linters/unwrap_abuse/__init__.py +30 -0
  43. src/linters/unwrap_abuse/config.py +59 -0
  44. src/linters/unwrap_abuse/linter.py +166 -0
  45. src/linters/unwrap_abuse/rust_analyzer.py +118 -0
  46. src/linters/unwrap_abuse/violation_builder.py +89 -0
  47. src/templates/thailint_config_template.yaml +88 -0
  48. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/METADATA +7 -3
  49. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/RECORD +52 -30
  50. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/WHEEL +0 -0
  51. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/entry_points.txt +0 -0
  52. {thailint-0.15.8.dist-info → thailint-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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 logging for the CLI application.
35
+ """Configure loguru for the CLI application.
39
36
 
40
37
  Args:
41
- verbose: Enable DEBUG level logging if True, INFO otherwise.
38
+ verbose: Enable DEBUG level logging if True, WARNING otherwise.
42
39
  """
43
- level = logging.DEBUG if verbose else logging.INFO
44
-
45
- logging.basicConfig(
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="%(asctime)s | %(levelname)-8s | %(message)s",
48
- datefmt="%Y-%m-%d %H:%M:%S",
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
- if verbose:
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
@@ -28,6 +28,7 @@ class Language(str, Enum):
28
28
  TYPESCRIPT = "typescript"
29
29
  JAVASCRIPT = "javascript"
30
30
  MARKDOWN = "markdown"
31
+ RUST = "rust"
31
32
 
32
33
 
33
34
  class StorageMode(str, Enum):
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,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), and prefix matching (category matches
8
- category.specific). All comparisons are case-insensitive to handle variations in
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 prefix support
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 prefixes).
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., "nesting.excessive-depth").
28
- pattern: Pattern with optional wildcard (e.g., "nesting.*" or "nesting").
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
+ )