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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Build Violation objects for Rust clone abuse patterns
|
|
3
|
+
|
|
4
|
+
Scope: Creates violations with actionable suggestions for clone-in-loop, clone-chain,
|
|
5
|
+
and unnecessary-clone patterns
|
|
6
|
+
|
|
7
|
+
Overview: Provides module-level functions that create Violation objects for detected
|
|
8
|
+
.clone() abuse patterns in Rust code. Each violation includes the rule ID, location,
|
|
9
|
+
descriptive message explaining the performance or correctness impact, and a suggestion
|
|
10
|
+
for safer alternatives such as borrowing, Rc/Arc for shared ownership, or Cow for
|
|
11
|
+
clone-on-write patterns.
|
|
12
|
+
|
|
13
|
+
Dependencies: src.core.types for Violation dataclass
|
|
14
|
+
|
|
15
|
+
Exports: build_clone_in_loop_violation, build_clone_chain_violation, build_unnecessary_clone_violation
|
|
16
|
+
|
|
17
|
+
Interfaces: Module functions taking file_path, line, column, context and returning Violation
|
|
18
|
+
|
|
19
|
+
Implementation: Factory functions for each clone abuse pattern with pattern-specific suggestions
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from src.core.types import Violation
|
|
23
|
+
|
|
24
|
+
_CLONE_IN_LOOP_SUGGESTION = (
|
|
25
|
+
"Consider borrowing instead of cloning in a loop. "
|
|
26
|
+
"If ownership is needed, use Rc/Arc for shared ownership or collect references."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_CLONE_CHAIN_SUGGESTION = (
|
|
30
|
+
"Chained .clone().clone() is redundant. "
|
|
31
|
+
"A single .clone() produces an owned copy; the second clone is unnecessary."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_UNNECESSARY_CLONE_SUGGESTION = (
|
|
35
|
+
"This .clone() may be unnecessary if the original value is not used after cloning. "
|
|
36
|
+
"Consider passing ownership directly, borrowing, or using Cow for clone-on-write."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_clone_in_loop_violation(
|
|
41
|
+
file_path: str,
|
|
42
|
+
line: int,
|
|
43
|
+
column: int,
|
|
44
|
+
context: str,
|
|
45
|
+
) -> Violation:
|
|
46
|
+
"""Build a violation for .clone() call inside a loop body."""
|
|
47
|
+
message = f".clone() called inside a loop body may cause performance issues: {context}"
|
|
48
|
+
|
|
49
|
+
return Violation(
|
|
50
|
+
rule_id="clone-abuse.clone-in-loop",
|
|
51
|
+
file_path=file_path,
|
|
52
|
+
line=line,
|
|
53
|
+
column=column,
|
|
54
|
+
message=message,
|
|
55
|
+
suggestion=_CLONE_IN_LOOP_SUGGESTION,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_clone_chain_violation(
|
|
60
|
+
file_path: str,
|
|
61
|
+
line: int,
|
|
62
|
+
column: int,
|
|
63
|
+
context: str,
|
|
64
|
+
) -> Violation:
|
|
65
|
+
"""Build a violation for chained .clone().clone() calls."""
|
|
66
|
+
message = f"Chained .clone().clone() is redundant: {context}"
|
|
67
|
+
|
|
68
|
+
return Violation(
|
|
69
|
+
rule_id="clone-abuse.clone-chain",
|
|
70
|
+
file_path=file_path,
|
|
71
|
+
line=line,
|
|
72
|
+
column=column,
|
|
73
|
+
message=message,
|
|
74
|
+
suggestion=_CLONE_CHAIN_SUGGESTION,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_unnecessary_clone_violation(
|
|
79
|
+
file_path: str,
|
|
80
|
+
line: int,
|
|
81
|
+
column: int,
|
|
82
|
+
context: str,
|
|
83
|
+
) -> Violation:
|
|
84
|
+
"""Build a violation for unnecessary .clone() before move."""
|
|
85
|
+
message = f".clone() may be unnecessary when the original is not used afterward: {context}"
|
|
86
|
+
|
|
87
|
+
return Violation(
|
|
88
|
+
rule_id="clone-abuse.unnecessary-clone",
|
|
89
|
+
file_path=file_path,
|
|
90
|
+
line=line,
|
|
91
|
+
column=column,
|
|
92
|
+
message=message,
|
|
93
|
+
suggestion=_UNNECESSARY_CLONE_SUGGESTION,
|
|
94
|
+
)
|
|
@@ -32,6 +32,7 @@ import ast
|
|
|
32
32
|
from pathlib import Path
|
|
33
33
|
from typing import Any
|
|
34
34
|
|
|
35
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE
|
|
35
36
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
36
37
|
from src.core.linter_utils import load_linter_config
|
|
37
38
|
from src.core.types import Violation
|
|
@@ -42,6 +43,7 @@ from .config import MagicNumberConfig
|
|
|
42
43
|
from .context_analyzer import is_acceptable_context
|
|
43
44
|
from .definition_detector import is_definition_file
|
|
44
45
|
from .python_analyzer import PythonMagicNumberAnalyzer
|
|
46
|
+
from .rust_analyzer import RustMagicNumberAnalyzer
|
|
45
47
|
from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
|
|
46
48
|
from .typescript_ignore_checker import TypeScriptIgnoreChecker
|
|
47
49
|
from .violation_builder import ViolationBuilder
|
|
@@ -456,6 +458,96 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
456
458
|
for pattern in [".test.", ".spec.", "test_", "_test.", "/tests/", "/test/"]
|
|
457
459
|
)
|
|
458
460
|
|
|
461
|
+
def _check_rust(self, context: BaseLintContext, config: MagicNumberConfig) -> list[Violation]:
|
|
462
|
+
"""Check Rust code for magic number violations.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
context: Lint context with Rust file information
|
|
466
|
+
config: Magic numbers configuration
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
List of violations found in Rust code
|
|
470
|
+
"""
|
|
471
|
+
if not TREE_SITTER_RUST_AVAILABLE:
|
|
472
|
+
return []
|
|
473
|
+
|
|
474
|
+
if self._is_file_ignored(context, config):
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
analyzer = RustMagicNumberAnalyzer()
|
|
478
|
+
root_node = analyzer.parse_rust(context.file_content or "")
|
|
479
|
+
if root_node is None:
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
numeric_literals = analyzer.find_numeric_literals(root_node)
|
|
483
|
+
return self._collect_rust_violations(numeric_literals, context, config, analyzer)
|
|
484
|
+
|
|
485
|
+
def _collect_rust_violations(
|
|
486
|
+
self,
|
|
487
|
+
numeric_literals: list,
|
|
488
|
+
context: BaseLintContext,
|
|
489
|
+
config: MagicNumberConfig,
|
|
490
|
+
analyzer: RustMagicNumberAnalyzer,
|
|
491
|
+
) -> list[Violation]:
|
|
492
|
+
"""Collect violations from Rust numeric literals.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
numeric_literals: List of (node, value, line_number) tuples
|
|
496
|
+
context: Lint context
|
|
497
|
+
config: Configuration
|
|
498
|
+
analyzer: Rust analyzer instance
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
List of violations
|
|
502
|
+
"""
|
|
503
|
+
violations = []
|
|
504
|
+
for node, value, line_number in numeric_literals:
|
|
505
|
+
violation = self._try_create_rust_violation(
|
|
506
|
+
node, value, line_number, context, config, analyzer
|
|
507
|
+
)
|
|
508
|
+
if violation is not None:
|
|
509
|
+
violations.append(violation)
|
|
510
|
+
return violations
|
|
511
|
+
|
|
512
|
+
def _try_create_rust_violation( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
513
|
+
self,
|
|
514
|
+
node: object,
|
|
515
|
+
value: float | int,
|
|
516
|
+
line_number: int,
|
|
517
|
+
context: BaseLintContext,
|
|
518
|
+
config: MagicNumberConfig,
|
|
519
|
+
analyzer: RustMagicNumberAnalyzer,
|
|
520
|
+
) -> Violation | None:
|
|
521
|
+
"""Try to create a violation for a Rust numeric literal.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
node: Tree-sitter node
|
|
525
|
+
value: Numeric value
|
|
526
|
+
line_number: Line number of literal
|
|
527
|
+
context: Lint context
|
|
528
|
+
config: Configuration
|
|
529
|
+
analyzer: Rust analyzer
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Violation or None if should not flag
|
|
533
|
+
"""
|
|
534
|
+
if value in config.allowed_numbers:
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
if analyzer.is_constant_definition(node):
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
if analyzer.is_test_context(node):
|
|
541
|
+
return None
|
|
542
|
+
|
|
543
|
+
violation = self._violation_builder.create_rust_violation(
|
|
544
|
+
value, line_number, context.file_path
|
|
545
|
+
)
|
|
546
|
+
if self._should_ignore(violation, context):
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
return violation
|
|
550
|
+
|
|
459
551
|
def _should_ignore_typescript(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
460
552
|
"""Check if TypeScript violation should be ignored.
|
|
461
553
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust magic number detection using tree-sitter AST analysis
|
|
3
|
+
|
|
4
|
+
Scope: Tree-sitter based numeric literal detection for Rust code
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Rust code to detect numeric literals that should be extracted to named
|
|
7
|
+
constants. Uses tree-sitter parser to traverse Rust AST and identify integer_literal
|
|
8
|
+
and float_literal nodes with their line numbers and values. Detects acceptable contexts
|
|
9
|
+
such as const/static definitions and UPPERCASE constant declarations to avoid false
|
|
10
|
+
positives. Handles Rust-specific syntax including const items, static items, and
|
|
11
|
+
array/slice indexing.
|
|
12
|
+
|
|
13
|
+
Dependencies: RustBaseAnalyzer for tree-sitter parsing, TREE_SITTER_RUST_AVAILABLE
|
|
14
|
+
|
|
15
|
+
Exports: RustMagicNumberAnalyzer class with find_numeric_literals and context detection
|
|
16
|
+
|
|
17
|
+
Interfaces: find_numeric_literals(root_node) -> list[tuple], is_constant_definition(node)
|
|
18
|
+
|
|
19
|
+
Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE, RustBaseAnalyzer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RustMagicNumberAnalyzer(RustBaseAnalyzer):
|
|
28
|
+
"""Analyzes Rust code for magic numbers using tree-sitter."""
|
|
29
|
+
|
|
30
|
+
# Node types that represent numeric literals in Rust
|
|
31
|
+
NUMERIC_LITERAL_TYPES = {"integer_literal", "float_literal"}
|
|
32
|
+
|
|
33
|
+
def find_numeric_literals(self, root_node: Any) -> list[tuple[Any, float | int, int]]:
|
|
34
|
+
"""Find all numeric literal nodes in Rust AST.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
root_node: Root tree-sitter node to search from
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of (node, value, line_number) tuples for each numeric literal
|
|
41
|
+
"""
|
|
42
|
+
if not TREE_SITTER_RUST_AVAILABLE or root_node is None:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
literals: list[tuple[Any, float | int, int]] = []
|
|
46
|
+
self._collect_numeric_literals(root_node, literals)
|
|
47
|
+
return literals
|
|
48
|
+
|
|
49
|
+
def _collect_numeric_literals(
|
|
50
|
+
self, node: Any, literals: list[tuple[Any, float | int, int]]
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Recursively collect numeric literals from AST.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
node: Current tree-sitter node
|
|
56
|
+
literals: List to accumulate found literals
|
|
57
|
+
"""
|
|
58
|
+
if node.type in self.NUMERIC_LITERAL_TYPES:
|
|
59
|
+
value = self._extract_numeric_value(node)
|
|
60
|
+
if value is not None:
|
|
61
|
+
line_number = node.start_point[0] + 1
|
|
62
|
+
literals.append((node, value, line_number))
|
|
63
|
+
|
|
64
|
+
for child in node.children:
|
|
65
|
+
self._collect_numeric_literals(child, literals)
|
|
66
|
+
|
|
67
|
+
def _extract_numeric_value(self, node: Any) -> float | int | None:
|
|
68
|
+
"""Extract numeric value from a literal node.
|
|
69
|
+
|
|
70
|
+
Handles Rust integer suffixes (i32, u64, etc.) and various formats.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
node: Tree-sitter numeric literal node
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Numeric value (int or float) or None if parsing fails
|
|
77
|
+
"""
|
|
78
|
+
text = self.extract_node_text(node)
|
|
79
|
+
# Strip Rust type suffixes (i32, u64, f64, usize, etc.)
|
|
80
|
+
cleaned = self._strip_type_suffix(text)
|
|
81
|
+
# Strip underscores used as visual separators (e.g., 1_000_000)
|
|
82
|
+
cleaned = cleaned.replace("_", "")
|
|
83
|
+
try:
|
|
84
|
+
if node.type == "float_literal":
|
|
85
|
+
return float(cleaned)
|
|
86
|
+
return int(cleaned, 0) # Handles hex, octal, binary
|
|
87
|
+
except (ValueError, TypeError):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def _strip_type_suffix(self, text: str) -> str:
|
|
91
|
+
"""Strip Rust numeric type suffixes from literal text.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
text: Raw literal text (e.g., "42i32", "3.14f64")
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Text with suffix removed
|
|
98
|
+
"""
|
|
99
|
+
suffixes = (
|
|
100
|
+
"u8",
|
|
101
|
+
"u16",
|
|
102
|
+
"u32",
|
|
103
|
+
"u64",
|
|
104
|
+
"u128",
|
|
105
|
+
"usize",
|
|
106
|
+
"i8",
|
|
107
|
+
"i16",
|
|
108
|
+
"i32",
|
|
109
|
+
"i64",
|
|
110
|
+
"i128",
|
|
111
|
+
"isize",
|
|
112
|
+
"f32",
|
|
113
|
+
"f64",
|
|
114
|
+
)
|
|
115
|
+
for suffix in suffixes:
|
|
116
|
+
if text.endswith(suffix):
|
|
117
|
+
return text[: -len(suffix)]
|
|
118
|
+
return text
|
|
119
|
+
|
|
120
|
+
def is_constant_definition(self, node: Any) -> bool:
|
|
121
|
+
"""Check if numeric literal is in a const or static definition.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
node: Numeric literal node
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if inside const_item or static_item
|
|
128
|
+
"""
|
|
129
|
+
if not TREE_SITTER_RUST_AVAILABLE:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
current = node.parent
|
|
133
|
+
while current is not None:
|
|
134
|
+
if current.type in ("const_item", "static_item"):
|
|
135
|
+
return True
|
|
136
|
+
current = current.parent
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def is_test_context(self, node: Any) -> bool:
|
|
140
|
+
"""Check if numeric literal is inside test code.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
node: Numeric literal node
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if inside #[test] function or #[cfg(test)] module
|
|
147
|
+
"""
|
|
148
|
+
return self.is_inside_test(node)
|
|
@@ -68,6 +68,37 @@ class ViolationBuilder:
|
|
|
68
68
|
suggestion=suggestion,
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
+
def create_rust_violation(
|
|
72
|
+
self,
|
|
73
|
+
value: int | float,
|
|
74
|
+
line: int,
|
|
75
|
+
file_path: Path | None,
|
|
76
|
+
) -> Violation:
|
|
77
|
+
"""Create a violation for a Rust magic number.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: The numeric value
|
|
81
|
+
line: Line number where the violation occurs
|
|
82
|
+
file_path: Path to the file
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Violation object with details about the magic number
|
|
86
|
+
"""
|
|
87
|
+
message = f"Magic number {value} should be a named constant"
|
|
88
|
+
|
|
89
|
+
suggestion = (
|
|
90
|
+
f"Extract {value} to a named constant (e.g., const CONSTANT_NAME: i32 = {value})"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return Violation(
|
|
94
|
+
rule_id=self.rule_id,
|
|
95
|
+
file_path=str(file_path) if file_path else "",
|
|
96
|
+
line=line,
|
|
97
|
+
column=0,
|
|
98
|
+
message=message,
|
|
99
|
+
suggestion=suggestion,
|
|
100
|
+
)
|
|
101
|
+
|
|
71
102
|
def create_typescript_violation(
|
|
72
103
|
self,
|
|
73
104
|
value: int | float,
|
src/linters/nesting/linter.py
CHANGED
|
@@ -21,6 +21,7 @@ Implementation: Composition pattern with helper classes, AST-based analysis with
|
|
|
21
21
|
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE
|
|
24
25
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
26
|
from src.core.linter_utils import load_linter_config, with_parsed_python
|
|
26
27
|
from src.core.types import Violation
|
|
@@ -28,6 +29,7 @@ from src.linter_config.ignore import get_ignore_parser
|
|
|
28
29
|
|
|
29
30
|
from .config import NestingConfig
|
|
30
31
|
from .python_analyzer import PythonNestingAnalyzer
|
|
32
|
+
from .rust_analyzer import RustNestingAnalyzer
|
|
31
33
|
from .typescript_analyzer import TypeScriptNestingAnalyzer
|
|
32
34
|
from .violation_builder import NestingViolationBuilder
|
|
33
35
|
|
|
@@ -42,6 +44,7 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
42
44
|
# Singleton analyzers for performance (avoid recreating per-file)
|
|
43
45
|
self._python_analyzer = PythonNestingAnalyzer()
|
|
44
46
|
self._typescript_analyzer = TypeScriptNestingAnalyzer()
|
|
47
|
+
self._rust_analyzer = RustNestingAnalyzer()
|
|
45
48
|
|
|
46
49
|
@property
|
|
47
50
|
def rule_id(self) -> str:
|
|
@@ -165,6 +168,53 @@ class NestingDepthRule(MultiLanguageLintRule):
|
|
|
165
168
|
functions, self._typescript_analyzer, config, context
|
|
166
169
|
)
|
|
167
170
|
|
|
171
|
+
def _check_rust(self, context: BaseLintContext, config: NestingConfig) -> list[Violation]:
|
|
172
|
+
"""Check Rust code for nesting violations.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
context: Lint context with Rust file information
|
|
176
|
+
config: Nesting configuration
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of violations found in Rust code
|
|
180
|
+
"""
|
|
181
|
+
if not TREE_SITTER_RUST_AVAILABLE:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
root_node = self._rust_analyzer.parse_rust(context.file_content or "")
|
|
185
|
+
if root_node is None:
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
functions = self._rust_analyzer.find_all_functions(root_node)
|
|
189
|
+
return self._process_rust_functions(functions, config, context)
|
|
190
|
+
|
|
191
|
+
def _process_rust_functions(
|
|
192
|
+
self, functions: list, config: NestingConfig, context: BaseLintContext
|
|
193
|
+
) -> list[Violation]:
|
|
194
|
+
"""Process Rust functions and collect violations.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
functions: List of (function_node, function_name) tuples
|
|
198
|
+
config: Nesting configuration
|
|
199
|
+
context: Lint context
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of violations
|
|
203
|
+
"""
|
|
204
|
+
violations = []
|
|
205
|
+
for func_node, func_name in functions:
|
|
206
|
+
max_depth, _line = self._rust_analyzer.calculate_max_depth(func_node)
|
|
207
|
+
if max_depth <= config.max_nesting_depth:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
violation = self._violation_builder.create_rust_nesting_violation(
|
|
211
|
+
(func_node, func_name), max_depth, config, context
|
|
212
|
+
)
|
|
213
|
+
# dry: ignore-block - Standard linter pattern (check-ignore-append)
|
|
214
|
+
if not self._should_ignore(violation, context):
|
|
215
|
+
violations.append(violation)
|
|
216
|
+
return violations
|
|
217
|
+
|
|
168
218
|
def _should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
169
219
|
"""Check if violation should be ignored based on inline directives.
|
|
170
220
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Rust AST-based nesting depth calculator
|
|
3
|
+
|
|
4
|
+
Scope: Rust code nesting depth analysis using tree-sitter parser
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes Rust code to calculate maximum nesting depth using AST traversal.
|
|
7
|
+
Extends RustBaseAnalyzer to reuse common tree-sitter initialization and parsing.
|
|
8
|
+
Implements visitor pattern to walk Rust AST, tracking current depth and maximum
|
|
9
|
+
depth found. Increments depth for control flow statements (if, match, loop, while,
|
|
10
|
+
for) and closures. Returns maximum depth and location for each function.
|
|
11
|
+
|
|
12
|
+
Dependencies: RustBaseAnalyzer, TREE_SITTER_RUST_AVAILABLE
|
|
13
|
+
|
|
14
|
+
Exports: RustNestingAnalyzer class with calculate_max_depth and find_all_functions
|
|
15
|
+
|
|
16
|
+
Interfaces: calculate_max_depth(func_node) -> tuple[int, int], find_all_functions(root_node)
|
|
17
|
+
|
|
18
|
+
Implementation: Inherits tree-sitter parsing from base, visitor pattern with depth tracking
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from src.analyzers.rust_base import TREE_SITTER_RUST_AVAILABLE, RustBaseAnalyzer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RustNestingAnalyzer(RustBaseAnalyzer):
|
|
27
|
+
"""Calculates maximum nesting depth in Rust functions."""
|
|
28
|
+
|
|
29
|
+
# Tree-sitter node types that increase nesting depth
|
|
30
|
+
NESTING_NODE_TYPES = {
|
|
31
|
+
"if_expression",
|
|
32
|
+
"match_expression",
|
|
33
|
+
"loop_expression",
|
|
34
|
+
"while_expression",
|
|
35
|
+
"for_expression",
|
|
36
|
+
"closure_expression",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def calculate_max_depth(self, func_node: Any) -> tuple[int, int]:
|
|
40
|
+
"""Calculate maximum nesting depth in a Rust function.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
func_node: Function item AST node
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple of (max_depth, line_number)
|
|
47
|
+
"""
|
|
48
|
+
if not TREE_SITTER_RUST_AVAILABLE:
|
|
49
|
+
return 0, 0
|
|
50
|
+
|
|
51
|
+
body_node = self._find_function_body(func_node)
|
|
52
|
+
if not body_node:
|
|
53
|
+
return 0, func_node.start_point[0] + 1
|
|
54
|
+
|
|
55
|
+
max_depth = 0
|
|
56
|
+
max_depth_line = body_node.start_point[0] + 1
|
|
57
|
+
|
|
58
|
+
def visit_node(node: Any, current_depth: int = 0) -> None:
|
|
59
|
+
nonlocal max_depth, max_depth_line
|
|
60
|
+
|
|
61
|
+
if current_depth > max_depth:
|
|
62
|
+
max_depth = current_depth
|
|
63
|
+
max_depth_line = node.start_point[0] + 1
|
|
64
|
+
|
|
65
|
+
new_depth = current_depth + 1 if node.type in self.NESTING_NODE_TYPES else current_depth
|
|
66
|
+
|
|
67
|
+
for child in node.children:
|
|
68
|
+
visit_node(child, new_depth)
|
|
69
|
+
|
|
70
|
+
# Start at depth 1 for function body children
|
|
71
|
+
for child in body_node.children:
|
|
72
|
+
visit_node(child, 1)
|
|
73
|
+
|
|
74
|
+
return max_depth, max_depth_line
|
|
75
|
+
|
|
76
|
+
def find_all_functions(self, root_node: Any) -> list[tuple[Any, str]]:
|
|
77
|
+
"""Find all function definitions in Rust AST.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
root_node: Root node to search from
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of (function_node, function_name) tuples
|
|
84
|
+
"""
|
|
85
|
+
if not TREE_SITTER_RUST_AVAILABLE or root_node is None:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
functions: list[tuple[Any, str]] = []
|
|
89
|
+
self._collect_functions_recursive(root_node, functions)
|
|
90
|
+
return functions
|
|
91
|
+
|
|
92
|
+
def _collect_functions_recursive(self, node: Any, functions: list[tuple[Any, str]]) -> None:
|
|
93
|
+
"""Recursively collect function nodes from Rust AST.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
node: Current node to examine
|
|
97
|
+
functions: List to append found functions to
|
|
98
|
+
"""
|
|
99
|
+
if node.type == "function_item":
|
|
100
|
+
name = self.extract_identifier_name(node)
|
|
101
|
+
functions.append((node, name))
|
|
102
|
+
|
|
103
|
+
for child in node.children:
|
|
104
|
+
self._collect_functions_recursive(child, functions)
|
|
105
|
+
|
|
106
|
+
def _find_function_body(self, func_node: Any) -> Any:
|
|
107
|
+
"""Find the block node (function body) in a function item.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
func_node: Function item node to search
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Block node or None
|
|
114
|
+
"""
|
|
115
|
+
for child in func_node.children:
|
|
116
|
+
if child.type == "block":
|
|
117
|
+
return child
|
|
118
|
+
return None
|
|
@@ -123,6 +123,38 @@ class NestingViolationBuilder(BaseViolationBuilder):
|
|
|
123
123
|
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
+
def create_rust_nesting_violation(
|
|
127
|
+
self,
|
|
128
|
+
func_info: tuple[Any, str],
|
|
129
|
+
max_depth: int,
|
|
130
|
+
config: NestingConfig,
|
|
131
|
+
context: BaseLintContext,
|
|
132
|
+
) -> Violation:
|
|
133
|
+
"""Create violation for excessive nesting in Rust function.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
func_info: Tuple of (func_node, func_name)
|
|
137
|
+
max_depth: Actual max nesting depth found
|
|
138
|
+
config: Nesting configuration
|
|
139
|
+
context: Lint context
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Nesting depth violation
|
|
143
|
+
"""
|
|
144
|
+
func_node, func_name = func_info
|
|
145
|
+
line = func_node.start_point[0] + 1 # Convert to 1-indexed
|
|
146
|
+
column = func_node.start_point[1]
|
|
147
|
+
|
|
148
|
+
return self.build_from_params(
|
|
149
|
+
rule_id=self.rule_id,
|
|
150
|
+
file_path=str(context.file_path or ""),
|
|
151
|
+
line=line,
|
|
152
|
+
column=column,
|
|
153
|
+
message=f"Function '{func_name}' has excessive nesting depth ({max_depth})",
|
|
154
|
+
severity=Severity.ERROR,
|
|
155
|
+
suggestion=self._generate_suggestion(max_depth, config.max_nesting_depth),
|
|
156
|
+
)
|
|
157
|
+
|
|
126
158
|
def _generate_suggestion(self, actual_depth: int, max_depth: int) -> str:
|
|
127
159
|
"""Generate refactoring suggestion based on depth.
|
|
128
160
|
|
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
"""
|
|
2
2
|
File: src/linters/print_statements/__init__.py
|
|
3
3
|
|
|
4
|
-
Purpose:
|
|
4
|
+
Purpose: Improper logging linter package exports and convenience functions
|
|
5
5
|
|
|
6
|
-
Exports: PrintStatementRule
|
|
6
|
+
Exports: PrintStatementRule, ConditionalVerboseRule classes, PrintStatementConfig dataclass,
|
|
7
|
+
lint() convenience function, ImproperLoggingPrintRule alias
|
|
7
8
|
|
|
8
|
-
Depends: .linter for PrintStatementRule, .
|
|
9
|
+
Depends: .linter for PrintStatementRule, .conditional_verbose_rule for ConditionalVerboseRule,
|
|
10
|
+
.config for PrintStatementConfig
|
|
9
11
|
|
|
10
12
|
Implements: lint(file_path, config) -> list[Violation] for simple linting operations
|
|
11
13
|
|
|
12
14
|
Related: src/linters/magic_numbers/__init__.py, src/core/base.py
|
|
13
15
|
|
|
14
|
-
Overview: Provides the public interface for the
|
|
15
|
-
PrintStatementRule
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
Overview: Provides the public interface for the improper logging linter package (formerly
|
|
17
|
+
print-statements). Exports PrintStatementRule for detecting print/console statements and
|
|
18
|
+
ConditionalVerboseRule for detecting conditional verbose logging anti-patterns. Both rules
|
|
19
|
+
use rule IDs prefixed with 'improper-logging.' for unified filtering. Includes lint()
|
|
20
|
+
convenience function for simple API usage without the orchestrator. ImproperLoggingPrintRule
|
|
21
|
+
is provided as an alias for PrintStatementRule for semantic clarity.
|
|
20
22
|
|
|
21
|
-
Usage: from src.linters.print_statements import PrintStatementRule, lint
|
|
23
|
+
Usage: from src.linters.print_statements import PrintStatementRule, ConditionalVerboseRule, lint
|
|
22
24
|
violations = lint("path/to/file.py")
|
|
23
25
|
|
|
24
26
|
Notes: Module-level exports with __all__ definition, convenience function wrapper
|
|
25
27
|
"""
|
|
26
28
|
|
|
29
|
+
from .conditional_verbose_rule import ConditionalVerboseRule
|
|
27
30
|
from .config import PrintStatementConfig
|
|
28
31
|
from .linter import PrintStatementRule
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
# Alias for semantic clarity (both detect improper logging patterns)
|
|
34
|
+
ImproperLoggingPrintRule = PrintStatementRule
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"PrintStatementRule",
|
|
38
|
+
"ConditionalVerboseRule",
|
|
39
|
+
"PrintStatementConfig",
|
|
40
|
+
"ImproperLoggingPrintRule",
|
|
41
|
+
"lint",
|
|
42
|
+
]
|
|
31
43
|
|
|
32
44
|
|
|
33
45
|
def lint(file_path: str, config: dict | None = None) -> list:
|