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
@@ -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,
@@ -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: Print statements linter package exports and convenience functions
4
+ Purpose: Improper logging linter package exports and convenience functions
5
5
 
6
- Exports: PrintStatementRule class, PrintStatementConfig dataclass, lint() convenience function
6
+ Exports: PrintStatementRule, ConditionalVerboseRule classes, PrintStatementConfig dataclass,
7
+ lint() convenience function, ImproperLoggingPrintRule alias
7
8
 
8
- Depends: .linter for PrintStatementRule, .config for PrintStatementConfig
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 print statements linter package. Exports main
15
- PrintStatementRule class for use by the orchestrator and PrintStatementConfig for configuration.
16
- Includes lint() convenience function that provides a simple API for running the print statements
17
- linter on a file without directly interacting with the orchestrator. This module serves as the
18
- entry point for users of the print statements linter, hiding implementation details and exposing
19
- only the essential components needed for linting operations.
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
- __all__ = ["PrintStatementRule", "PrintStatementConfig", "lint"]
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: