thailint 0.5.0__py3-none-any.whl → 0.15.3__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/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.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: is_single_statement, should_include_block functions
|
|
14
|
+
|
|
15
|
+
Interfaces: is_single_statement(content, start_line, end_line) -> bool,
|
|
16
|
+
should_include_block(content, start_line, end_line) -> bool
|
|
17
|
+
|
|
18
|
+
Implementation: Tree-sitter AST walking with pattern matching for TypeScript constructs
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Generator
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
|
|
28
|
+
|
|
29
|
+
if TREE_SITTER_AVAILABLE:
|
|
30
|
+
from tree_sitter import Node
|
|
31
|
+
else:
|
|
32
|
+
Node = Any # type: ignore[assignment,misc]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_single_statement(content: str, start_line: int, end_line: int) -> bool:
|
|
36
|
+
"""Check if a line range is a single logical statement.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
content: TypeScript source code
|
|
40
|
+
start_line: Starting line number (1-indexed)
|
|
41
|
+
end_line: Ending line number (1-indexed)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if this range represents a single logical statement/expression
|
|
45
|
+
"""
|
|
46
|
+
if not TREE_SITTER_AVAILABLE:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
from src.analyzers.typescript_base import TypeScriptBaseAnalyzer
|
|
50
|
+
|
|
51
|
+
analyzer = TypeScriptBaseAnalyzer()
|
|
52
|
+
root = analyzer.parse_typescript(content)
|
|
53
|
+
if not root:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
return _check_overlapping_nodes(root, start_line, end_line)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def should_include_block(content: str, start_line: int, end_line: int) -> bool:
|
|
60
|
+
"""Check if block should be included (not overlapping interface definitions).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
content: File content
|
|
64
|
+
start_line: Block start line
|
|
65
|
+
end_line: Block end line
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
False if block overlaps interface definition, True otherwise
|
|
69
|
+
"""
|
|
70
|
+
interface_ranges = _find_interface_ranges(content)
|
|
71
|
+
return not _overlaps_interface(start_line, end_line, interface_ranges)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _check_overlapping_nodes(root: Node, start_line: int, end_line: int) -> bool:
|
|
75
|
+
"""Check if any AST node overlaps and matches single-statement pattern."""
|
|
76
|
+
ts_start = start_line - 1 # Convert to 0-indexed
|
|
77
|
+
ts_end = end_line - 1
|
|
78
|
+
|
|
79
|
+
return any(_node_overlaps_and_matches(node, ts_start, ts_end) for node in _walk_nodes(root))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _walk_nodes(node: Node) -> Generator[Node, None, None]:
|
|
83
|
+
"""Generator to walk all nodes in tree."""
|
|
84
|
+
yield node
|
|
85
|
+
for child in node.children:
|
|
86
|
+
yield from _walk_nodes(child)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _node_overlaps_and_matches(node: Node, ts_start: int, ts_end: int) -> bool:
|
|
90
|
+
"""Check if node overlaps with range and matches single-statement pattern."""
|
|
91
|
+
node_start = node.start_point[0]
|
|
92
|
+
node_end = node.end_point[0]
|
|
93
|
+
|
|
94
|
+
overlaps = not (node_end < ts_start or node_start > ts_end)
|
|
95
|
+
if not overlaps:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
return _is_single_statement_pattern(node, ts_start, ts_end)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_single_statement_pattern(node: Node, ts_start: int, ts_end: int) -> bool:
|
|
102
|
+
"""Check if an AST node represents a single-statement pattern to filter."""
|
|
103
|
+
node_start = node.start_point[0]
|
|
104
|
+
node_end = node.end_point[0]
|
|
105
|
+
contains = (node_start <= ts_start) and (node_end >= ts_end)
|
|
106
|
+
|
|
107
|
+
matchers = [
|
|
108
|
+
_matches_simple_container_pattern(node, contains),
|
|
109
|
+
_matches_call_expression_pattern(node, ts_start, ts_end, contains),
|
|
110
|
+
_matches_declaration_pattern(node, contains),
|
|
111
|
+
_matches_jsx_pattern(node, contains),
|
|
112
|
+
_matches_class_body_pattern(node, ts_start, ts_end),
|
|
113
|
+
]
|
|
114
|
+
return any(matchers)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _matches_simple_container_pattern(node: Node, contains: bool) -> bool:
|
|
118
|
+
"""Check if node is a simple container pattern (decorator, object, etc.)."""
|
|
119
|
+
simple_types = (
|
|
120
|
+
"decorator",
|
|
121
|
+
"object",
|
|
122
|
+
"member_expression",
|
|
123
|
+
"as_expression",
|
|
124
|
+
"array_pattern",
|
|
125
|
+
)
|
|
126
|
+
return node.type in simple_types and contains
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _matches_call_expression_pattern(
|
|
130
|
+
node: Node, ts_start: int, ts_end: int, contains: bool
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Check if node is a call expression pattern."""
|
|
133
|
+
if node.type != "call_expression":
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
node_start = node.start_point[0]
|
|
137
|
+
node_end = node.end_point[0]
|
|
138
|
+
is_multiline = node_start < node_end
|
|
139
|
+
if is_multiline and node_start <= ts_start <= node_end:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
return contains
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _matches_declaration_pattern(node: Node, contains: bool) -> bool:
|
|
146
|
+
"""Check if node is a lexical declaration pattern."""
|
|
147
|
+
if node.type != "lexical_declaration" or not contains:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
if _contains_function_body(node):
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _matches_jsx_pattern(node: Node, contains: bool) -> bool:
|
|
157
|
+
"""Check if node is a JSX element pattern."""
|
|
158
|
+
jsx_types = ("jsx_opening_element", "jsx_self_closing_element")
|
|
159
|
+
return node.type in jsx_types and contains
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _matches_class_body_pattern(node: Node, ts_start: int, ts_end: int) -> bool:
|
|
163
|
+
"""Check if node is a class body field definition pattern."""
|
|
164
|
+
if node.type != "class_body":
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
return _is_in_class_field_area(node, ts_start, ts_end)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _contains_function_body(node: Node) -> bool:
|
|
171
|
+
"""Check if node contains an arrow function or function expression."""
|
|
172
|
+
for child in node.children:
|
|
173
|
+
if child.type in ("arrow_function", "function", "function_expression"):
|
|
174
|
+
return True
|
|
175
|
+
if _contains_function_body(child):
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _find_first_method_line(class_body: Node) -> int | None:
|
|
181
|
+
"""Find line number of first method in class body."""
|
|
182
|
+
for child in class_body.children:
|
|
183
|
+
if child.type in ("method_definition", "function_declaration"):
|
|
184
|
+
return child.start_point[0]
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _is_in_class_field_area(class_body: Node, ts_start: int, ts_end: int) -> bool:
|
|
189
|
+
"""Check if range is in class field definition area (before methods)."""
|
|
190
|
+
first_method_line = _find_first_method_line(class_body)
|
|
191
|
+
class_start = class_body.start_point[0]
|
|
192
|
+
class_end = class_body.end_point[0]
|
|
193
|
+
|
|
194
|
+
if first_method_line is None:
|
|
195
|
+
return class_start <= ts_start and class_end >= ts_end
|
|
196
|
+
|
|
197
|
+
return class_start <= ts_start and ts_end < first_method_line
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _find_interface_ranges(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
|
+
_process_line_for_interface(stripped, i, state, ranges)
|
|
209
|
+
|
|
210
|
+
return ranges
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _process_line_for_interface(
|
|
214
|
+
stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Process single line for interface detection."""
|
|
217
|
+
if _is_interface_start(stripped):
|
|
218
|
+
_handle_interface_start(stripped, line_num, state, ranges)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if state["in_interface"]:
|
|
222
|
+
_handle_interface_continuation(stripped, line_num, state, ranges)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_interface_start(stripped: str) -> bool:
|
|
226
|
+
"""Check if line starts interface/type definition."""
|
|
227
|
+
return stripped.startswith(("interface ", "type ")) and "{" in stripped
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _handle_interface_start(
|
|
231
|
+
stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Handle start of interface definition."""
|
|
234
|
+
state["in_interface"] = True
|
|
235
|
+
state["start_line"] = line_num
|
|
236
|
+
state["brace_count"] = stripped.count("{") - stripped.count("}")
|
|
237
|
+
|
|
238
|
+
if state["brace_count"] == 0:
|
|
239
|
+
ranges.append((line_num, line_num))
|
|
240
|
+
state["in_interface"] = False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _handle_interface_continuation(
|
|
244
|
+
stripped: str, line_num: int, state: dict[str, Any], ranges: list[tuple[int, int]]
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Handle continuation of interface definition."""
|
|
247
|
+
state["brace_count"] += stripped.count("{") - stripped.count("}")
|
|
248
|
+
if state["brace_count"] == 0:
|
|
249
|
+
ranges.append((state["start_line"], line_num))
|
|
250
|
+
state["in_interface"] = False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _overlaps_interface(start: int, end: int, interface_ranges: list[tuple[int, int]]) -> bool:
|
|
254
|
+
"""Check if block overlaps with any interface range."""
|
|
255
|
+
return any(start <= if_end and end >= if_start for if_start, if_end in interface_ranges)
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
Suppressions:
|
|
19
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from contextlib import suppress
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE
|
|
26
|
+
|
|
27
|
+
if TREE_SITTER_AVAILABLE:
|
|
28
|
+
from tree_sitter import Node
|
|
29
|
+
else:
|
|
30
|
+
Node = Any # type: ignore[assignment,misc]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TypeScriptValueExtractor:
|
|
34
|
+
"""Extracts value representations from TypeScript AST nodes."""
|
|
35
|
+
|
|
36
|
+
# Types that return their literal text
|
|
37
|
+
LITERAL_TYPES = frozenset(("number", "string", "true", "false", "null", "identifier"))
|
|
38
|
+
|
|
39
|
+
# Types with fixed representations
|
|
40
|
+
FIXED_REPRESENTATIONS = {"array": "[...]", "object": "{...}"}
|
|
41
|
+
|
|
42
|
+
def get_node_text(self, node: Node, content: str) -> str:
|
|
43
|
+
"""Get text content of a node."""
|
|
44
|
+
return content[node.start_byte : node.end_byte]
|
|
45
|
+
|
|
46
|
+
def get_value_string(self, node: Node, content: str) -> str | None:
|
|
47
|
+
"""Get string representation of a value node."""
|
|
48
|
+
if node.type in self.LITERAL_TYPES:
|
|
49
|
+
return self.get_node_text(node, content)
|
|
50
|
+
with suppress(KeyError):
|
|
51
|
+
return self.FIXED_REPRESENTATIONS[node.type]
|
|
52
|
+
if node.type == "call_expression":
|
|
53
|
+
return self._get_call_string(node, content)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def _get_call_string(self, node: Node, content: str) -> str:
|
|
57
|
+
"""Get string representation of a call expression.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
node: call_expression node
|
|
61
|
+
content: Original source content
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
String like "functionName(...)"
|
|
65
|
+
"""
|
|
66
|
+
for child in node.children:
|
|
67
|
+
if child.type == "identifier":
|
|
68
|
+
func_name = self.get_node_text(child, content)
|
|
69
|
+
return f"{func_name}(...)"
|
|
70
|
+
return "call(...)"
|
|
@@ -27,6 +27,10 @@ from .cache import CodeBlock
|
|
|
27
27
|
class DRYViolationBuilder:
|
|
28
28
|
"""Builds violation messages for duplicate code."""
|
|
29
29
|
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize the DRY violation builder."""
|
|
32
|
+
pass # Stateless builder for duplicate code violations
|
|
33
|
+
|
|
30
34
|
def build_violation(
|
|
31
35
|
self, block: CodeBlock, all_duplicates: list[CodeBlock], rule_id: str
|
|
32
36
|
) -> Violation:
|
|
@@ -25,6 +25,10 @@ DEFAULT_FALLBACK_LINE_COUNT = 5
|
|
|
25
25
|
class ViolationFilter:
|
|
26
26
|
"""Filters overlapping violations."""
|
|
27
27
|
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the violation filter."""
|
|
30
|
+
pass # Stateless filter for overlapping violations
|
|
31
|
+
|
|
28
32
|
def filter_overlapping(self, sorted_violations: list[Violation]) -> list[Violation]:
|
|
29
33
|
"""Filter overlapping violations, keeping first occurrence.
|
|
30
34
|
|
|
@@ -50,10 +54,7 @@ class ViolationFilter:
|
|
|
50
54
|
Returns:
|
|
51
55
|
True if violation overlaps with any kept violation
|
|
52
56
|
"""
|
|
53
|
-
for kept in kept_violations
|
|
54
|
-
if self._overlaps(violation, kept):
|
|
55
|
-
return True
|
|
56
|
-
return False
|
|
57
|
+
return any(self._overlaps(violation, kept) for kept in kept_violations)
|
|
57
58
|
|
|
58
59
|
def _overlaps(self, v1: Violation, v2: Violation) -> bool:
|
|
59
60
|
"""Check if two violations overlap.
|
|
@@ -10,14 +10,16 @@ Overview: Handles violation generation for duplicate code blocks. Queries storag
|
|
|
10
10
|
|
|
11
11
|
Dependencies: DuplicateStorage, ViolationDeduplicator, DRYViolationBuilder, Violation, DRYConfig
|
|
12
12
|
|
|
13
|
-
Exports: ViolationGenerator class
|
|
13
|
+
Exports: ViolationGenerator class, IgnoreContext dataclass
|
|
14
14
|
|
|
15
15
|
Interfaces: ViolationGenerator.generate_violations(storage, rule_id, config) -> list[Violation]
|
|
16
16
|
|
|
17
17
|
Implementation: Queries storage, deduplicates blocks, builds violations, filters by ignore patterns
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
from dataclasses import dataclass
|
|
20
21
|
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
21
23
|
|
|
22
24
|
from src.core.types import Violation
|
|
23
25
|
from src.orchestrator.language_detector import detect_language
|
|
@@ -28,6 +30,18 @@ from .duplicate_storage import DuplicateStorage
|
|
|
28
30
|
from .inline_ignore import InlineIgnoreParser
|
|
29
31
|
from .violation_builder import DRYViolationBuilder
|
|
30
32
|
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from src.linter_config.ignore import IgnoreDirectiveParser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class IgnoreContext:
|
|
39
|
+
"""Context for ignore directive filtering."""
|
|
40
|
+
|
|
41
|
+
inline_ignore: InlineIgnoreParser
|
|
42
|
+
shared_parser: "IgnoreDirectiveParser | None" = None
|
|
43
|
+
file_contents: dict[str, str] | None = None
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
class ViolationGenerator:
|
|
33
47
|
"""Generates violations from duplicate code blocks."""
|
|
@@ -42,7 +56,7 @@ class ViolationGenerator:
|
|
|
42
56
|
storage: DuplicateStorage,
|
|
43
57
|
rule_id: str,
|
|
44
58
|
config: DRYConfig,
|
|
45
|
-
|
|
59
|
+
ignore_ctx: IgnoreContext,
|
|
46
60
|
) -> list[Violation]:
|
|
47
61
|
"""Generate violations from storage.
|
|
48
62
|
|
|
@@ -50,19 +64,42 @@ class ViolationGenerator:
|
|
|
50
64
|
storage: Duplicate storage instance
|
|
51
65
|
rule_id: Rule identifier for violations
|
|
52
66
|
config: DRY configuration with ignore patterns
|
|
53
|
-
|
|
67
|
+
ignore_ctx: Context containing ignore parsers and file contents
|
|
54
68
|
|
|
55
69
|
Returns:
|
|
56
70
|
List of violations filtered by ignore patterns and inline directives
|
|
57
71
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
raw_violations = self._collect_violations(storage, rule_id, config)
|
|
73
|
+
deduplicated = self._deduplicator.deduplicate_violations(raw_violations)
|
|
74
|
+
pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
|
|
75
|
+
inline_filtered = self._filter_inline_ignored(pattern_filtered, ignore_ctx.inline_ignore)
|
|
76
|
+
|
|
77
|
+
# Apply shared ignore directive filtering for block and line directives
|
|
78
|
+
if ignore_ctx.shared_parser and ignore_ctx.file_contents:
|
|
79
|
+
return self._filter_shared_ignored(
|
|
80
|
+
inline_filtered, ignore_ctx.shared_parser, ignore_ctx.file_contents
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return inline_filtered
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
def _collect_violations(
|
|
86
|
+
self, storage: DuplicateStorage, rule_id: str, config: DRYConfig
|
|
87
|
+
) -> list[Violation]:
|
|
88
|
+
"""Collect raw violations from storage duplicate hashes.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
storage: Duplicate storage instance
|
|
92
|
+
rule_id: Rule identifier for violations
|
|
93
|
+
config: DRY configuration
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of raw violations before filtering
|
|
97
|
+
"""
|
|
98
|
+
violations = []
|
|
99
|
+
for hash_value in storage.duplicate_hashes:
|
|
62
100
|
blocks = storage.get_blocks_for_hash(hash_value)
|
|
63
101
|
dedup_blocks = self._deduplicator.deduplicate_blocks(blocks)
|
|
64
102
|
|
|
65
|
-
# Check min_occurrences threshold (language-aware)
|
|
66
103
|
if not self._meets_min_occurrences(dedup_blocks, config):
|
|
67
104
|
continue
|
|
68
105
|
|
|
@@ -70,9 +107,7 @@ class ViolationGenerator:
|
|
|
70
107
|
violation = self._violation_builder.build_violation(block, dedup_blocks, rule_id)
|
|
71
108
|
violations.append(violation)
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
pattern_filtered = self._filter_ignored(deduplicated, config.ignore_patterns)
|
|
75
|
-
return self._filter_inline_ignored(pattern_filtered, inline_ignore)
|
|
110
|
+
return violations
|
|
76
111
|
|
|
77
112
|
def _meets_min_occurrences(self, blocks: list, config: DRYConfig) -> bool:
|
|
78
113
|
"""Check if blocks meet minimum occurrence threshold for the language.
|
|
@@ -128,10 +163,7 @@ class ViolationGenerator:
|
|
|
128
163
|
True if file should be ignored
|
|
129
164
|
"""
|
|
130
165
|
path_str = str(Path(file_path))
|
|
131
|
-
for pattern in ignore_patterns
|
|
132
|
-
if pattern in path_str:
|
|
133
|
-
return True
|
|
134
|
-
return False
|
|
166
|
+
return any(pattern in path_str for pattern in ignore_patterns)
|
|
135
167
|
|
|
136
168
|
def _filter_inline_ignored(
|
|
137
169
|
self, violations: list[Violation], inline_ignore: InlineIgnoreParser
|
|
@@ -172,3 +204,28 @@ class ViolationGenerator:
|
|
|
172
204
|
return int(message[start:end])
|
|
173
205
|
except (ValueError, IndexError):
|
|
174
206
|
return 1
|
|
207
|
+
|
|
208
|
+
def _filter_shared_ignored(
|
|
209
|
+
self,
|
|
210
|
+
violations: list[Violation],
|
|
211
|
+
ignore_parser: "IgnoreDirectiveParser",
|
|
212
|
+
file_contents: dict[str, str],
|
|
213
|
+
) -> list[Violation]:
|
|
214
|
+
"""Filter violations using the shared ignore directive parser.
|
|
215
|
+
|
|
216
|
+
This enables standard # thailint: ignore-start/end directives for DRY linter.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
violations: List of violations to filter
|
|
220
|
+
ignore_parser: Shared ignore directive parser
|
|
221
|
+
file_contents: Cached file contents for ignore checking
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Filtered list of violations
|
|
225
|
+
"""
|
|
226
|
+
filtered = []
|
|
227
|
+
for violation in violations:
|
|
228
|
+
file_content = file_contents.get(violation.file_path, "")
|
|
229
|
+
if not ignore_parser.should_ignore_violation(violation, file_content):
|
|
230
|
+
filtered.append(violation)
|
|
231
|
+
return filtered
|
|
@@ -1,64 +1,82 @@
|
|
|
1
1
|
"""
|
|
2
|
-
File: src/linters/file_header/atemporal_detector.py
|
|
3
2
|
Purpose: Detects temporal language patterns in file headers
|
|
4
|
-
Exports: AtemporalDetector class
|
|
5
|
-
Depends: re module for regex matching
|
|
6
|
-
Implements: Regex-based pattern matching with configurable patterns
|
|
7
|
-
Related: linter.py for detector usage, violation_builder.py for violation creation
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Scope: File header validation for atemporal language compliance
|
|
5
|
+
|
|
6
|
+
Overview: Implements pattern-based detection of temporal language that violates atemporal
|
|
11
7
|
documentation requirements. Detects dates, temporal qualifiers, state change language,
|
|
12
8
|
and future references using regex patterns. Provides violation details for each pattern match.
|
|
9
|
+
Uses four pattern categories (dates, temporal qualifiers, state changes, future references)
|
|
10
|
+
to identify violations and returns detailed information for each match.
|
|
11
|
+
|
|
12
|
+
Dependencies: re module for regex-based pattern matching
|
|
13
|
+
|
|
14
|
+
Exports: AtemporalDetector class with detect_violations method
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
detector = AtemporalDetector()
|
|
16
|
-
violations = detector.detect_violations(header_text)
|
|
16
|
+
Interfaces: detect_violations(text) -> list[tuple[str, str, int]] returns pattern matches with line numbers
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Implementation: Regex-based pattern matching with pre-compiled patterns organized by category
|
|
19
|
+
|
|
20
|
+
Suppressions:
|
|
21
|
+
- nesting: detect_violations iterates over pattern categories and their patterns.
|
|
22
|
+
Natural grouping by category requires nested loops.
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
25
|
import re
|
|
26
|
+
from re import Pattern
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compile_patterns(patterns: list[tuple[str, str]]) -> list[tuple[Pattern[str], str]]:
|
|
30
|
+
"""Compile regex patterns for efficient reuse."""
|
|
31
|
+
return [(re.compile(pattern, re.IGNORECASE), desc) for pattern, desc in patterns]
|
|
22
32
|
|
|
23
33
|
|
|
24
34
|
class AtemporalDetector:
|
|
25
35
|
"""Detects temporal language patterns in text."""
|
|
26
36
|
|
|
27
|
-
#
|
|
28
|
-
DATE_PATTERNS =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
37
|
+
# Pre-compiled date patterns
|
|
38
|
+
DATE_PATTERNS = _compile_patterns(
|
|
39
|
+
[
|
|
40
|
+
(r"\d{4}-\d{2}-\d{2}", "ISO date format (YYYY-MM-DD)"),
|
|
41
|
+
(
|
|
42
|
+
r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{4}",
|
|
43
|
+
"Month Year format",
|
|
44
|
+
),
|
|
45
|
+
(r"(?:Created|Updated|Modified):\s*\d{4}", "Date metadata"),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Pre-compiled temporal qualifiers
|
|
50
|
+
TEMPORAL_QUALIFIERS = _compile_patterns(
|
|
51
|
+
[
|
|
52
|
+
(r"\bcurrently\b", 'temporal qualifier "currently"'),
|
|
53
|
+
(r"\bnow\b", 'temporal qualifier "now"'),
|
|
54
|
+
(r"\brecently\b", 'temporal qualifier "recently"'),
|
|
55
|
+
(r"\bsoon\b", 'temporal qualifier "soon"'),
|
|
56
|
+
(r"\bfor now\b", 'temporal qualifier "for now"'),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Pre-compiled state change language
|
|
61
|
+
STATE_CHANGE = _compile_patterns(
|
|
62
|
+
[
|
|
63
|
+
(r"\breplaces?\b", 'state change "replaces"'),
|
|
64
|
+
(r"\bmigrated from\b", 'state change "migrated from"'),
|
|
65
|
+
(r"\bformerly\b", 'state change "formerly"'),
|
|
66
|
+
(r"\bold implementation\b", 'state change "old"'),
|
|
67
|
+
(r"\bnew implementation\b", 'state change "new"'),
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Pre-compiled future references
|
|
72
|
+
FUTURE_REFS = _compile_patterns(
|
|
73
|
+
[
|
|
74
|
+
(r"\bwill be\b", 'future reference "will be"'),
|
|
75
|
+
(r"\bplanned\b", 'future reference "planned"'),
|
|
76
|
+
(r"\bto be added\b", 'future reference "to be added"'),
|
|
77
|
+
(r"\bcoming soon\b", 'future reference "coming soon"'),
|
|
78
|
+
]
|
|
79
|
+
)
|
|
62
80
|
|
|
63
81
|
def detect_violations( # thailint: ignore[nesting]
|
|
64
82
|
self, text: str
|
|
@@ -73,15 +91,15 @@ class AtemporalDetector:
|
|
|
73
91
|
"""
|
|
74
92
|
violations = []
|
|
75
93
|
|
|
76
|
-
# Check all pattern categories
|
|
94
|
+
# Check all pattern categories (patterns are pre-compiled)
|
|
77
95
|
all_patterns = (
|
|
78
96
|
self.DATE_PATTERNS + self.TEMPORAL_QUALIFIERS + self.STATE_CHANGE + self.FUTURE_REFS
|
|
79
97
|
)
|
|
80
98
|
|
|
81
99
|
lines = text.split("\n")
|
|
82
100
|
for line_num, line in enumerate(lines, start=1):
|
|
83
|
-
for
|
|
84
|
-
if
|
|
85
|
-
violations.append((pattern, description, line_num))
|
|
101
|
+
for compiled_pattern, description in all_patterns:
|
|
102
|
+
if compiled_pattern.search(line):
|
|
103
|
+
violations.append((compiled_pattern.pattern, description, line_num))
|
|
86
104
|
|
|
87
105
|
return violations
|