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.
Files changed (204) hide show
  1. src/__init__.py +1 -0
  2. src/analyzers/__init__.py +4 -3
  3. src/analyzers/ast_utils.py +54 -0
  4. src/analyzers/rust_base.py +155 -0
  5. src/analyzers/rust_context.py +141 -0
  6. src/analyzers/typescript_base.py +4 -0
  7. src/cli/__init__.py +30 -0
  8. src/cli/__main__.py +22 -0
  9. src/cli/config.py +480 -0
  10. src/cli/config_merge.py +241 -0
  11. src/cli/linters/__init__.py +67 -0
  12. src/cli/linters/code_patterns.py +270 -0
  13. src/cli/linters/code_smells.py +342 -0
  14. src/cli/linters/documentation.py +83 -0
  15. src/cli/linters/performance.py +287 -0
  16. src/cli/linters/shared.py +331 -0
  17. src/cli/linters/structure.py +327 -0
  18. src/cli/linters/structure_quality.py +328 -0
  19. src/cli/main.py +120 -0
  20. src/cli/utils.py +395 -0
  21. src/cli_main.py +37 -0
  22. src/config.py +38 -25
  23. src/core/base.py +7 -2
  24. src/core/cli_utils.py +19 -2
  25. src/core/config_parser.py +5 -2
  26. src/core/constants.py +54 -0
  27. src/core/linter_utils.py +95 -6
  28. src/core/python_lint_rule.py +101 -0
  29. src/core/registry.py +1 -1
  30. src/core/rule_discovery.py +147 -84
  31. src/core/types.py +13 -0
  32. src/core/violation_builder.py +78 -15
  33. src/core/violation_utils.py +69 -0
  34. src/formatters/__init__.py +22 -0
  35. src/formatters/sarif.py +202 -0
  36. src/linter_config/directive_markers.py +109 -0
  37. src/linter_config/ignore.py +254 -395
  38. src/linter_config/loader.py +45 -12
  39. src/linter_config/pattern_utils.py +65 -0
  40. src/linter_config/rule_matcher.py +89 -0
  41. src/linters/collection_pipeline/__init__.py +90 -0
  42. src/linters/collection_pipeline/any_all_analyzer.py +281 -0
  43. src/linters/collection_pipeline/ast_utils.py +40 -0
  44. src/linters/collection_pipeline/config.py +75 -0
  45. src/linters/collection_pipeline/continue_analyzer.py +94 -0
  46. src/linters/collection_pipeline/detector.py +360 -0
  47. src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
  48. src/linters/collection_pipeline/linter.py +420 -0
  49. src/linters/collection_pipeline/suggestion_builder.py +130 -0
  50. src/linters/cqs/__init__.py +54 -0
  51. src/linters/cqs/config.py +55 -0
  52. src/linters/cqs/function_analyzer.py +201 -0
  53. src/linters/cqs/input_detector.py +139 -0
  54. src/linters/cqs/linter.py +159 -0
  55. src/linters/cqs/output_detector.py +84 -0
  56. src/linters/cqs/python_analyzer.py +54 -0
  57. src/linters/cqs/types.py +82 -0
  58. src/linters/cqs/typescript_cqs_analyzer.py +61 -0
  59. src/linters/cqs/typescript_function_analyzer.py +192 -0
  60. src/linters/cqs/typescript_input_detector.py +203 -0
  61. src/linters/cqs/typescript_output_detector.py +117 -0
  62. src/linters/cqs/violation_builder.py +94 -0
  63. src/linters/dry/base_token_analyzer.py +16 -9
  64. src/linters/dry/block_filter.py +120 -20
  65. src/linters/dry/block_grouper.py +4 -0
  66. src/linters/dry/cache.py +104 -10
  67. src/linters/dry/cache_query.py +4 -0
  68. src/linters/dry/config.py +54 -11
  69. src/linters/dry/constant.py +92 -0
  70. src/linters/dry/constant_matcher.py +223 -0
  71. src/linters/dry/constant_violation_builder.py +98 -0
  72. src/linters/dry/duplicate_storage.py +5 -4
  73. src/linters/dry/file_analyzer.py +4 -2
  74. src/linters/dry/inline_ignore.py +7 -16
  75. src/linters/dry/linter.py +183 -48
  76. src/linters/dry/python_analyzer.py +60 -439
  77. src/linters/dry/python_constant_extractor.py +100 -0
  78. src/linters/dry/single_statement_detector.py +417 -0
  79. src/linters/dry/token_hasher.py +116 -112
  80. src/linters/dry/typescript_analyzer.py +68 -382
  81. src/linters/dry/typescript_constant_extractor.py +138 -0
  82. src/linters/dry/typescript_statement_detector.py +255 -0
  83. src/linters/dry/typescript_value_extractor.py +70 -0
  84. src/linters/dry/violation_builder.py +4 -0
  85. src/linters/dry/violation_filter.py +5 -4
  86. src/linters/dry/violation_generator.py +71 -14
  87. src/linters/file_header/atemporal_detector.py +68 -50
  88. src/linters/file_header/base_parser.py +93 -0
  89. src/linters/file_header/bash_parser.py +66 -0
  90. src/linters/file_header/config.py +90 -16
  91. src/linters/file_header/css_parser.py +70 -0
  92. src/linters/file_header/field_validator.py +36 -33
  93. src/linters/file_header/linter.py +140 -144
  94. src/linters/file_header/markdown_parser.py +130 -0
  95. src/linters/file_header/python_parser.py +14 -58
  96. src/linters/file_header/typescript_parser.py +73 -0
  97. src/linters/file_header/violation_builder.py +13 -12
  98. src/linters/file_placement/config_loader.py +3 -1
  99. src/linters/file_placement/directory_matcher.py +4 -0
  100. src/linters/file_placement/linter.py +66 -34
  101. src/linters/file_placement/pattern_matcher.py +41 -6
  102. src/linters/file_placement/pattern_validator.py +31 -12
  103. src/linters/file_placement/rule_checker.py +12 -7
  104. src/linters/lazy_ignores/__init__.py +43 -0
  105. src/linters/lazy_ignores/config.py +74 -0
  106. src/linters/lazy_ignores/directive_utils.py +164 -0
  107. src/linters/lazy_ignores/header_parser.py +177 -0
  108. src/linters/lazy_ignores/linter.py +158 -0
  109. src/linters/lazy_ignores/matcher.py +168 -0
  110. src/linters/lazy_ignores/python_analyzer.py +209 -0
  111. src/linters/lazy_ignores/rule_id_utils.py +180 -0
  112. src/linters/lazy_ignores/skip_detector.py +298 -0
  113. src/linters/lazy_ignores/types.py +71 -0
  114. src/linters/lazy_ignores/typescript_analyzer.py +146 -0
  115. src/linters/lazy_ignores/violation_builder.py +135 -0
  116. src/linters/lbyl/__init__.py +31 -0
  117. src/linters/lbyl/config.py +63 -0
  118. src/linters/lbyl/linter.py +67 -0
  119. src/linters/lbyl/pattern_detectors/__init__.py +53 -0
  120. src/linters/lbyl/pattern_detectors/base.py +63 -0
  121. src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
  122. src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
  123. src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
  124. src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
  125. src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
  126. src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
  127. src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
  128. src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
  129. src/linters/lbyl/python_analyzer.py +215 -0
  130. src/linters/lbyl/violation_builder.py +354 -0
  131. src/linters/magic_numbers/context_analyzer.py +227 -225
  132. src/linters/magic_numbers/linter.py +28 -82
  133. src/linters/magic_numbers/python_analyzer.py +4 -16
  134. src/linters/magic_numbers/typescript_analyzer.py +9 -12
  135. src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
  136. src/linters/method_property/__init__.py +49 -0
  137. src/linters/method_property/config.py +138 -0
  138. src/linters/method_property/linter.py +414 -0
  139. src/linters/method_property/python_analyzer.py +473 -0
  140. src/linters/method_property/violation_builder.py +119 -0
  141. src/linters/nesting/linter.py +24 -16
  142. src/linters/nesting/python_analyzer.py +4 -0
  143. src/linters/nesting/typescript_analyzer.py +6 -12
  144. src/linters/nesting/violation_builder.py +1 -0
  145. src/linters/performance/__init__.py +91 -0
  146. src/linters/performance/config.py +43 -0
  147. src/linters/performance/constants.py +49 -0
  148. src/linters/performance/linter.py +149 -0
  149. src/linters/performance/python_analyzer.py +365 -0
  150. src/linters/performance/regex_analyzer.py +312 -0
  151. src/linters/performance/regex_linter.py +139 -0
  152. src/linters/performance/typescript_analyzer.py +236 -0
  153. src/linters/performance/violation_builder.py +160 -0
  154. src/linters/print_statements/config.py +7 -12
  155. src/linters/print_statements/linter.py +26 -43
  156. src/linters/print_statements/python_analyzer.py +91 -93
  157. src/linters/print_statements/typescript_analyzer.py +15 -25
  158. src/linters/print_statements/violation_builder.py +12 -14
  159. src/linters/srp/class_analyzer.py +11 -7
  160. src/linters/srp/heuristics.py +56 -22
  161. src/linters/srp/linter.py +15 -16
  162. src/linters/srp/python_analyzer.py +55 -20
  163. src/linters/srp/typescript_metrics_calculator.py +110 -50
  164. src/linters/stateless_class/__init__.py +25 -0
  165. src/linters/stateless_class/config.py +58 -0
  166. src/linters/stateless_class/linter.py +349 -0
  167. src/linters/stateless_class/python_analyzer.py +290 -0
  168. src/linters/stringly_typed/__init__.py +36 -0
  169. src/linters/stringly_typed/config.py +189 -0
  170. src/linters/stringly_typed/context_filter.py +451 -0
  171. src/linters/stringly_typed/function_call_violation_builder.py +135 -0
  172. src/linters/stringly_typed/ignore_checker.py +100 -0
  173. src/linters/stringly_typed/ignore_utils.py +51 -0
  174. src/linters/stringly_typed/linter.py +376 -0
  175. src/linters/stringly_typed/python/__init__.py +33 -0
  176. src/linters/stringly_typed/python/analyzer.py +348 -0
  177. src/linters/stringly_typed/python/call_tracker.py +175 -0
  178. src/linters/stringly_typed/python/comparison_tracker.py +257 -0
  179. src/linters/stringly_typed/python/condition_extractor.py +134 -0
  180. src/linters/stringly_typed/python/conditional_detector.py +179 -0
  181. src/linters/stringly_typed/python/constants.py +21 -0
  182. src/linters/stringly_typed/python/match_analyzer.py +94 -0
  183. src/linters/stringly_typed/python/validation_detector.py +189 -0
  184. src/linters/stringly_typed/python/variable_extractor.py +96 -0
  185. src/linters/stringly_typed/storage.py +620 -0
  186. src/linters/stringly_typed/storage_initializer.py +45 -0
  187. src/linters/stringly_typed/typescript/__init__.py +28 -0
  188. src/linters/stringly_typed/typescript/analyzer.py +157 -0
  189. src/linters/stringly_typed/typescript/call_tracker.py +335 -0
  190. src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
  191. src/linters/stringly_typed/violation_generator.py +419 -0
  192. src/orchestrator/core.py +252 -14
  193. src/orchestrator/language_detector.py +5 -3
  194. src/templates/thailint_config_template.yaml +196 -0
  195. src/utils/project_root.py +3 -0
  196. thailint-0.15.3.dist-info/METADATA +187 -0
  197. thailint-0.15.3.dist-info/RECORD +226 -0
  198. thailint-0.15.3.dist-info/entry_points.txt +4 -0
  199. src/cli.py +0 -1665
  200. thailint-0.5.0.dist-info/METADATA +0 -1286
  201. thailint-0.5.0.dist-info/RECORD +0 -96
  202. thailint-0.5.0.dist-info/entry_points.txt +0 -4
  203. {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
  204. {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)