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,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Detect scattered string comparisons in TypeScript code using tree-sitter
|
|
3
|
+
|
|
4
|
+
Scope: Find equality/inequality comparisons with string literals across TypeScript files
|
|
5
|
+
|
|
6
|
+
Overview: Provides TypeScriptComparisonTracker class that uses tree-sitter to traverse
|
|
7
|
+
TypeScript AST and find scattered string comparisons like `if (env === "production")`.
|
|
8
|
+
Tracks the variable name, compared string value, and operator to enable cross-file
|
|
9
|
+
aggregation. When a variable is compared to multiple unique string values across files,
|
|
10
|
+
it suggests the variable should be an enum. Excludes common false positives like template
|
|
11
|
+
literals and typeof comparisons.
|
|
12
|
+
|
|
13
|
+
Dependencies: TypeScriptBaseAnalyzer for tree-sitter parsing, dataclasses for pattern structure,
|
|
14
|
+
src.core.constants for MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
15
|
+
|
|
16
|
+
Exports: TypeScriptComparisonTracker class, TypeScriptComparisonPattern dataclass
|
|
17
|
+
|
|
18
|
+
Interfaces: TypeScriptComparisonTracker.find_patterns(code) -> list[TypeScriptComparisonPattern]
|
|
19
|
+
|
|
20
|
+
Implementation: Tree-sitter node traversal with binary_expression node handling for string
|
|
21
|
+
comparisons
|
|
22
|
+
|
|
23
|
+
Suppressions:
|
|
24
|
+
- type:ignore[assignment,misc]: Tree-sitter Node type alias (optional dependency fallback)
|
|
25
|
+
- srp: Tracker implements tree-sitter traversal with helper methods for node extraction.
|
|
26
|
+
Methods support single responsibility of comparison pattern detection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from src.analyzers.typescript_base import TREE_SITTER_AVAILABLE, TypeScriptBaseAnalyzer
|
|
33
|
+
from src.core.constants import MAX_ATTRIBUTE_CHAIN_DEPTH
|
|
34
|
+
|
|
35
|
+
if TREE_SITTER_AVAILABLE:
|
|
36
|
+
from tree_sitter import Node
|
|
37
|
+
else:
|
|
38
|
+
Node = Any # type: ignore[assignment,misc]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TypeScriptComparisonPattern:
|
|
43
|
+
"""Represents a string comparison found in TypeScript code.
|
|
44
|
+
|
|
45
|
+
Captures information about a comparison like `if (env === "production")` to
|
|
46
|
+
enable cross-file analysis for detecting scattered string comparisons that
|
|
47
|
+
suggest missing enums.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
variable_name: str
|
|
51
|
+
"""Variable name being compared (e.g., 'env' or 'this.status')."""
|
|
52
|
+
|
|
53
|
+
compared_value: str
|
|
54
|
+
"""The string literal value being compared to."""
|
|
55
|
+
|
|
56
|
+
operator: str
|
|
57
|
+
"""The comparison operator ('===', '==', '!==', '!=')."""
|
|
58
|
+
|
|
59
|
+
line_number: int
|
|
60
|
+
"""Line number where the comparison occurs (1-indexed)."""
|
|
61
|
+
|
|
62
|
+
column: int
|
|
63
|
+
"""Column number where the comparison starts (0-indexed)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Operators that indicate string comparison
|
|
67
|
+
_COMPARISON_OPERATORS = frozenset({"===", "==", "!==", "!="})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TypeScriptComparisonTracker(TypeScriptBaseAnalyzer): # thailint: ignore[srp]
|
|
71
|
+
"""Tracks scattered string comparisons in TypeScript code.
|
|
72
|
+
|
|
73
|
+
Finds patterns like `if (env === "production")` and `if (status !== "deleted")`
|
|
74
|
+
where string literals are used for comparisons that could use enums instead.
|
|
75
|
+
|
|
76
|
+
Note: Method count exceeds SRP limit because tree-sitter traversal requires
|
|
77
|
+
multiple helper methods for extracting variable names, member expressions,
|
|
78
|
+
and string handling. All methods support the single responsibility of
|
|
79
|
+
tracking string comparisons.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
"""Initialize the tracker."""
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.patterns: list[TypeScriptComparisonPattern] = []
|
|
86
|
+
|
|
87
|
+
def find_patterns(self, code: str) -> list[TypeScriptComparisonPattern]:
|
|
88
|
+
"""Find all string comparisons in the code.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
code: TypeScript source code to analyze
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of TypeScriptComparisonPattern instances for each detected comparison
|
|
95
|
+
"""
|
|
96
|
+
if not self.tree_sitter_available:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
root = self.parse_typescript(code)
|
|
100
|
+
if root is None:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
return self.find_patterns_from_tree(root)
|
|
104
|
+
|
|
105
|
+
def find_patterns_from_tree(self, tree: Node) -> list[TypeScriptComparisonPattern]:
|
|
106
|
+
"""Find all string comparisons from a pre-parsed tree.
|
|
107
|
+
|
|
108
|
+
Optimized for single-parse workflows where the tree is shared between trackers.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
tree: Pre-parsed tree-sitter root node
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of TypeScriptComparisonPattern instances for each detected comparison
|
|
115
|
+
"""
|
|
116
|
+
self.patterns = []
|
|
117
|
+
self._traverse_tree(tree)
|
|
118
|
+
return self.patterns
|
|
119
|
+
|
|
120
|
+
def _traverse_tree(self, node: Node) -> None:
|
|
121
|
+
"""Recursively traverse tree looking for binary expressions.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
node: Current tree-sitter node
|
|
125
|
+
"""
|
|
126
|
+
if node.type == "binary_expression":
|
|
127
|
+
self._process_binary_expression(node)
|
|
128
|
+
|
|
129
|
+
for child in node.children:
|
|
130
|
+
self._traverse_tree(child)
|
|
131
|
+
|
|
132
|
+
def _process_binary_expression(self, node: Node) -> None:
|
|
133
|
+
"""Process a binary expression node for string comparisons.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
node: binary_expression node
|
|
137
|
+
"""
|
|
138
|
+
operator = self._extract_operator(node)
|
|
139
|
+
if operator is None or operator not in _COMPARISON_OPERATORS:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Get left and right operands
|
|
143
|
+
operands = self._get_operands(node)
|
|
144
|
+
if operands is None:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
left, right = operands
|
|
148
|
+
|
|
149
|
+
# Try both orientations: var === "string" and "string" === var
|
|
150
|
+
self._try_extract_pattern(left, right, operator, node)
|
|
151
|
+
self._try_extract_pattern(right, left, operator, node)
|
|
152
|
+
|
|
153
|
+
def _extract_operator(self, node: Node) -> str | None:
|
|
154
|
+
"""Extract the operator from a binary expression.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
node: binary_expression node
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Operator string or None
|
|
161
|
+
"""
|
|
162
|
+
for child in node.children:
|
|
163
|
+
if child.type in ("===", "==", "!==", "!="):
|
|
164
|
+
return child.type
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def _get_operands(self, node: Node) -> tuple[Node, Node] | None:
|
|
168
|
+
"""Get the left and right operands of a binary expression.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
node: binary_expression node
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Tuple of (left, right) nodes or None if structure is unexpected
|
|
175
|
+
"""
|
|
176
|
+
operands = []
|
|
177
|
+
for child in node.children:
|
|
178
|
+
# Skip operators
|
|
179
|
+
if child.type not in ("===", "==", "!==", "!="):
|
|
180
|
+
operands.append(child)
|
|
181
|
+
|
|
182
|
+
if len(operands) >= 2:
|
|
183
|
+
return (operands[0], operands[1])
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def _try_extract_pattern(
|
|
187
|
+
self,
|
|
188
|
+
var_side: Node,
|
|
189
|
+
string_side: Node,
|
|
190
|
+
operator: str,
|
|
191
|
+
node: Node,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Try to extract a pattern from a comparison.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
var_side: The node that might be a variable
|
|
197
|
+
string_side: The node that might be a string literal
|
|
198
|
+
operator: The comparison operator
|
|
199
|
+
node: The original binary_expression node for location info
|
|
200
|
+
"""
|
|
201
|
+
# Check if string_side is a string literal (not a template literal)
|
|
202
|
+
string_value = self._extract_string_value(string_side)
|
|
203
|
+
if string_value is None:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Extract variable name
|
|
207
|
+
var_name = self._extract_variable_name(var_side)
|
|
208
|
+
if var_name is None:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Check for excluded patterns
|
|
212
|
+
if self._should_exclude(var_side, var_name, string_value):
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
self._add_pattern(var_name, string_value, operator, node)
|
|
216
|
+
|
|
217
|
+
def _extract_string_value(self, node: Node) -> str | None:
|
|
218
|
+
"""Extract string value from a node if it's a string literal.
|
|
219
|
+
|
|
220
|
+
Excludes template literals with interpolation.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
node: Potential string literal node
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
String value without quotes, or None if not a simple string
|
|
227
|
+
"""
|
|
228
|
+
if node.type != "string":
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
text = self.extract_node_text(node)
|
|
232
|
+
if len(text) < 2:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
return self._strip_quotes(text)
|
|
236
|
+
|
|
237
|
+
def _strip_quotes(self, text: str) -> str | None:
|
|
238
|
+
"""Strip quotes from a string literal, excluding template interpolation.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
text: The raw string text including quotes
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The string value without quotes, or None if invalid
|
|
245
|
+
"""
|
|
246
|
+
first_char = text[0]
|
|
247
|
+
|
|
248
|
+
# Template literal (backticks) - exclude if has interpolation
|
|
249
|
+
if first_char == "`":
|
|
250
|
+
return None if "${" in text else text[1:-1]
|
|
251
|
+
|
|
252
|
+
# Regular string literal (single or double quotes)
|
|
253
|
+
if first_char in ('"', "'") and text[-1] == first_char:
|
|
254
|
+
return text[1:-1]
|
|
255
|
+
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def _extract_variable_name(self, node: Node) -> str | None:
|
|
259
|
+
"""Extract variable name from a node.
|
|
260
|
+
|
|
261
|
+
Handles simple identifiers and member expressions.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
node: Potential variable node
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Variable name string or None if not extractable
|
|
268
|
+
"""
|
|
269
|
+
if node.type == "identifier":
|
|
270
|
+
return self.extract_node_text(node)
|
|
271
|
+
if node.type == "member_expression":
|
|
272
|
+
return self._extract_member_expression_name(node)
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def _extract_member_expression_name(self, node: Node) -> str | None:
|
|
276
|
+
"""Extract name from a member expression.
|
|
277
|
+
|
|
278
|
+
Builds qualified names like 'obj.attr' or 'a.b.attr'.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
node: member_expression node
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Qualified name or None if too complex
|
|
285
|
+
"""
|
|
286
|
+
parts: list[str] = []
|
|
287
|
+
current: Node | None = node
|
|
288
|
+
|
|
289
|
+
for _ in range(MAX_ATTRIBUTE_CHAIN_DEPTH):
|
|
290
|
+
if current is None:
|
|
291
|
+
break
|
|
292
|
+
self._add_property_name(current, parts)
|
|
293
|
+
current = self._get_next_node(current, parts)
|
|
294
|
+
|
|
295
|
+
return ".".join(reversed(parts)) if parts else None
|
|
296
|
+
|
|
297
|
+
def _add_property_name(self, node: Node, parts: list[str]) -> None:
|
|
298
|
+
"""Add property name to parts list if found.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
node: member_expression node
|
|
302
|
+
parts: List to append property name to
|
|
303
|
+
"""
|
|
304
|
+
for child in node.children:
|
|
305
|
+
if child.type == "property_identifier":
|
|
306
|
+
parts.append(self.extract_node_text(child))
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
def _get_next_node(self, current: Node, parts: list[str]) -> Node | None:
|
|
310
|
+
"""Get the next node to process in member expression chain.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
current: Current member_expression node
|
|
314
|
+
parts: List of parts (modified if terminal node found)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Next node to process or None to stop
|
|
318
|
+
"""
|
|
319
|
+
for child in current.children:
|
|
320
|
+
if child.type == "identifier":
|
|
321
|
+
parts.append(self.extract_node_text(child))
|
|
322
|
+
return None
|
|
323
|
+
if child.type == "member_expression":
|
|
324
|
+
return child
|
|
325
|
+
if child.type == "this":
|
|
326
|
+
parts.append("this")
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# Complex expression
|
|
330
|
+
parts.append("_")
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def _should_exclude(self, var_node: Node, var_name: str, string_value: str) -> bool:
|
|
334
|
+
"""Check if this comparison should be excluded.
|
|
335
|
+
|
|
336
|
+
Filters out common patterns that are not stringly-typed code:
|
|
337
|
+
- typeof comparisons
|
|
338
|
+
- Standard type checks
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
var_node: The variable side node
|
|
342
|
+
var_name: The variable name
|
|
343
|
+
string_value: The string value
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
True if the comparison should be excluded
|
|
347
|
+
"""
|
|
348
|
+
if self._is_typeof_expression(var_node):
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
if var_name == "typeof":
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def _is_typeof_expression(self, node: Node) -> bool:
|
|
357
|
+
"""Check if node is a typeof unary expression."""
|
|
358
|
+
if node.type != "unary_expression":
|
|
359
|
+
return False
|
|
360
|
+
return any(child.type == "typeof" for child in node.children)
|
|
361
|
+
|
|
362
|
+
def _add_pattern(self, var_name: str, string_value: str, operator: str, node: Node) -> None:
|
|
363
|
+
"""Create and add a comparison pattern to results.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
var_name: The variable name
|
|
367
|
+
string_value: The string value being compared
|
|
368
|
+
operator: The comparison operator
|
|
369
|
+
node: The binary_expression node for location info
|
|
370
|
+
"""
|
|
371
|
+
pattern = TypeScriptComparisonPattern(
|
|
372
|
+
variable_name=var_name,
|
|
373
|
+
compared_value=string_value,
|
|
374
|
+
operator=operator,
|
|
375
|
+
line_number=node.start_point[0] + 1, # 1-indexed
|
|
376
|
+
column=node.start_point[1],
|
|
377
|
+
)
|
|
378
|
+
self.patterns.append(pattern)
|