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,630 @@
1
+ # pylint: disable=too-many-lines
2
+ # Justification: Storage module for three related data types (patterns, function calls,
3
+ # comparisons) with their dataclasses, conversion functions, SQL schemas, and CRUD methods.
4
+ # Splitting into separate files would fragment cohesive SQLite storage logic.
5
+ """
6
+ Purpose: SQLite storage manager for stringly-typed pattern detection
7
+
8
+ Scope: String validation pattern storage, function call tracking, comparison tracking, and
9
+ cross-file detection
10
+
11
+ Overview: Implements in-memory or temporary-file SQLite storage for stringly-typed pattern
12
+ detection. Stores string validation patterns with hash values computed from the string
13
+ values, enabling cross-file duplicate detection during a single linter run. Also tracks
14
+ function calls with string arguments to detect parameters that should be enums. Tracks
15
+ scattered string comparisons (`var == "string"`) to detect variables compared to multiple
16
+ string values across files. Supports both :memory: mode (fast, RAM-only) and tempfile mode
17
+ (disk-backed for large projects). No persistence between runs - storage is cleared when
18
+ linter completes. Includes indexes for fast hash lookups enabling efficient cross-file
19
+ detection.
20
+
21
+ Dependencies: Python sqlite3 module (stdlib), tempfile module (stdlib), pathlib.Path,
22
+ dataclasses, json module (stdlib)
23
+
24
+ Exports: StoredPattern dataclass, StoredFunctionCall dataclass, StoredComparison dataclass,
25
+ StringlyTypedStorage class
26
+
27
+ Interfaces: StringlyTypedStorage.__init__(storage_mode), add_pattern(pattern),
28
+ add_patterns(patterns), get_duplicate_hashes(min_files), get_patterns_by_hash(hash_value),
29
+ add_function_call(call), add_function_calls(calls), get_limited_value_functions(min_values,
30
+ max_values, min_files), get_calls_by_function(function_name, param_index),
31
+ add_comparison(comparison), add_comparisons(comparisons),
32
+ get_variables_with_multiple_values(min_values, min_files),
33
+ get_comparisons_by_variable(variable_name), get_all_comparisons(), clear(), close()
34
+
35
+ Implementation: SQLite with string_validations, function_calls, and string_comparisons tables,
36
+ indexed on string_set_hash, function_name+param_index, and variable_name for performance
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import sqlite3
43
+ import tempfile
44
+ from dataclasses import dataclass
45
+ from pathlib import Path
46
+
47
+ # Row index constants for SQLite query results
48
+ _COL_FILE_PATH = 0
49
+ _COL_LINE_NUMBER = 1
50
+ _COL_COLUMN = 2
51
+ _COL_VARIABLE_NAME = 3
52
+ _COL_STRING_SET_HASH = 4
53
+ _COL_STRING_VALUES = 5
54
+ _COL_PATTERN_TYPE = 6
55
+ _COL_DETAILS = 7
56
+
57
+ # Schema SQL for table creation
58
+ _CREATE_TABLE_SQL = """CREATE TABLE IF NOT EXISTS string_validations (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ file_path TEXT NOT NULL,
61
+ line_number INTEGER NOT NULL,
62
+ column_number INTEGER NOT NULL,
63
+ variable_name TEXT,
64
+ string_set_hash INTEGER NOT NULL,
65
+ string_values TEXT NOT NULL,
66
+ pattern_type TEXT NOT NULL,
67
+ details TEXT NOT NULL,
68
+ UNIQUE(file_path, line_number, column_number)
69
+ )"""
70
+
71
+ _CREATE_HASH_INDEX_SQL = (
72
+ "CREATE INDEX IF NOT EXISTS idx_string_hash ON string_validations(string_set_hash)"
73
+ )
74
+
75
+ _CREATE_FILE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_file_path ON string_validations(file_path)"
76
+
77
+ # Function calls table schema
78
+ _CREATE_FUNCTION_CALLS_TABLE_SQL = """CREATE TABLE IF NOT EXISTS function_calls (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ file_path TEXT NOT NULL,
81
+ line_number INTEGER NOT NULL,
82
+ column_number INTEGER NOT NULL,
83
+ function_name TEXT NOT NULL,
84
+ param_index INTEGER NOT NULL,
85
+ string_value TEXT NOT NULL
86
+ )"""
87
+
88
+ _CREATE_FUNCTION_PARAM_INDEX_SQL = (
89
+ "CREATE INDEX IF NOT EXISTS idx_function_param ON function_calls(function_name, param_index)"
90
+ )
91
+
92
+ _CREATE_FUNCTION_FILE_INDEX_SQL = (
93
+ "CREATE INDEX IF NOT EXISTS idx_function_file ON function_calls(file_path)"
94
+ )
95
+
96
+ # String comparisons table schema (for scattered comparison detection)
97
+ _CREATE_COMPARISONS_TABLE_SQL = """CREATE TABLE IF NOT EXISTS string_comparisons (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ file_path TEXT NOT NULL,
100
+ line_number INTEGER NOT NULL,
101
+ column_number INTEGER NOT NULL,
102
+ variable_name TEXT NOT NULL,
103
+ compared_value TEXT NOT NULL,
104
+ operator TEXT NOT NULL
105
+ )"""
106
+
107
+ _CREATE_COMPARISONS_VAR_INDEX_SQL = (
108
+ "CREATE INDEX IF NOT EXISTS idx_comparison_var ON string_comparisons(variable_name)"
109
+ )
110
+
111
+ _CREATE_COMPARISONS_FILE_INDEX_SQL = (
112
+ "CREATE INDEX IF NOT EXISTS idx_comparison_file ON string_comparisons(file_path)"
113
+ )
114
+
115
+ # Row index constants for function_calls query results
116
+ _CALL_COL_FILE_PATH = 0
117
+ _CALL_COL_LINE_NUMBER = 1
118
+ _CALL_COL_COLUMN = 2
119
+ _CALL_COL_FUNCTION_NAME = 3
120
+ _CALL_COL_PARAM_INDEX = 4
121
+ _CALL_COL_STRING_VALUE = 5
122
+
123
+ # Row index constants for string_comparisons query results
124
+ _COMP_COL_FILE_PATH = 0
125
+ _COMP_COL_LINE_NUMBER = 1
126
+ _COMP_COL_COLUMN = 2
127
+ _COMP_COL_VARIABLE_NAME = 3
128
+ _COMP_COL_COMPARED_VALUE = 4
129
+ _COMP_COL_OPERATOR = 5
130
+
131
+
132
+ @dataclass
133
+ class StoredFunctionCall:
134
+ """Represents a function call with a string argument stored in SQLite.
135
+
136
+ Captures information about a function or method call where a string literal
137
+ is passed as an argument, enabling cross-file analysis to detect limited
138
+ value sets that should be enums.
139
+ """
140
+
141
+ file_path: Path
142
+ """Path to the file containing the call."""
143
+
144
+ line_number: int
145
+ """Line number where the call occurs (1-indexed)."""
146
+
147
+ column: int
148
+ """Column number where the call starts (0-indexed)."""
149
+
150
+ function_name: str
151
+ """Fully qualified function name (e.g., 'process' or 'obj.method')."""
152
+
153
+ param_index: int
154
+ """Index of the parameter receiving the string value (0-indexed)."""
155
+
156
+ string_value: str
157
+ """The string literal value passed to the function."""
158
+
159
+
160
+ @dataclass
161
+ class StoredComparison:
162
+ """Represents a string comparison stored in SQLite.
163
+
164
+ Captures information about a comparison like `if (env == "production")` to
165
+ enable cross-file analysis for detecting scattered string comparisons that
166
+ suggest missing enums.
167
+ """
168
+
169
+ file_path: Path
170
+ """Path to the file containing the comparison."""
171
+
172
+ line_number: int
173
+ """Line number where the comparison occurs (1-indexed)."""
174
+
175
+ column: int
176
+ """Column number where the comparison starts (0-indexed)."""
177
+
178
+ variable_name: str
179
+ """Variable name being compared (e.g., 'env' or 'self.status')."""
180
+
181
+ compared_value: str
182
+ """The string literal value being compared to."""
183
+
184
+ operator: str
185
+ """The comparison operator ('==', '!=', '===', '!==')."""
186
+
187
+
188
+ def _row_to_comparison(row: tuple) -> StoredComparison:
189
+ """Convert a database row tuple to StoredComparison.
190
+
191
+ Args:
192
+ row: Tuple from SQLite query result
193
+
194
+ Returns:
195
+ StoredComparison instance
196
+ """
197
+ return StoredComparison(
198
+ file_path=Path(row[_COMP_COL_FILE_PATH]),
199
+ line_number=row[_COMP_COL_LINE_NUMBER],
200
+ column=row[_COMP_COL_COLUMN],
201
+ variable_name=row[_COMP_COL_VARIABLE_NAME],
202
+ compared_value=row[_COMP_COL_COMPARED_VALUE],
203
+ operator=row[_COMP_COL_OPERATOR],
204
+ )
205
+
206
+
207
+ def _row_to_pattern(row: tuple) -> StoredPattern:
208
+ """Convert a database row tuple to StoredPattern.
209
+
210
+ Args:
211
+ row: Tuple from SQLite query result
212
+
213
+ Returns:
214
+ StoredPattern instance
215
+ """
216
+ return StoredPattern(
217
+ file_path=Path(row[_COL_FILE_PATH]),
218
+ line_number=row[_COL_LINE_NUMBER],
219
+ column=row[_COL_COLUMN],
220
+ variable_name=row[_COL_VARIABLE_NAME],
221
+ string_set_hash=row[_COL_STRING_SET_HASH],
222
+ string_values=json.loads(row[_COL_STRING_VALUES]),
223
+ pattern_type=row[_COL_PATTERN_TYPE],
224
+ details=row[_COL_DETAILS],
225
+ )
226
+
227
+
228
+ def _row_to_function_call(row: tuple) -> StoredFunctionCall:
229
+ """Convert a database row tuple to StoredFunctionCall.
230
+
231
+ Args:
232
+ row: Tuple from SQLite query result
233
+
234
+ Returns:
235
+ StoredFunctionCall instance
236
+ """
237
+ return StoredFunctionCall(
238
+ file_path=Path(row[_CALL_COL_FILE_PATH]),
239
+ line_number=row[_CALL_COL_LINE_NUMBER],
240
+ column=row[_CALL_COL_COLUMN],
241
+ function_name=row[_CALL_COL_FUNCTION_NAME],
242
+ param_index=row[_CALL_COL_PARAM_INDEX],
243
+ string_value=row[_CALL_COL_STRING_VALUE],
244
+ )
245
+
246
+
247
+ # pylint: disable=too-many-instance-attributes
248
+ # Justification: StoredPattern is a pure data transfer object for SQLite storage.
249
+ # All 8 fields are necessary: file location (3), variable info (1), hash/values (3), pattern type (1).
250
+ @dataclass
251
+ class StoredPattern:
252
+ """Represents a stringly-typed pattern stored in SQLite.
253
+
254
+ Captures all information needed to detect cross-file duplicates and generate
255
+ violations with meaningful context.
256
+ """
257
+
258
+ file_path: Path
259
+ """Path to the file containing the pattern."""
260
+
261
+ line_number: int
262
+ """Line number where the pattern occurs (1-indexed)."""
263
+
264
+ column: int
265
+ """Column number where the pattern starts (0-indexed)."""
266
+
267
+ variable_name: str | None
268
+ """Variable name involved in the pattern, if identifiable."""
269
+
270
+ string_set_hash: int
271
+ """Hash of the normalized string values for cross-file matching."""
272
+
273
+ string_values: list[str]
274
+ """Sorted list of string values in the pattern."""
275
+
276
+ pattern_type: str
277
+ """Type of pattern: membership_validation, equality_chain, etc."""
278
+
279
+ details: str
280
+ """Human-readable description of the detected pattern."""
281
+
282
+
283
+ class StringlyTypedStorage: # thailint: ignore srp
284
+ """SQLite-backed storage for stringly-typed pattern detection.
285
+
286
+ Stores patterns from analyzed files and provides queries to find patterns
287
+ that appear across multiple files, enabling cross-file duplicate detection.
288
+ """
289
+
290
+ def __init__(self, storage_mode: str = "memory") -> None:
291
+ """Initialize storage with SQLite database.
292
+
293
+ Args:
294
+ storage_mode: Storage mode - "memory" (default) or "tempfile"
295
+ """
296
+ self._storage_mode = storage_mode
297
+ self._tempfile = None
298
+
299
+ # Create SQLite connection based on storage mode
300
+ if storage_mode == "memory":
301
+ self._db = sqlite3.connect(":memory:")
302
+ elif storage_mode == "tempfile":
303
+ # pylint: disable=consider-using-with
304
+ # Justification: tempfile must remain open for SQLite connection lifetime.
305
+ # It is explicitly closed in close() method when storage is finalized.
306
+ self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True)
307
+ self._db = sqlite3.connect(self._tempfile.name)
308
+ else:
309
+ raise ValueError(f"Invalid storage_mode: {storage_mode}")
310
+
311
+ # Create schema inline
312
+ self._db.execute(_CREATE_TABLE_SQL)
313
+ self._db.execute(_CREATE_HASH_INDEX_SQL)
314
+ self._db.execute(_CREATE_FILE_INDEX_SQL)
315
+ self._db.execute(_CREATE_FUNCTION_CALLS_TABLE_SQL)
316
+ self._db.execute(_CREATE_FUNCTION_PARAM_INDEX_SQL)
317
+ self._db.execute(_CREATE_FUNCTION_FILE_INDEX_SQL)
318
+ self._db.execute(_CREATE_COMPARISONS_TABLE_SQL)
319
+ self._db.execute(_CREATE_COMPARISONS_VAR_INDEX_SQL)
320
+ self._db.execute(_CREATE_COMPARISONS_FILE_INDEX_SQL)
321
+ self._db.commit()
322
+
323
+ def add_pattern(self, pattern: StoredPattern) -> None:
324
+ """Add a single pattern to storage.
325
+
326
+ Args:
327
+ pattern: StoredPattern instance to store
328
+ """
329
+ self.add_patterns([pattern])
330
+
331
+ def add_patterns(self, patterns: list[StoredPattern]) -> None:
332
+ """Add multiple patterns to storage in a batch.
333
+
334
+ Args:
335
+ patterns: List of StoredPattern instances to store
336
+ """
337
+ if not patterns:
338
+ return
339
+
340
+ for pattern in patterns:
341
+ self._db.execute(
342
+ """INSERT OR REPLACE INTO string_validations
343
+ (file_path, line_number, column_number, variable_name,
344
+ string_set_hash, string_values, pattern_type, details)
345
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
346
+ (
347
+ str(pattern.file_path),
348
+ pattern.line_number,
349
+ pattern.column,
350
+ pattern.variable_name,
351
+ pattern.string_set_hash,
352
+ json.dumps(pattern.string_values),
353
+ pattern.pattern_type,
354
+ pattern.details,
355
+ ),
356
+ )
357
+
358
+ self._db.commit()
359
+
360
+ def get_duplicate_hashes(self, min_files: int = 2) -> list[int]:
361
+ """Get hash values that appear in min_files or more files.
362
+
363
+ Args:
364
+ min_files: Minimum number of distinct files (default: 2)
365
+
366
+ Returns:
367
+ List of hash values appearing in at least min_files files
368
+ """
369
+ cursor = self._db.execute(
370
+ """SELECT string_set_hash FROM string_validations
371
+ GROUP BY string_set_hash
372
+ HAVING COUNT(DISTINCT file_path) >= ?""",
373
+ (min_files,),
374
+ )
375
+ return [row[0] for row in cursor.fetchall()]
376
+
377
+ def get_patterns_by_hash(self, hash_value: int) -> list[StoredPattern]:
378
+ """Get all patterns with the given hash value.
379
+
380
+ Args:
381
+ hash_value: Hash value to search for
382
+
383
+ Returns:
384
+ List of StoredPattern instances with this hash
385
+ """
386
+ cursor = self._db.execute(
387
+ """SELECT file_path, line_number, column_number, variable_name,
388
+ string_set_hash, string_values, pattern_type, details
389
+ FROM string_validations
390
+ WHERE string_set_hash = ?
391
+ ORDER BY file_path, line_number""",
392
+ (hash_value,),
393
+ )
394
+
395
+ return [_row_to_pattern(row) for row in cursor.fetchall()]
396
+
397
+ def get_all_patterns(self) -> list[StoredPattern]:
398
+ """Get all stored patterns.
399
+
400
+ Returns:
401
+ List of all StoredPattern instances in storage
402
+ """
403
+ cursor = self._db.execute(
404
+ """SELECT file_path, line_number, column_number, variable_name,
405
+ string_set_hash, string_values, pattern_type, details
406
+ FROM string_validations
407
+ ORDER BY file_path, line_number"""
408
+ )
409
+
410
+ return [_row_to_pattern(row) for row in cursor.fetchall()]
411
+
412
+ def add_function_call(self, call: StoredFunctionCall) -> None:
413
+ """Add a single function call to storage.
414
+
415
+ Args:
416
+ call: StoredFunctionCall instance to store
417
+ """
418
+ self.add_function_calls([call])
419
+
420
+ def add_function_calls(self, calls: list[StoredFunctionCall]) -> None:
421
+ """Add multiple function calls to storage in a batch.
422
+
423
+ Args:
424
+ calls: List of StoredFunctionCall instances to store
425
+ """
426
+ if not calls:
427
+ return
428
+
429
+ for call in calls:
430
+ self._db.execute(
431
+ """INSERT INTO function_calls
432
+ (file_path, line_number, column_number, function_name,
433
+ param_index, string_value)
434
+ VALUES (?, ?, ?, ?, ?, ?)""",
435
+ (
436
+ str(call.file_path),
437
+ call.line_number,
438
+ call.column,
439
+ call.function_name,
440
+ call.param_index,
441
+ call.string_value,
442
+ ),
443
+ )
444
+
445
+ self._db.commit()
446
+
447
+ def get_limited_value_functions(
448
+ self, min_values: int, max_values: int, min_files: int = 1
449
+ ) -> list[tuple[str, int, set[str]]]:
450
+ """Get function+param combinations with limited unique string values.
451
+
452
+ Finds function parameters that are called with a limited set of string
453
+ values, suggesting they should be enums.
454
+
455
+ Args:
456
+ min_values: Minimum unique values to consider (default: 2)
457
+ max_values: Maximum unique values to consider (default: 6)
458
+ min_files: Minimum files the pattern must appear in (default: 1)
459
+
460
+ Returns:
461
+ List of (function_name, param_index, unique_values) tuples
462
+ """
463
+ cursor = self._db.execute(
464
+ """SELECT function_name, param_index, GROUP_CONCAT(DISTINCT string_value)
465
+ FROM function_calls
466
+ GROUP BY function_name, param_index
467
+ HAVING COUNT(DISTINCT string_value) >= ?
468
+ AND COUNT(DISTINCT string_value) <= ?
469
+ AND COUNT(DISTINCT file_path) >= ?""",
470
+ (min_values, max_values, min_files),
471
+ )
472
+
473
+ results: list[tuple[str, int, set[str]]] = []
474
+ for row in cursor.fetchall():
475
+ values = set(row[2].split(",")) if row[2] else set()
476
+ results.append((row[0], row[1], values))
477
+
478
+ return results
479
+
480
+ def get_calls_by_function(
481
+ self, function_name: str, param_index: int
482
+ ) -> list[StoredFunctionCall]:
483
+ """Get all calls for a specific function and parameter.
484
+
485
+ Args:
486
+ function_name: Name of the function
487
+ param_index: Index of the parameter
488
+
489
+ Returns:
490
+ List of StoredFunctionCall instances for this function+param
491
+ """
492
+ cursor = self._db.execute(
493
+ """SELECT file_path, line_number, column_number, function_name,
494
+ param_index, string_value
495
+ FROM function_calls
496
+ WHERE function_name = ? AND param_index = ?
497
+ ORDER BY file_path, line_number""",
498
+ (function_name, param_index),
499
+ )
500
+
501
+ return [_row_to_function_call(row) for row in cursor.fetchall()]
502
+
503
+ def get_all_function_calls(self) -> list[StoredFunctionCall]:
504
+ """Get all stored function calls.
505
+
506
+ Returns:
507
+ List of all StoredFunctionCall instances in storage
508
+ """
509
+ cursor = self._db.execute(
510
+ """SELECT file_path, line_number, column_number, function_name,
511
+ param_index, string_value
512
+ FROM function_calls
513
+ ORDER BY file_path, line_number"""
514
+ )
515
+
516
+ return [_row_to_function_call(row) for row in cursor.fetchall()]
517
+
518
+ def add_comparison(self, comparison: StoredComparison) -> None:
519
+ """Add a single comparison to storage.
520
+
521
+ Args:
522
+ comparison: StoredComparison instance to store
523
+ """
524
+ self.add_comparisons([comparison])
525
+
526
+ def add_comparisons(self, comparisons: list[StoredComparison]) -> None:
527
+ """Add multiple comparisons to storage in a batch.
528
+
529
+ Args:
530
+ comparisons: List of StoredComparison instances to store
531
+ """
532
+ if not comparisons:
533
+ return
534
+
535
+ for comparison in comparisons:
536
+ self._db.execute(
537
+ """INSERT INTO string_comparisons
538
+ (file_path, line_number, column_number, variable_name,
539
+ compared_value, operator)
540
+ VALUES (?, ?, ?, ?, ?, ?)""",
541
+ (
542
+ str(comparison.file_path),
543
+ comparison.line_number,
544
+ comparison.column,
545
+ comparison.variable_name,
546
+ comparison.compared_value,
547
+ comparison.operator,
548
+ ),
549
+ )
550
+
551
+ self._db.commit()
552
+
553
+ def get_variables_with_multiple_values(
554
+ self, min_values: int = 2, min_files: int = 1
555
+ ) -> list[tuple[str, set[str]]]:
556
+ """Get variables compared to multiple unique string values.
557
+
558
+ Finds variables that are compared to at least min_values unique strings,
559
+ suggesting they should be enums.
560
+
561
+ Args:
562
+ min_values: Minimum unique values to consider (default: 2)
563
+ min_files: Minimum files the pattern must appear in (default: 1)
564
+
565
+ Returns:
566
+ List of (variable_name, unique_values) tuples
567
+ """
568
+ cursor = self._db.execute(
569
+ """SELECT variable_name, GROUP_CONCAT(DISTINCT compared_value)
570
+ FROM string_comparisons
571
+ GROUP BY variable_name
572
+ HAVING COUNT(DISTINCT compared_value) >= ?
573
+ AND COUNT(DISTINCT file_path) >= ?""",
574
+ (min_values, min_files),
575
+ )
576
+
577
+ results: list[tuple[str, set[str]]] = []
578
+ for row in cursor.fetchall():
579
+ values = set(row[1].split(",")) if row[1] else set()
580
+ results.append((row[0], values))
581
+
582
+ return results
583
+
584
+ def get_comparisons_by_variable(self, variable_name: str) -> list[StoredComparison]:
585
+ """Get all comparisons for a specific variable.
586
+
587
+ Args:
588
+ variable_name: Name of the variable
589
+
590
+ Returns:
591
+ List of StoredComparison instances for this variable
592
+ """
593
+ cursor = self._db.execute(
594
+ """SELECT file_path, line_number, column_number, variable_name,
595
+ compared_value, operator
596
+ FROM string_comparisons
597
+ WHERE variable_name = ?
598
+ ORDER BY file_path, line_number""",
599
+ (variable_name,),
600
+ )
601
+
602
+ return [_row_to_comparison(row) for row in cursor.fetchall()]
603
+
604
+ def get_all_comparisons(self) -> list[StoredComparison]:
605
+ """Get all stored comparisons.
606
+
607
+ Returns:
608
+ List of all StoredComparison instances in storage
609
+ """
610
+ cursor = self._db.execute(
611
+ """SELECT file_path, line_number, column_number, variable_name,
612
+ compared_value, operator
613
+ FROM string_comparisons
614
+ ORDER BY file_path, line_number"""
615
+ )
616
+
617
+ return [_row_to_comparison(row) for row in cursor.fetchall()]
618
+
619
+ def clear(self) -> None:
620
+ """Clear all stored patterns, function calls, and comparisons."""
621
+ self._db.execute("DELETE FROM string_validations")
622
+ self._db.execute("DELETE FROM function_calls")
623
+ self._db.execute("DELETE FROM string_comparisons")
624
+ self._db.commit()
625
+
626
+ def close(self) -> None:
627
+ """Close database connection and cleanup tempfile if used."""
628
+ self._db.close()
629
+ if self._tempfile:
630
+ self._tempfile.close()
@@ -0,0 +1,45 @@
1
+ """
2
+ Purpose: Storage initialization for stringly-typed linter
3
+
4
+ Scope: Initializes StringlyTypedStorage with SQLite storage
5
+
6
+ Overview: Handles storage initialization for stringly-typed pattern detection. Creates SQLite
7
+ storage in memory mode for efficient cross-file analysis during a single linter run.
8
+ Separates initialization logic from main linter rule to maintain SRP compliance.
9
+
10
+ Dependencies: BaseLintContext, StringlyTypedConfig, StringlyTypedStorage
11
+
12
+ Exports: StorageInitializer class
13
+
14
+ Interfaces: StorageInitializer.initialize(context, config) -> StringlyTypedStorage
15
+
16
+ Implementation: Creates StringlyTypedStorage with memory mode for fast in-memory storage
17
+ """
18
+
19
+ from src.core.base import BaseLintContext
20
+
21
+ from .config import StringlyTypedConfig
22
+ from .storage import StringlyTypedStorage
23
+
24
+
25
+ class StorageInitializer:
26
+ """Initializes storage for stringly-typed pattern detection."""
27
+
28
+ def initialize(
29
+ self, context: BaseLintContext, config: StringlyTypedConfig
30
+ ) -> StringlyTypedStorage:
31
+ """Initialize storage based on configuration.
32
+
33
+ Args:
34
+ context: Lint context (reserved for future use)
35
+ config: Stringly-typed configuration (reserved for future storage_mode)
36
+
37
+ Returns:
38
+ StringlyTypedStorage instance with SQLite storage
39
+ """
40
+ # Context and config reserved for future storage_mode configuration
41
+ _ = context
42
+ _ = config
43
+
44
+ # Create SQLite storage in memory mode
45
+ return StringlyTypedStorage(storage_mode="memory")
@@ -0,0 +1,28 @@
1
+ """
2
+ Purpose: TypeScript stringly-typed pattern detection module
3
+
4
+ Scope: TypeScript and JavaScript code analysis for stringly-typed patterns
5
+
6
+ Overview: Provides TypeScript-specific analyzers for detecting stringly-typed code patterns
7
+ using tree-sitter AST analysis. Includes function call tracking for detecting function
8
+ parameters that consistently receive limited string value sets. Supports both TypeScript
9
+ and JavaScript files with shared detection logic. Designed to identify parameters that
10
+ should use enums instead of raw strings.
11
+
12
+ Dependencies: tree-sitter, tree-sitter-typescript, TypeScriptBaseAnalyzer
13
+
14
+ Exports: TypeScriptCallTracker, TypeScriptFunctionCallPattern
15
+
16
+ Interfaces: TypeScriptCallTracker.find_patterns(code) -> list[TypeScriptFunctionCallPattern]
17
+
18
+ Implementation: Tree-sitter based AST traversal for call expression analysis
19
+ """
20
+
21
+ from .analyzer import TypeScriptStringlyTypedAnalyzer
22
+ from .call_tracker import TypeScriptCallTracker, TypeScriptFunctionCallPattern
23
+
24
+ __all__ = [
25
+ "TypeScriptCallTracker",
26
+ "TypeScriptFunctionCallPattern",
27
+ "TypeScriptStringlyTypedAnalyzer",
28
+ ]