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