thailint 0.9.0__py3-none-any.whl → 0.11.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/__init__.py +1 -0
- src/cli/__init__.py +27 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +478 -0
- src/cli/linters/__init__.py +58 -0
- src/cli/linters/code_patterns.py +372 -0
- src/cli/linters/code_smells.py +343 -0
- src/cli/linters/documentation.py +155 -0
- src/cli/linters/shared.py +89 -0
- src/cli/linters/structure.py +313 -0
- src/cli/linters/structure_quality.py +316 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +375 -0
- src/cli_main.py +34 -0
- src/config.py +2 -3
- src/core/rule_discovery.py +43 -10
- src/core/types.py +13 -0
- src/core/violation_utils.py +69 -0
- src/linter_config/ignore.py +32 -16
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/config.py +63 -0
- src/linters/collection_pipeline/continue_analyzer.py +100 -0
- src/linters/collection_pipeline/detector.py +130 -0
- src/linters/collection_pipeline/linter.py +437 -0
- src/linters/collection_pipeline/suggestion_builder.py +63 -0
- src/linters/dry/block_filter.py +99 -9
- src/linters/dry/cache.py +94 -6
- src/linters/dry/config.py +47 -10
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +214 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/linter.py +89 -48
- src/linters/dry/python_analyzer.py +44 -431
- src/linters/dry/python_constant_extractor.py +101 -0
- src/linters/dry/single_statement_detector.py +415 -0
- src/linters/dry/token_hasher.py +5 -5
- src/linters/dry/typescript_analyzer.py +63 -382
- src/linters/dry/typescript_constant_extractor.py +134 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +66 -0
- src/linters/file_header/linter.py +9 -13
- src/linters/file_placement/linter.py +30 -10
- src/linters/file_placement/pattern_matcher.py +19 -5
- src/linters/magic_numbers/linter.py +8 -67
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/nesting/linter.py +12 -9
- src/linters/print_statements/linter.py +7 -24
- src/linters/srp/class_analyzer.py +9 -9
- src/linters/srp/heuristics.py +6 -5
- src/linters/srp/linter.py +4 -5
- src/linters/stateless_class/linter.py +2 -2
- src/linters/stringly_typed/__init__.py +23 -0
- src/linters/stringly_typed/config.py +165 -0
- src/linters/stringly_typed/python/__init__.py +29 -0
- src/linters/stringly_typed/python/analyzer.py +198 -0
- src/linters/stringly_typed/python/condition_extractor.py +131 -0
- src/linters/stringly_typed/python/conditional_detector.py +176 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +88 -0
- src/linters/stringly_typed/python/validation_detector.py +186 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/orchestrator/core.py +241 -12
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/METADATA +116 -3
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/RECORD +67 -29
- thailint-0.11.0.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -2014
- thailint-0.9.0.dist-info/entry_points.txt +0 -4
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/WHEEL +0 -0
- {thailint-0.9.0.dist-info → thailint-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detects single-statement patterns in TypeScript/JavaScript code for DRY linter filtering
|
|
3
|
+
|
|
4
|
+
Scope: Tree-sitter AST analysis to identify single logical statements that should not be flagged
|
|
5
|
+
|
|
6
|
+
Overview: Provides sophisticated single-statement pattern detection to filter false positives in the
|
|
7
|
+
DRY linter for TypeScript/JavaScript code. Uses tree-sitter AST to identify when a code block
|
|
8
|
+
represents a single logical statement (decorators, call expressions, object literals, class fields,
|
|
9
|
+
JSX elements, interface definitions) that should not be flagged as duplicate code.
|
|
10
|
+
|
|
11
|
+
Dependencies: tree-sitter for TypeScript AST parsing
|
|
12
|
+
|
|
13
|
+
Exports: TypeScriptStatementDetector class
|
|
14
|
+
|
|
15
|
+
Interfaces: TypeScriptStatementDetector.is_single_statement(content, start_line, end_line) -> bool
|
|
16
|
+
|
|
17
|
+
Implementation: Tree-sitter AST walking with pattern matching for TypeScript constructs
|
|
18
|
+
|
|
19
|
+
SRP Exception: TypeScriptStatementDetector has 20 methods (exceeds max 8 methods)
|
|
20
|
+
Justification: Complex tree-sitter AST analysis algorithm for single-statement pattern detection.
|
|
21
|
+
Methods form tightly coupled algorithm pipeline: decorator detection, call expression analysis,
|
|
22
|
+
declaration patterns, JSX element handling, class body field definitions, and interface filtering.
|
|
23
|
+
Similar to parser or compiler pass architecture where algorithmic cohesion is critical. Splitting
|
|
24
|
+
would fragment the algorithm logic and make maintenance harder by separating interdependent
|
|
25
|
+
tree-sitter AST analysis steps. All methods contribute to single responsibility: accurately
|
|
26
|
+
detecting single-statement patterns to prevent false positives in TypeScript duplicate detection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from collections.abc import Generator
|
|
30
|
+
|
|
31
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
|
|
32
|
+
|
|
33
|
+
if TREE_SITTER_AVAILABLE:
|
|
34
|
+
from tree_sitter import Node
|
|
35
|
+
else:
|
|
36
|
+
Node = None # type: ignore[assignment,misc] # pylint: disable=invalid-name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TypeScriptStatementDetector: # thailint: ignore[srp.violation]
|
|
40
|
+
"""Detects single-statement patterns in TypeScript/JavaScript for duplicate filtering.
|
|
41
|
+
|
|
42
|
+
SRP suppression: Complex tree-sitter AST analysis algorithm requires 20 methods to implement
|
|
43
|
+
sophisticated single-statement detection. See file header for justification.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def is_single_statement(self, content: str, start_line: int, end_line: int) -> bool:
|
|
47
|
+
"""Check if a line range is a single logical statement.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
content: TypeScript source code
|
|
51
|
+
start_line: Starting line number (1-indexed)
|
|
52
|
+
end_line: Ending line number (1-indexed)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if this range represents a single logical statement/expression
|
|
56
|
+
"""
|
|
57
|
+
if not TREE_SITTER_AVAILABLE:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
61
|
+
|
|
62
|
+
analyzer = TypeScriptBaseAnalyzer()
|
|
63
|
+
root = analyzer.parse_typescript(content)
|
|
64
|
+
if not root:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return self._check_overlapping_nodes(root, start_line, end_line)
|
|
68
|
+
|
|
69
|
+
def _check_overlapping_nodes(self, root: Node, start_line: int, end_line: int) -> bool:
|
|
70
|
+
"""Check if any AST node overlaps and matches single-statement pattern."""
|
|
71
|
+
ts_start = start_line - 1 # Convert to 0-indexed
|
|
72
|
+
ts_end = end_line - 1
|
|
73
|
+
|
|
74
|
+
for node in self._walk_nodes(root):
|
|
75
|
+
if self._node_overlaps_and_matches(node, ts_start, ts_end):
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def _walk_nodes(self, node: Node) -> Generator[Node, None, None]:
|
|
80
|
+
"""Generator to walk all nodes in tree."""
|
|
81
|
+
yield node
|
|
82
|
+
for child in node.children:
|
|
83
|
+
yield from self._walk_nodes(child)
|
|
84
|
+
|
|
85
|
+
def _node_overlaps_and_matches(self, node: Node, ts_start: int, ts_end: int) -> bool:
|
|
86
|
+
"""Check if node overlaps with range and matches single-statement pattern."""
|
|
87
|
+
node_start = node.start_point[0]
|
|
88
|
+
node_end = node.end_point[0]
|
|
89
|
+
|
|
90
|
+
overlaps = not (node_end < ts_start or node_start > ts_end)
|
|
91
|
+
if not overlaps:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
return self._is_single_statement_pattern(node, ts_start, ts_end)
|
|
95
|
+
|
|
96
|
+
def _is_single_statement_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
|
|
97
|
+
"""Check if an AST node represents a single-statement pattern to filter."""
|
|
98
|
+
node_start = node.start_point[0]
|
|
99
|
+
node_end = node.end_point[0]
|
|
100
|
+
contains = (node_start <= ts_start) and (node_end >= ts_end)
|
|
101
|
+
|
|
102
|
+
matchers = [
|
|
103
|
+
self._matches_simple_container_pattern(node, contains),
|
|
104
|
+
self._matches_call_expression_pattern(node, ts_start, ts_end, contains),
|
|
105
|
+
self._matches_declaration_pattern(node, contains),
|
|
106
|
+
self._matches_jsx_pattern(node, contains),
|
|
107
|
+
self._matches_class_body_pattern(node, ts_start, ts_end),
|
|
108
|
+
]
|
|
109
|
+
return any(matchers)
|
|
110
|
+
|
|
111
|
+
def _matches_simple_container_pattern(self, node: Node, contains: bool) -> bool:
|
|
112
|
+
"""Check if node is a simple container pattern (decorator, object, etc.)."""
|
|
113
|
+
simple_types = (
|
|
114
|
+
"decorator",
|
|
115
|
+
"object",
|
|
116
|
+
"member_expression",
|
|
117
|
+
"as_expression",
|
|
118
|
+
"array_pattern",
|
|
119
|
+
)
|
|
120
|
+
return node.type in simple_types and contains
|
|
121
|
+
|
|
122
|
+
def _matches_call_expression_pattern(
|
|
123
|
+
self, node: Node, ts_start: int, ts_end: int, contains: bool
|
|
124
|
+
) -> bool:
|
|
125
|
+
"""Check if node is a call expression pattern."""
|
|
126
|
+
if node.type != "call_expression":
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
node_start = node.start_point[0]
|
|
130
|
+
node_end = node.end_point[0]
|
|
131
|
+
is_multiline = node_start < node_end
|
|
132
|
+
if is_multiline and node_start <= ts_start <= node_end:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
return contains
|
|
136
|
+
|
|
137
|
+
def _matches_declaration_pattern(self, node: Node, contains: bool) -> bool:
|
|
138
|
+
"""Check if node is a lexical declaration pattern."""
|
|
139
|
+
if node.type != "lexical_declaration" or not contains:
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
if self._contains_function_body(node):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
def _matches_jsx_pattern(self, node: Node, contains: bool) -> bool:
|
|
148
|
+
"""Check if node is a JSX element pattern."""
|
|
149
|
+
jsx_types = ("jsx_opening_element", "jsx_self_closing_element")
|
|
150
|
+
return node.type in jsx_types and contains
|
|
151
|
+
|
|
152
|
+
def _matches_class_body_pattern(self, node: Node, ts_start: int, ts_end: int) -> bool:
|
|
153
|
+
"""Check if node is a class body field definition pattern."""
|
|
154
|
+
if node.type != "class_body":
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
return self._is_in_class_field_area(node, ts_start, ts_end)
|
|
158
|
+
|
|
159
|
+
def _contains_function_body(self, node: Node) -> bool:
|
|
160
|
+
"""Check if node contains an arrow function or function expression."""
|
|
161
|
+
for child in node.children:
|
|
162
|
+
if child.type in ("arrow_function", "function", "function_expression"):
|
|
163
|
+
return True
|
|
164
|
+
if self._contains_function_body(child):
|
|
165
|
+
return True
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def _find_first_method_line(self, class_body: Node) -> int | None:
|
|
169
|
+
"""Find line number of first method in class body."""
|
|
170
|
+
for child in class_body.children:
|
|
171
|
+
if child.type in ("method_definition", "function_declaration"):
|
|
172
|
+
return child.start_point[0]
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _is_in_class_field_area(self, class_body: Node, ts_start: int, ts_end: int) -> bool:
|
|
176
|
+
"""Check if range is in class field definition area (before methods)."""
|
|
177
|
+
first_method_line = self._find_first_method_line(class_body)
|
|
178
|
+
class_start = class_body.start_point[0]
|
|
179
|
+
class_end = class_body.end_point[0]
|
|
180
|
+
|
|
181
|
+
if first_method_line is None:
|
|
182
|
+
return class_start <= ts_start and class_end >= ts_end
|
|
183
|
+
|
|
184
|
+
return class_start <= ts_start and ts_end < first_method_line
|
|
185
|
+
|
|
186
|
+
def should_include_block(self, content: str, start_line: int, end_line: int) -> bool:
|
|
187
|
+
"""Check if block should be included (not overlapping interface definitions).
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
content: File content
|
|
191
|
+
start_line: Block start line
|
|
192
|
+
end_line: Block end line
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
False if block overlaps interface definition, True otherwise
|
|
196
|
+
"""
|
|
197
|
+
interface_ranges = self._find_interface_ranges(content)
|
|
198
|
+
return not self._overlaps_interface(start_line, end_line, interface_ranges)
|
|
199
|
+
|
|
200
|
+
def _find_interface_ranges(self, content: str) -> list[tuple[int, int]]:
|
|
201
|
+
"""Find line ranges of interface/type definitions."""
|
|
202
|
+
ranges: list[tuple[int, int]] = []
|
|
203
|
+
lines = content.split("\n")
|
|
204
|
+
state = {"in_interface": False, "start_line": 0, "brace_count": 0}
|
|
205
|
+
|
|
206
|
+
for i, line in enumerate(lines, start=1):
|
|
207
|
+
stripped = line.strip()
|
|
208
|
+
self._process_line_for_interface(stripped, i, state, ranges)
|
|
209
|
+
|
|
210
|
+
return ranges
|
|
211
|
+
|
|
212
|
+
def _process_line_for_interface(
|
|
213
|
+
self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Process single line for interface detection."""
|
|
216
|
+
if self._is_interface_start(stripped):
|
|
217
|
+
self._handle_interface_start(stripped, line_num, state, ranges)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
if state["in_interface"]:
|
|
221
|
+
self._handle_interface_continuation(stripped, line_num, state, ranges)
|
|
222
|
+
|
|
223
|
+
def _is_interface_start(self, stripped: str) -> bool:
|
|
224
|
+
"""Check if line starts interface/type definition."""
|
|
225
|
+
return stripped.startswith(("interface ", "type ")) and "{" in stripped
|
|
226
|
+
|
|
227
|
+
def _handle_interface_start(
|
|
228
|
+
self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Handle start of interface definition."""
|
|
231
|
+
state["in_interface"] = True
|
|
232
|
+
state["start_line"] = line_num
|
|
233
|
+
state["brace_count"] = stripped.count("{") - stripped.count("}")
|
|
234
|
+
|
|
235
|
+
if state["brace_count"] == 0:
|
|
236
|
+
ranges.append((line_num, line_num))
|
|
237
|
+
state["in_interface"] = False
|
|
238
|
+
|
|
239
|
+
def _handle_interface_continuation(
|
|
240
|
+
self, stripped: str, line_num: int, state: dict, ranges: list[tuple[int, int]]
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Handle continuation of interface definition."""
|
|
243
|
+
state["brace_count"] += stripped.count("{") - stripped.count("}")
|
|
244
|
+
if state["brace_count"] == 0:
|
|
245
|
+
ranges.append((state["start_line"], line_num))
|
|
246
|
+
state["in_interface"] = False
|
|
247
|
+
|
|
248
|
+
def _overlaps_interface(
|
|
249
|
+
self, start: int, end: int, interface_ranges: list[tuple[int, int]]
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""Check if block overlaps with any interface range."""
|
|
252
|
+
for if_start, if_end in interface_ranges:
|
|
253
|
+
if start <= if_end and end >= if_start:
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Extract value representations from TypeScript AST nodes
|
|
3
|
+
|
|
4
|
+
Scope: Helper for TypeScript constant extraction to extract value strings
|
|
5
|
+
|
|
6
|
+
Overview: Provides utility methods to extract string representations from tree-sitter AST nodes
|
|
7
|
+
for TypeScript value types (numbers, strings, booleans, arrays, objects, call expressions).
|
|
8
|
+
Used by TypeScriptConstantExtractor to get value context for duplicate constant detection.
|
|
9
|
+
|
|
10
|
+
Dependencies: tree-sitter, tree-sitter-typescript, src.analyzers.typescript_base
|
|
11
|
+
|
|
12
|
+
Exports: TypeScriptValueExtractor class
|
|
13
|
+
|
|
14
|
+
Interfaces: TypeScriptValueExtractor.get_value_string(node, content) -> str | None
|
|
15
|
+
|
|
16
|
+
Implementation: Tree-sitter node traversal with type-specific string formatting
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
|
|
22
|
+
|
|
23
|
+
if TREE_SITTER_AVAILABLE:
|
|
24
|
+
from tree_sitter import Node
|
|
25
|
+
else:
|
|
26
|
+
Node = Any # type: ignore[assignment,misc]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TypeScriptValueExtractor:
|
|
30
|
+
"""Extracts value representations from TypeScript AST nodes."""
|
|
31
|
+
|
|
32
|
+
# Types that return their literal text
|
|
33
|
+
LITERAL_TYPES = frozenset(("number", "string", "true", "false", "null", "identifier"))
|
|
34
|
+
|
|
35
|
+
# Types with fixed representations
|
|
36
|
+
FIXED_REPRESENTATIONS = {"array": "[...]", "object": "{...}"}
|
|
37
|
+
|
|
38
|
+
def get_node_text(self, node: Node, content: str) -> str:
|
|
39
|
+
"""Get text content of a node."""
|
|
40
|
+
return content[node.start_byte : node.end_byte]
|
|
41
|
+
|
|
42
|
+
def get_value_string(self, node: Node, content: str) -> str | None:
|
|
43
|
+
"""Get string representation of a value node."""
|
|
44
|
+
if node.type in self.LITERAL_TYPES:
|
|
45
|
+
return self.get_node_text(node, content)
|
|
46
|
+
if node.type in self.FIXED_REPRESENTATIONS:
|
|
47
|
+
return self.FIXED_REPRESENTATIONS[node.type]
|
|
48
|
+
if node.type == "call_expression":
|
|
49
|
+
return self._get_call_string(node, content)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def _get_call_string(self, node: Node, content: str) -> str:
|
|
53
|
+
"""Get string representation of a call expression.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
node: call_expression node
|
|
57
|
+
content: Original source content
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
String like "functionName(...)"
|
|
61
|
+
"""
|
|
62
|
+
for child in node.children:
|
|
63
|
+
if child.type == "identifier":
|
|
64
|
+
func_name = self.get_node_text(child, content)
|
|
65
|
+
return f"{func_name}(...)"
|
|
66
|
+
return "call(...)"
|
|
@@ -28,7 +28,7 @@ from typing import Protocol
|
|
|
28
28
|
from src.core.base import BaseLintContext, BaseLintRule
|
|
29
29
|
from src.core.linter_utils import load_linter_config
|
|
30
30
|
from src.core.types import Violation
|
|
31
|
-
from src.linter_config.ignore import
|
|
31
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
32
32
|
|
|
33
33
|
from .atemporal_detector import AtemporalDetector
|
|
34
34
|
from .bash_parser import BashHeaderParser
|
|
@@ -73,7 +73,7 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
73
73
|
def __init__(self) -> None:
|
|
74
74
|
"""Initialize the file header rule."""
|
|
75
75
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
76
|
-
self._ignore_parser =
|
|
76
|
+
self._ignore_parser = get_ignore_parser()
|
|
77
77
|
|
|
78
78
|
@property
|
|
79
79
|
def rule_id(self) -> str:
|
|
@@ -273,17 +273,13 @@ class FileHeaderRule(BaseLintRule): # thailint: ignore[srp]
|
|
|
273
273
|
file_content = context.file_content or ""
|
|
274
274
|
lines = file_content.splitlines()
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
filtered.append(v)
|
|
285
|
-
|
|
286
|
-
return filtered
|
|
276
|
+
non_ignored = (
|
|
277
|
+
v
|
|
278
|
+
for v in violations
|
|
279
|
+
if not self._ignore_parser.should_ignore_violation(v, file_content)
|
|
280
|
+
and not self._has_line_level_ignore(lines, v)
|
|
281
|
+
)
|
|
282
|
+
return list(non_ignored)
|
|
287
283
|
|
|
288
284
|
def _has_line_level_ignore(self, lines: list[str], violation: Violation) -> bool:
|
|
289
285
|
"""Check for thailint-ignore-line directive."""
|
|
@@ -124,20 +124,40 @@ class FilePlacementLinter:
|
|
|
124
124
|
Returns:
|
|
125
125
|
List of all violations found
|
|
126
126
|
"""
|
|
127
|
-
|
|
127
|
+
valid_files = self._get_valid_files(dir_path, recursive)
|
|
128
|
+
return self._lint_files(valid_files)
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
def _get_valid_files(self, dir_path: Path, recursive: bool) -> list[Path]:
|
|
131
|
+
"""Get list of valid files to lint from directory.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
dir_path: Directory to scan
|
|
135
|
+
recursive: Scan recursively
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of file paths to lint
|
|
139
|
+
"""
|
|
140
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
141
|
+
|
|
142
|
+
ignore_parser = get_ignore_parser(self.project_root)
|
|
130
143
|
pattern = "**/*" if recursive else "*"
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
return [
|
|
146
|
+
f for f in dir_path.glob(pattern) if f.is_file() and not ignore_parser.is_ignored(f)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
def _lint_files(self, file_paths: list[Path]) -> list[Violation]:
|
|
150
|
+
"""Lint multiple files and collect violations.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
file_paths: List of file paths to lint
|
|
140
154
|
|
|
155
|
+
Returns:
|
|
156
|
+
List of all violations found
|
|
157
|
+
"""
|
|
158
|
+
violations = []
|
|
159
|
+
for file_path in file_paths:
|
|
160
|
+
violations.extend(self.lint_path(file_path))
|
|
141
161
|
return violations
|
|
142
162
|
|
|
143
163
|
|
|
@@ -18,14 +18,28 @@ Implementation: Uses re.search() for pattern matching with IGNORECASE flag
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
|
+
from re import Pattern
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class PatternMatcher:
|
|
24
25
|
"""Handles regex pattern matching for file paths."""
|
|
25
26
|
|
|
26
27
|
def __init__(self) -> None:
|
|
27
|
-
"""Initialize the pattern matcher."""
|
|
28
|
-
|
|
28
|
+
"""Initialize the pattern matcher with compiled regex cache."""
|
|
29
|
+
self._compiled_patterns: dict[str, Pattern[str]] = {}
|
|
30
|
+
|
|
31
|
+
def _get_compiled(self, pattern: str) -> Pattern[str]:
|
|
32
|
+
"""Get compiled regex pattern, caching for reuse.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
pattern: Regex pattern string
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Compiled regex Pattern object
|
|
39
|
+
"""
|
|
40
|
+
if pattern not in self._compiled_patterns:
|
|
41
|
+
self._compiled_patterns[pattern] = re.compile(pattern, re.IGNORECASE)
|
|
42
|
+
return self._compiled_patterns[pattern]
|
|
29
43
|
|
|
30
44
|
def match_deny_patterns(
|
|
31
45
|
self, path_str: str, deny_patterns: list[dict[str, str]]
|
|
@@ -40,8 +54,8 @@ class PatternMatcher:
|
|
|
40
54
|
Tuple of (is_denied, reason)
|
|
41
55
|
"""
|
|
42
56
|
for deny_item in deny_patterns:
|
|
43
|
-
|
|
44
|
-
if
|
|
57
|
+
compiled = self._get_compiled(deny_item["pattern"])
|
|
58
|
+
if compiled.search(path_str):
|
|
45
59
|
reason = deny_item.get("reason", "File not allowed in this location")
|
|
46
60
|
return True, reason
|
|
47
61
|
return False, None
|
|
@@ -56,4 +70,4 @@ class PatternMatcher:
|
|
|
56
70
|
Returns:
|
|
57
71
|
True if path matches any pattern
|
|
58
72
|
"""
|
|
59
|
-
return any(
|
|
73
|
+
return any(self._get_compiled(pattern).search(path_str) for pattern in allow_patterns)
|
|
@@ -29,12 +29,14 @@ from pathlib import Path
|
|
|
29
29
|
from src.core.base import BaseLintContext, MultiLanguageLintRule
|
|
30
30
|
from src.core.linter_utils import load_linter_config
|
|
31
31
|
from src.core.types import Violation
|
|
32
|
-
from src.
|
|
32
|
+
from src.core.violation_utils import get_violation_line, has_python_noqa
|
|
33
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
33
34
|
|
|
34
35
|
from .config import MagicNumberConfig
|
|
35
36
|
from .context_analyzer import ContextAnalyzer
|
|
36
37
|
from .python_analyzer import PythonMagicNumberAnalyzer
|
|
37
38
|
from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
|
|
39
|
+
from .typescript_ignore_checker import TypeScriptIgnoreChecker
|
|
38
40
|
from .violation_builder import ViolationBuilder
|
|
39
41
|
|
|
40
42
|
|
|
@@ -43,9 +45,10 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
43
45
|
|
|
44
46
|
def __init__(self) -> None:
|
|
45
47
|
"""Initialize the magic numbers rule."""
|
|
46
|
-
self._ignore_parser =
|
|
48
|
+
self._ignore_parser = get_ignore_parser()
|
|
47
49
|
self._violation_builder = ViolationBuilder(self.rule_id)
|
|
48
50
|
self._context_analyzer = ContextAnalyzer()
|
|
51
|
+
self._typescript_ignore_checker = TypeScriptIgnoreChecker()
|
|
49
52
|
|
|
50
53
|
@property
|
|
51
54
|
def rule_id(self) -> str:
|
|
@@ -282,28 +285,17 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
282
285
|
Returns:
|
|
283
286
|
True if line has generic ignore directive
|
|
284
287
|
"""
|
|
285
|
-
line_text =
|
|
288
|
+
line_text = get_violation_line(violation, context)
|
|
286
289
|
if line_text is None:
|
|
287
290
|
return False
|
|
288
291
|
|
|
289
292
|
return self._has_generic_ignore_directive(line_text)
|
|
290
293
|
|
|
291
|
-
def _get_violation_line(self, violation: Violation, context: BaseLintContext) -> str | None:
|
|
292
|
-
"""Get the line text for a violation."""
|
|
293
|
-
if not context.file_content:
|
|
294
|
-
return None
|
|
295
|
-
|
|
296
|
-
lines = context.file_content.splitlines()
|
|
297
|
-
if violation.line <= 0 or violation.line > len(lines):
|
|
298
|
-
return None
|
|
299
|
-
|
|
300
|
-
return lines[violation.line - 1].lower()
|
|
301
|
-
|
|
302
294
|
def _has_generic_ignore_directive(self, line_text: str) -> bool:
|
|
303
295
|
"""Check if line has generic ignore directive."""
|
|
304
296
|
if self._has_generic_thailint_ignore(line_text):
|
|
305
297
|
return True
|
|
306
|
-
return
|
|
298
|
+
return has_python_noqa(line_text)
|
|
307
299
|
|
|
308
300
|
def _has_generic_thailint_ignore(self, line_text: str) -> bool:
|
|
309
301
|
"""Check for generic thailint: ignore (no brackets)."""
|
|
@@ -312,10 +304,6 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
312
304
|
after_ignore = line_text.split("# thailint: ignore")[1].split("#")[0]
|
|
313
305
|
return "[" not in after_ignore
|
|
314
306
|
|
|
315
|
-
def _has_noqa_directive(self, line_text: str) -> bool:
|
|
316
|
-
"""Check for noqa-style comments."""
|
|
317
|
-
return "# noqa" in line_text
|
|
318
|
-
|
|
319
307
|
def _check_typescript(
|
|
320
308
|
self, context: BaseLintContext, config: MagicNumberConfig
|
|
321
309
|
) -> list[Violation]:
|
|
@@ -466,51 +454,4 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
|
|
|
466
454
|
Returns:
|
|
467
455
|
True if should ignore
|
|
468
456
|
"""
|
|
469
|
-
|
|
470
|
-
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
471
|
-
return True
|
|
472
|
-
|
|
473
|
-
# Check TypeScript-style comments
|
|
474
|
-
return self._check_typescript_ignore(violation, context)
|
|
475
|
-
|
|
476
|
-
def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
477
|
-
"""Check for TypeScript-style ignore directives.
|
|
478
|
-
|
|
479
|
-
Args:
|
|
480
|
-
violation: Violation to check
|
|
481
|
-
context: Lint context
|
|
482
|
-
|
|
483
|
-
Returns:
|
|
484
|
-
True if line has ignore directive
|
|
485
|
-
"""
|
|
486
|
-
line_text = self._get_violation_line(violation, context)
|
|
487
|
-
if line_text is None:
|
|
488
|
-
return False
|
|
489
|
-
|
|
490
|
-
# Check for // thailint: ignore or // noqa
|
|
491
|
-
return self._has_typescript_ignore_directive(line_text)
|
|
492
|
-
|
|
493
|
-
def _has_typescript_ignore_directive(self, line_text: str) -> bool:
|
|
494
|
-
"""Check if line has TypeScript-style ignore directive.
|
|
495
|
-
|
|
496
|
-
Args:
|
|
497
|
-
line_text: Line text to check
|
|
498
|
-
|
|
499
|
-
Returns:
|
|
500
|
-
True if has ignore directive
|
|
501
|
-
"""
|
|
502
|
-
# Check for // thailint: ignore[magic-numbers]
|
|
503
|
-
if "// thailint: ignore[magic-numbers]" in line_text:
|
|
504
|
-
return True
|
|
505
|
-
|
|
506
|
-
# Check for // thailint: ignore (generic)
|
|
507
|
-
if "// thailint: ignore" in line_text:
|
|
508
|
-
after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
|
|
509
|
-
if "[" not in after_ignore:
|
|
510
|
-
return True
|
|
511
|
-
|
|
512
|
-
# Check for // noqa
|
|
513
|
-
if "// noqa" in line_text:
|
|
514
|
-
return True
|
|
515
|
-
|
|
516
|
-
return False
|
|
457
|
+
return self._typescript_ignore_checker.should_ignore(violation, context)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: TypeScript-specific ignore directive checking for magic numbers linter
|
|
3
|
+
|
|
4
|
+
Scope: Ignore directive detection for TypeScript/JavaScript files
|
|
5
|
+
|
|
6
|
+
Overview: Provides ignore directive checking functionality specifically for TypeScript and JavaScript
|
|
7
|
+
files in the magic numbers linter. Handles both thailint-style and noqa-style ignore comments
|
|
8
|
+
using TypeScript comment syntax (// instead of #). Extracted from linter.py to reduce file
|
|
9
|
+
size and improve modularity.
|
|
10
|
+
|
|
11
|
+
Dependencies: IgnoreDirectiveParser from src.linter_config.ignore, Violation type, violation_utils
|
|
12
|
+
|
|
13
|
+
Exports: TypeScriptIgnoreChecker class
|
|
14
|
+
|
|
15
|
+
Interfaces: TypeScriptIgnoreChecker.should_ignore(violation, context) -> bool
|
|
16
|
+
|
|
17
|
+
Implementation: Comment parsing with TypeScript-specific syntax handling, uses shared utilities
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from src.core.base import BaseLintContext
|
|
21
|
+
from src.core.types import Violation
|
|
22
|
+
from src.core.violation_utils import get_violation_line, has_typescript_noqa
|
|
23
|
+
from src.linter_config.ignore import get_ignore_parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TypeScriptIgnoreChecker:
|
|
27
|
+
"""Checks for TypeScript-style ignore directives in magic numbers linter."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""Initialize with standard ignore parser."""
|
|
31
|
+
self._ignore_parser = get_ignore_parser()
|
|
32
|
+
|
|
33
|
+
def should_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
34
|
+
"""Check if TypeScript violation should be ignored.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
violation: Violation to check
|
|
38
|
+
context: Lint context
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if should ignore
|
|
42
|
+
"""
|
|
43
|
+
if self._ignore_parser.should_ignore_violation(violation, context.file_content or ""):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
return self._check_typescript_ignore(violation, context)
|
|
47
|
+
|
|
48
|
+
def _check_typescript_ignore(self, violation: Violation, context: BaseLintContext) -> bool:
|
|
49
|
+
"""Check for TypeScript-style ignore directives.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
violation: Violation to check
|
|
53
|
+
context: Lint context
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if line has ignore directive
|
|
57
|
+
"""
|
|
58
|
+
line_text = get_violation_line(violation, context)
|
|
59
|
+
if line_text is None:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
return self._has_typescript_ignore_directive(line_text)
|
|
63
|
+
|
|
64
|
+
def _has_typescript_ignore_directive(self, line_text: str) -> bool:
|
|
65
|
+
"""Check if line has TypeScript-style ignore directive.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
line_text: Line text to check
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if has ignore directive
|
|
72
|
+
"""
|
|
73
|
+
if "// thailint: ignore[magic-numbers]" in line_text:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if "// thailint: ignore" in line_text:
|
|
77
|
+
after_ignore = line_text.split("// thailint: ignore")[1].split("//")[0]
|
|
78
|
+
if "[" not in after_ignore:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
return has_typescript_noqa(line_text)
|