thailint 0.2.1__py3-none-any.whl → 0.3.1__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.py +101 -0
- src/config.py +6 -2
- src/core/base.py +90 -5
- src/linters/dry/block_filter.py +5 -2
- src/linters/dry/cache.py +46 -92
- src/linters/dry/config.py +17 -13
- src/linters/dry/duplicate_storage.py +17 -80
- src/linters/dry/file_analyzer.py +11 -48
- src/linters/dry/linter.py +5 -12
- src/linters/dry/python_analyzer.py +12 -1
- src/linters/dry/storage_initializer.py +9 -18
- src/linters/dry/violation_filter.py +4 -1
- src/linters/magic_numbers/__init__.py +48 -0
- src/linters/magic_numbers/config.py +71 -0
- src/linters/magic_numbers/context_analyzer.py +247 -0
- src/linters/magic_numbers/linter.py +452 -0
- src/linters/magic_numbers/python_analyzer.py +76 -0
- src/linters/magic_numbers/typescript_analyzer.py +217 -0
- src/linters/magic_numbers/violation_builder.py +98 -0
- src/linters/nesting/__init__.py +6 -2
- src/linters/nesting/config.py +6 -3
- src/linters/nesting/linter.py +8 -19
- src/linters/srp/__init__.py +3 -3
- src/linters/srp/config.py +12 -6
- src/linters/srp/linter.py +33 -24
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/METADATA +196 -42
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/RECORD +30 -23
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/LICENSE +0 -0
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/WHEEL +0 -0
- {thailint-0.2.1.dist-info → thailint-0.3.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Python AST analysis for finding numeric literal nodes
|
|
3
|
+
|
|
4
|
+
Scope: Python magic number detection through AST traversal
|
|
5
|
+
|
|
6
|
+
Overview: Provides PythonMagicNumberAnalyzer class that traverses Python AST to find all numeric
|
|
7
|
+
literal nodes (integers and floats). Uses ast.NodeVisitor pattern to walk the syntax tree and
|
|
8
|
+
collect Constant nodes containing numeric values along with their parent nodes and line numbers.
|
|
9
|
+
Returns structured data about each numeric literal including the AST node, parent node, numeric
|
|
10
|
+
value, and source location. This analyzer handles Python-specific AST structure and provides
|
|
11
|
+
the foundation for magic number detection by identifying all candidates before context filtering.
|
|
12
|
+
|
|
13
|
+
Dependencies: ast module for AST parsing and node types
|
|
14
|
+
|
|
15
|
+
Exports: PythonMagicNumberAnalyzer class
|
|
16
|
+
|
|
17
|
+
Interfaces: PythonMagicNumberAnalyzer.find_numeric_literals(tree) -> list[tuple],
|
|
18
|
+
returns list of (node, parent, value, line_number) tuples
|
|
19
|
+
|
|
20
|
+
Implementation: AST NodeVisitor pattern with parent tracking, filters for numeric Constant nodes
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PythonMagicNumberAnalyzer(ast.NodeVisitor):
|
|
28
|
+
"""Analyzes Python AST to find numeric literals."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize the analyzer."""
|
|
32
|
+
self.numeric_literals: list[tuple[ast.Constant, ast.AST | None, Any, int]] = []
|
|
33
|
+
self.parent_map: dict[ast.AST, ast.AST] = {}
|
|
34
|
+
|
|
35
|
+
def find_numeric_literals(
|
|
36
|
+
self, tree: ast.AST
|
|
37
|
+
) -> list[tuple[ast.Constant, ast.AST | None, Any, int]]:
|
|
38
|
+
"""Find all numeric literals in the AST.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
tree: The AST to analyze
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of tuples (node, parent, value, line_number)
|
|
45
|
+
"""
|
|
46
|
+
self.numeric_literals = []
|
|
47
|
+
self.parent_map = {}
|
|
48
|
+
self._build_parent_map(tree)
|
|
49
|
+
self.visit(tree)
|
|
50
|
+
return self.numeric_literals
|
|
51
|
+
|
|
52
|
+
def _build_parent_map(self, node: ast.AST, parent: ast.AST | None = None) -> None:
|
|
53
|
+
"""Build a map of nodes to their parents.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
node: Current AST node
|
|
57
|
+
parent: Parent of current node
|
|
58
|
+
"""
|
|
59
|
+
if parent is not None:
|
|
60
|
+
self.parent_map[node] = parent
|
|
61
|
+
|
|
62
|
+
for child in ast.iter_child_nodes(node):
|
|
63
|
+
self._build_parent_map(child, node)
|
|
64
|
+
|
|
65
|
+
def visit_Constant(self, node: ast.Constant) -> None:
|
|
66
|
+
"""Visit a Constant node and check if it's a numeric literal.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
node: The Constant node to check
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(node.value, (int, float)):
|
|
72
|
+
parent = self.parent_map.get(node)
|
|
73
|
+
line_number = node.lineno if hasattr(node, "lineno") else 0
|
|
74
|
+
self.numeric_literals.append((node, parent, node.value, line_number))
|
|
75
|
+
|
|
76
|
+
self.generic_visit(node)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript/JavaScript magic number detection using Tree-sitter AST analysis
|
|
3
|
+
|
|
4
|
+
Scope: Tree-sitter based numeric literal detection for TypeScript and JavaScript code
|
|
5
|
+
|
|
6
|
+
Overview: Analyzes TypeScript and JavaScript code to detect numeric literals that should be
|
|
7
|
+
extracted to named constants. Uses Tree-sitter parser to traverse TypeScript AST and
|
|
8
|
+
identify numeric literal nodes with their line numbers and values. Detects acceptable
|
|
9
|
+
contexts such as enum definitions and UPPERCASE constant declarations to avoid false
|
|
10
|
+
positives. Supports both TypeScript and JavaScript files with shared detection logic.
|
|
11
|
+
Handles TypeScript-specific syntax including enums, const assertions, readonly properties,
|
|
12
|
+
arrow functions, async functions, and class methods.
|
|
13
|
+
|
|
14
|
+
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, tree-sitter Node type
|
|
15
|
+
|
|
16
|
+
Exports: TypeScriptMagicNumberAnalyzer class with find_numeric_literals and context detection
|
|
17
|
+
|
|
18
|
+
Interfaces: find_numeric_literals(root_node) -> list[tuple], is_enum_context(node),
|
|
19
|
+
is_constant_definition(node)
|
|
20
|
+
|
|
21
|
+
Implementation: Tree-sitter node traversal with visitor pattern, context-aware filtering
|
|
22
|
+
for acceptable numeric literal locations
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from tree_sitter import Node
|
|
31
|
+
|
|
32
|
+
TREE_SITTER_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
TREE_SITTER_AVAILABLE = False
|
|
35
|
+
Node = Any # type: ignore
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TypeScriptMagicNumberAnalyzer(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
39
|
+
"""Analyzes TypeScript/JavaScript code for magic numbers using Tree-sitter.
|
|
40
|
+
|
|
41
|
+
Note: Method count (11) exceeds SRP limit (8) because refactoring for A-grade
|
|
42
|
+
complexity requires extracting helper methods. Class maintains single responsibility
|
|
43
|
+
of TypeScript magic number detection - all methods support this core purpose.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def find_numeric_literals(self, root_node: Node) -> list[tuple[Node, float | int, int]]:
|
|
47
|
+
"""Find all numeric literal nodes in TypeScript/JavaScript AST.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
root_node: Root tree-sitter node to search from
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of (node, value, line_number) tuples for each numeric literal
|
|
54
|
+
"""
|
|
55
|
+
if not TREE_SITTER_AVAILABLE or root_node is None:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
literals: list[tuple[Node, float | int, int]] = []
|
|
59
|
+
self._collect_numeric_literals(root_node, literals)
|
|
60
|
+
return literals
|
|
61
|
+
|
|
62
|
+
def _collect_numeric_literals(
|
|
63
|
+
self, node: Node, literals: list[tuple[Node, float | int, int]]
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Recursively collect numeric literals from AST.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
node: Current tree-sitter node
|
|
69
|
+
literals: List to accumulate found literals
|
|
70
|
+
"""
|
|
71
|
+
if node.type == "number":
|
|
72
|
+
value = self._extract_numeric_value(node)
|
|
73
|
+
if value is not None:
|
|
74
|
+
line_number = node.start_point[0] + 1
|
|
75
|
+
literals.append((node, value, line_number))
|
|
76
|
+
|
|
77
|
+
for child in node.children:
|
|
78
|
+
self._collect_numeric_literals(child, literals)
|
|
79
|
+
|
|
80
|
+
def _extract_numeric_value(self, node: Node) -> float | int | None:
|
|
81
|
+
"""Extract numeric value from number node.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
node: Tree-sitter number node
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Numeric value (int or float) or None if parsing fails
|
|
88
|
+
"""
|
|
89
|
+
text = self.extract_node_text(node)
|
|
90
|
+
try:
|
|
91
|
+
# Try int first
|
|
92
|
+
if "." not in text and "e" not in text.lower():
|
|
93
|
+
return int(text, 0) # Handles hex, octal, binary
|
|
94
|
+
# Otherwise float
|
|
95
|
+
return float(text)
|
|
96
|
+
except (ValueError, TypeError):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def is_enum_context(self, node: Node) -> bool:
|
|
100
|
+
"""Check if numeric literal is in enum definition.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
node: Numeric literal node
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if node is within enum_declaration
|
|
107
|
+
"""
|
|
108
|
+
if not TREE_SITTER_AVAILABLE:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
current = node.parent
|
|
112
|
+
while current is not None:
|
|
113
|
+
if current.type == "enum_declaration":
|
|
114
|
+
return True
|
|
115
|
+
current = current.parent
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def is_constant_definition(self, node: Node, source_code: str) -> bool:
|
|
119
|
+
"""Check if numeric literal is in UPPERCASE constant definition.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
node: Numeric literal node
|
|
123
|
+
source_code: Full source code to extract variable names
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if assigned to UPPERCASE constant variable
|
|
127
|
+
"""
|
|
128
|
+
if not TREE_SITTER_AVAILABLE:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Find the declaration parent
|
|
132
|
+
parent = self._find_declaration_parent(node)
|
|
133
|
+
if parent is None:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
# Check if identifier is UPPERCASE constant
|
|
137
|
+
return self._has_uppercase_identifier(parent)
|
|
138
|
+
|
|
139
|
+
def _find_declaration_parent(self, node: Node) -> Node | None:
|
|
140
|
+
"""Find the declaration parent node.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
node: Starting node
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Declaration parent or None
|
|
147
|
+
"""
|
|
148
|
+
parent = node.parent
|
|
149
|
+
if self._is_declaration_type(parent):
|
|
150
|
+
return parent
|
|
151
|
+
|
|
152
|
+
# Try grandparent for nested cases
|
|
153
|
+
if parent is not None:
|
|
154
|
+
grandparent = parent.parent
|
|
155
|
+
if self._is_declaration_type(grandparent):
|
|
156
|
+
return grandparent
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def _is_declaration_type(self, node: Node | None) -> bool:
|
|
161
|
+
"""Check if node is a declaration type."""
|
|
162
|
+
if node is None:
|
|
163
|
+
return False
|
|
164
|
+
return node.type in ("variable_declarator", "lexical_declaration", "pair")
|
|
165
|
+
|
|
166
|
+
def _has_uppercase_identifier(self, parent_node: Node) -> bool:
|
|
167
|
+
"""Check if declaration has UPPERCASE identifier.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
parent_node: Declaration parent node
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if identifier is UPPERCASE
|
|
174
|
+
"""
|
|
175
|
+
identifier_node = self._find_identifier_in_declaration(parent_node)
|
|
176
|
+
if identifier_node is None:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
identifier_text = self.extract_node_text(identifier_node)
|
|
180
|
+
return self._is_uppercase_constant(identifier_text)
|
|
181
|
+
|
|
182
|
+
def _find_identifier_in_declaration(self, node: Node) -> Node | None:
|
|
183
|
+
"""Find identifier node in variable declaration.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
node: Variable declarator or lexical declaration node
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Identifier node or None
|
|
190
|
+
"""
|
|
191
|
+
# Walk children looking for identifier
|
|
192
|
+
for child in node.children:
|
|
193
|
+
if child.type in ("identifier", "property_identifier"):
|
|
194
|
+
return child
|
|
195
|
+
# Recursively check children
|
|
196
|
+
result = self._find_identifier_in_declaration(child)
|
|
197
|
+
if result is not None:
|
|
198
|
+
return result
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def _is_uppercase_constant(self, name: str) -> bool:
|
|
202
|
+
"""Check if identifier is UPPERCASE constant style.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
name: Identifier name
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if name is UPPERCASE with optional underscores
|
|
209
|
+
"""
|
|
210
|
+
if not name:
|
|
211
|
+
return False
|
|
212
|
+
# Must be at least one letter and all letters must be uppercase
|
|
213
|
+
# Allow underscores and numbers
|
|
214
|
+
letters_only = "".join(c for c in name if c.isalpha())
|
|
215
|
+
if not letters_only:
|
|
216
|
+
return False
|
|
217
|
+
return letters_only.isupper()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Builds Violation objects for magic number detection
|
|
3
|
+
|
|
4
|
+
Scope: Violation message construction for magic numbers linter
|
|
5
|
+
|
|
6
|
+
Overview: Provides ViolationBuilder class that creates Violation objects for magic number detections.
|
|
7
|
+
Generates helpful, descriptive messages suggesting constant extraction for numeric literals.
|
|
8
|
+
Constructs complete Violation instances with rule_id, file_path, line number, column, message,
|
|
9
|
+
and suggestions. Formats messages to mention the specific numeric value and encourage using
|
|
10
|
+
named constants for better code maintainability and readability. Provides consistent violation
|
|
11
|
+
structure across all magic number detections.
|
|
12
|
+
|
|
13
|
+
Dependencies: src.core.types for Violation dataclass, pathlib for Path handling, ast for node types
|
|
14
|
+
|
|
15
|
+
Exports: ViolationBuilder class
|
|
16
|
+
|
|
17
|
+
Interfaces: ViolationBuilder.create_violation(node, value, line, file_path) -> Violation,
|
|
18
|
+
builds complete Violation object with all required fields
|
|
19
|
+
|
|
20
|
+
Implementation: Message template with value interpolation, structured violation construction
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from src.core.types import Violation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ViolationBuilder:
|
|
30
|
+
"""Builds violations for magic number detections."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, rule_id: str) -> None:
|
|
33
|
+
"""Initialize the violation builder.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
rule_id: The rule ID to use in violations
|
|
37
|
+
"""
|
|
38
|
+
self.rule_id = rule_id
|
|
39
|
+
|
|
40
|
+
def create_violation(
|
|
41
|
+
self,
|
|
42
|
+
node: ast.Constant,
|
|
43
|
+
value: int | float,
|
|
44
|
+
line: int,
|
|
45
|
+
file_path: Path | None,
|
|
46
|
+
) -> Violation:
|
|
47
|
+
"""Create a violation for a magic number.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
node: The AST node containing the magic number
|
|
51
|
+
value: The numeric value
|
|
52
|
+
line: Line number where the violation occurs
|
|
53
|
+
file_path: Path to the file
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Violation object with details about the magic number
|
|
57
|
+
"""
|
|
58
|
+
message = f"Magic number {value} should be a named constant"
|
|
59
|
+
|
|
60
|
+
suggestion = f"Extract {value} to a named constant (e.g., CONSTANT_NAME = {value})"
|
|
61
|
+
|
|
62
|
+
return Violation(
|
|
63
|
+
rule_id=self.rule_id,
|
|
64
|
+
file_path=str(file_path) if file_path else "",
|
|
65
|
+
line=line,
|
|
66
|
+
column=node.col_offset if hasattr(node, "col_offset") else 0,
|
|
67
|
+
message=message,
|
|
68
|
+
suggestion=suggestion,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def create_typescript_violation(
|
|
72
|
+
self,
|
|
73
|
+
value: int | float,
|
|
74
|
+
line: int,
|
|
75
|
+
file_path: Path | None,
|
|
76
|
+
) -> Violation:
|
|
77
|
+
"""Create a violation for a TypeScript 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 = f"Extract {value} to a named constant (e.g., const CONSTANT_NAME = {value})"
|
|
90
|
+
|
|
91
|
+
return Violation(
|
|
92
|
+
rule_id=self.rule_id,
|
|
93
|
+
file_path=str(file_path) if file_path else "",
|
|
94
|
+
line=line,
|
|
95
|
+
column=0, # Tree-sitter nodes don't have easy column access
|
|
96
|
+
message=message,
|
|
97
|
+
suggestion=suggestion,
|
|
98
|
+
)
|
src/linters/nesting/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ Implementation: Simple re-export pattern for package interface, convenience func
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
-
from .config import NestingConfig
|
|
25
|
+
from .config import DEFAULT_MAX_NESTING_DEPTH, NestingConfig
|
|
26
26
|
from .linter import NestingDepthRule
|
|
27
27
|
from .python_analyzer import PythonNestingAnalyzer
|
|
28
28
|
from .typescript_analyzer import TypeScriptNestingAnalyzer
|
|
@@ -36,7 +36,11 @@ __all__ = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def lint(
|
|
39
|
+
def lint(
|
|
40
|
+
path: Path | str,
|
|
41
|
+
config: dict[str, Any] | None = None,
|
|
42
|
+
max_depth: int = DEFAULT_MAX_NESTING_DEPTH,
|
|
43
|
+
) -> list:
|
|
40
44
|
"""Lint a file or directory for nesting depth violations.
|
|
41
45
|
|
|
42
46
|
Args:
|
src/linters/nesting/config.py
CHANGED
|
@@ -21,12 +21,15 @@ Implementation: Dataclass with validation and defaults, matches reference implem
|
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
# Default nesting threshold constant
|
|
25
|
+
DEFAULT_MAX_NESTING_DEPTH = 4
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
@dataclass
|
|
26
29
|
class NestingConfig:
|
|
27
30
|
"""Configuration for nesting depth linter."""
|
|
28
31
|
|
|
29
|
-
max_nesting_depth: int =
|
|
32
|
+
max_nesting_depth: int = DEFAULT_MAX_NESTING_DEPTH # Default from reference implementation
|
|
30
33
|
enabled: bool = True
|
|
31
34
|
|
|
32
35
|
def __post_init__(self) -> None:
|
|
@@ -49,10 +52,10 @@ class NestingConfig:
|
|
|
49
52
|
if language and language in config:
|
|
50
53
|
lang_config = config[language]
|
|
51
54
|
max_nesting_depth = lang_config.get(
|
|
52
|
-
"max_nesting_depth", config.get("max_nesting_depth",
|
|
55
|
+
"max_nesting_depth", config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
|
|
53
56
|
)
|
|
54
57
|
else:
|
|
55
|
-
max_nesting_depth = config.get("max_nesting_depth",
|
|
58
|
+
max_nesting_depth = config.get("max_nesting_depth", DEFAULT_MAX_NESTING_DEPTH)
|
|
56
59
|
|
|
57
60
|
return cls(
|
|
58
61
|
max_nesting_depth=max_nesting_depth,
|
src/linters/nesting/linter.py
CHANGED
|
@@ -21,8 +21,8 @@ Implementation: Composition pattern with helper classes, AST-based analysis with
|
|
|
21
21
|
import ast
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
-
from src.core.base import BaseLintContext,
|
|
25
|
-
from src.core.linter_utils import
|
|
24
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
25
|
+
from src.core.linter_utils import load_linter_config
|
|
26
26
|
from src.core.types import Violation
|
|
27
27
|
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ from .typescript_analyzer import TypeScriptNestingAnalyzer
|
|
|
32
32
|
from .violation_builder import NestingViolationBuilder
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
class NestingDepthRule(
|
|
35
|
+
class NestingDepthRule(MultiLanguageLintRule):
|
|
36
36
|
"""Detects excessive nesting depth in functions."""
|
|
37
37
|
|
|
38
38
|
def __init__(self) -> None:
|
|
@@ -55,27 +55,16 @@ class NestingDepthRule(BaseLintRule):
|
|
|
55
55
|
"""Description of what this rule checks."""
|
|
56
56
|
return "Functions should not have excessive nesting depth for better readability"
|
|
57
57
|
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
58
|
+
def _load_config(self, context: BaseLintContext) -> NestingConfig:
|
|
59
|
+
"""Load configuration from context.
|
|
60
60
|
|
|
61
61
|
Args:
|
|
62
|
-
context: Lint context
|
|
62
|
+
context: Lint context
|
|
63
63
|
|
|
64
64
|
Returns:
|
|
65
|
-
|
|
65
|
+
NestingConfig instance
|
|
66
66
|
"""
|
|
67
|
-
|
|
68
|
-
return []
|
|
69
|
-
|
|
70
|
-
config = load_linter_config(context, "nesting", NestingConfig)
|
|
71
|
-
if not config.enabled:
|
|
72
|
-
return []
|
|
73
|
-
|
|
74
|
-
if context.language == "python":
|
|
75
|
-
return self._check_python(context, config)
|
|
76
|
-
if context.language in ("typescript", "javascript"):
|
|
77
|
-
return self._check_typescript(context, config)
|
|
78
|
-
return []
|
|
67
|
+
return load_linter_config(context, "nesting", NestingConfig)
|
|
79
68
|
|
|
80
69
|
def _process_python_functions(
|
|
81
70
|
self, functions: list, analyzer: Any, config: NestingConfig, context: BaseLintContext
|
src/linters/srp/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ Implementation: Simple re-export pattern for package interface, convenience func
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
-
from .config import SRPConfig
|
|
25
|
+
from .config import DEFAULT_MAX_LOC_PER_CLASS, DEFAULT_MAX_METHODS_PER_CLASS, SRPConfig
|
|
26
26
|
from .linter import SRPRule
|
|
27
27
|
from .python_analyzer import PythonSRPAnalyzer
|
|
28
28
|
from .typescript_analyzer import TypeScriptSRPAnalyzer
|
|
@@ -39,8 +39,8 @@ __all__ = [
|
|
|
39
39
|
def lint(
|
|
40
40
|
path: Path | str,
|
|
41
41
|
config: dict[str, Any] | None = None,
|
|
42
|
-
max_methods: int =
|
|
43
|
-
max_loc: int =
|
|
42
|
+
max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS,
|
|
43
|
+
max_loc: int = DEFAULT_MAX_LOC_PER_CLASS,
|
|
44
44
|
) -> list:
|
|
45
45
|
"""Lint a file or directory for SRP violations.
|
|
46
46
|
|
src/linters/srp/config.py
CHANGED
|
@@ -23,13 +23,17 @@ Implementation: Dataclass with validation and defaults, heuristic-based SRP dete
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
# Default SRP threshold constants
|
|
27
|
+
DEFAULT_MAX_METHODS_PER_CLASS = 7
|
|
28
|
+
DEFAULT_MAX_LOC_PER_CLASS = 200
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class SRPConfig:
|
|
29
33
|
"""Configuration for SRP linter."""
|
|
30
34
|
|
|
31
|
-
max_methods: int =
|
|
32
|
-
max_loc: int =
|
|
35
|
+
max_methods: int = DEFAULT_MAX_METHODS_PER_CLASS # Maximum methods per class
|
|
36
|
+
max_loc: int = DEFAULT_MAX_LOC_PER_CLASS # Maximum lines of code per class
|
|
33
37
|
enabled: bool = True
|
|
34
38
|
check_keywords: bool = True
|
|
35
39
|
keywords: list[str] = field(
|
|
@@ -58,11 +62,13 @@ class SRPConfig:
|
|
|
58
62
|
# Get language-specific config if available
|
|
59
63
|
if language and language in config:
|
|
60
64
|
lang_config = config[language]
|
|
61
|
-
max_methods = lang_config.get(
|
|
62
|
-
|
|
65
|
+
max_methods = lang_config.get(
|
|
66
|
+
"max_methods", config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
|
|
67
|
+
)
|
|
68
|
+
max_loc = lang_config.get("max_loc", config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS))
|
|
63
69
|
else:
|
|
64
|
-
max_methods = config.get("max_methods",
|
|
65
|
-
max_loc = config.get("max_loc",
|
|
70
|
+
max_methods = config.get("max_methods", DEFAULT_MAX_METHODS_PER_CLASS)
|
|
71
|
+
max_loc = config.get("max_loc", DEFAULT_MAX_LOC_PER_CLASS)
|
|
66
72
|
|
|
67
73
|
return cls(
|
|
68
74
|
max_methods=max_methods,
|
src/linters/srp/linter.py
CHANGED
|
@@ -18,8 +18,8 @@ Interfaces: SRPRule.check(context) -> list[Violation], properties for rule metad
|
|
|
18
18
|
Implementation: Composition pattern with helper classes, heuristic-based SRP analysis
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
from src.core.base import BaseLintContext,
|
|
22
|
-
from src.core.linter_utils import
|
|
21
|
+
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
22
|
+
from src.core.linter_utils import load_linter_config
|
|
23
23
|
from src.core.types import Violation
|
|
24
24
|
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ from .metrics_evaluator import evaluate_metrics
|
|
|
29
29
|
from .violation_builder import ViolationBuilder
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class SRPRule(
|
|
32
|
+
class SRPRule(MultiLanguageLintRule):
|
|
33
33
|
"""Detects Single Responsibility Principle violations in classes."""
|
|
34
34
|
|
|
35
35
|
def __init__(self) -> None:
|
|
@@ -54,7 +54,9 @@ class SRPRule(BaseLintRule):
|
|
|
54
54
|
return "Classes should have a single, well-defined responsibility"
|
|
55
55
|
|
|
56
56
|
def check(self, context: BaseLintContext) -> list[Violation]:
|
|
57
|
-
"""Check for SRP violations.
|
|
57
|
+
"""Check for SRP violations with custom ignore pattern handling.
|
|
58
|
+
|
|
59
|
+
Overrides parent to add file-level ignore pattern checking before dispatch.
|
|
58
60
|
|
|
59
61
|
Args:
|
|
60
62
|
context: Lint context with file information
|
|
@@ -62,39 +64,33 @@ class SRPRule(BaseLintRule):
|
|
|
62
64
|
Returns:
|
|
63
65
|
List of violations found
|
|
64
66
|
"""
|
|
65
|
-
|
|
66
|
-
return []
|
|
67
|
+
from src.core.linter_utils import has_file_content
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
if not self._is_linter_enabled(context, config):
|
|
69
|
+
if not has_file_content(context):
|
|
70
70
|
return []
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"""Check if file has content to analyze.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
context: Lint context
|
|
72
|
+
config = self._load_config(context)
|
|
73
|
+
if not self._should_process_file(context, config):
|
|
74
|
+
return []
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"""
|
|
83
|
-
return has_file_content(context)
|
|
76
|
+
# Standard language dispatch
|
|
77
|
+
return self._dispatch_by_language(context, config)
|
|
84
78
|
|
|
85
|
-
def
|
|
86
|
-
"""Check if
|
|
79
|
+
def _should_process_file(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
80
|
+
"""Check if file should be processed.
|
|
87
81
|
|
|
88
82
|
Args:
|
|
89
83
|
context: Lint context
|
|
90
84
|
config: SRP configuration
|
|
91
85
|
|
|
92
86
|
Returns:
|
|
93
|
-
True if
|
|
87
|
+
True if file should be processed
|
|
94
88
|
"""
|
|
95
|
-
|
|
89
|
+
if not config.enabled:
|
|
90
|
+
return False
|
|
91
|
+
return not self._is_file_ignored(context, config)
|
|
96
92
|
|
|
97
|
-
def
|
|
93
|
+
def _dispatch_by_language(self, context: BaseLintContext, config: SRPConfig) -> list[Violation]:
|
|
98
94
|
"""Dispatch to language-specific checker.
|
|
99
95
|
|
|
100
96
|
Args:
|
|
@@ -106,10 +102,23 @@ class SRPRule(BaseLintRule):
|
|
|
106
102
|
"""
|
|
107
103
|
if context.language == "python":
|
|
108
104
|
return self._check_python(context, config)
|
|
105
|
+
|
|
109
106
|
if context.language in ("typescript", "javascript"):
|
|
110
107
|
return self._check_typescript(context, config)
|
|
108
|
+
|
|
111
109
|
return []
|
|
112
110
|
|
|
111
|
+
def _load_config(self, context: BaseLintContext) -> SRPConfig:
|
|
112
|
+
"""Load configuration from context.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
context: Lint context
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
SRPConfig instance
|
|
119
|
+
"""
|
|
120
|
+
return load_linter_config(context, "srp", SRPConfig)
|
|
121
|
+
|
|
113
122
|
def _is_file_ignored(self, context: BaseLintContext, config: SRPConfig) -> bool:
|
|
114
123
|
"""Check if file matches ignore patterns.
|
|
115
124
|
|