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,620 @@
1
+ # pylint: disable=too-many-lines
2
+ """
3
+ Purpose: SQLite storage manager for stringly-typed pattern detection
4
+
5
+ Scope: String validation pattern storage, function call tracking, comparison tracking, and
6
+ cross-file detection
7
+
8
+ Overview: Implements in-memory or temporary-file SQLite storage for stringly-typed pattern
9
+ detection. Stores string validation patterns with hash values computed from the string
10
+ values, enabling cross-file duplicate detection during a single linter run. Also tracks
11
+ function calls with string arguments to detect parameters that should be enums. Tracks
12
+ scattered string comparisons (`var == "string"`) to detect variables compared to multiple
13
+ string values across files. Supports both :memory: mode (fast, RAM-only) and tempfile mode
14
+ (disk-backed for large projects). No persistence between runs - storage is cleared when
15
+ linter completes. Includes indexes for fast hash lookups enabling efficient cross-file
16
+ detection.
17
+
18
+ Dependencies: Python sqlite3 module (stdlib), tempfile module (stdlib), pathlib.Path,
19
+ dataclasses, json module (stdlib)
20
+
21
+ Exports: StoredPattern dataclass, StoredFunctionCall dataclass, StoredComparison dataclass,
22
+ StringlyTypedStorage class
23
+
24
+ Interfaces: StringlyTypedStorage.__init__(storage_mode), add_pattern(pattern),
25
+ add_patterns(patterns), get_duplicate_hashes(min_files), get_patterns_by_hash(hash_value),
26
+ add_function_call(call), add_function_calls(calls), get_limited_value_functions(min_values,
27
+ max_values, min_files), get_calls_by_function(function_name, param_index),
28
+ add_comparison(comparison), add_comparisons(comparisons),
29
+ get_variables_with_multiple_values(min_values, min_files),
30
+ get_comparisons_by_variable(variable_name), get_all_comparisons(), clear(), close()
31
+
32
+ Implementation: SQLite with string_validations, function_calls, and string_comparisons tables,
33
+ indexed on string_set_hash, function_name+param_index, and variable_name for performance
34
+
35
+ Suppressions:
36
+ - too-many-lines: Storage module for three related data types with dataclasses, SQL schemas, and CRUD methods
37
+ - too-many-instance-attributes: StoredPattern is a pure DTO with 8 necessary fields for SQLite storage
38
+ - consider-using-with: NamedTemporaryFile must remain open for SQLite connection lifetime (closed in close())
39
+ - srp: Storage class manages SQLite for three pattern types (validations, calls, comparisons).
40
+ Splitting would fragment related storage operations.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ import sqlite3
47
+ import tempfile
48
+ from dataclasses import dataclass
49
+ from pathlib import Path
50
+
51
+ from src.core.constants import StorageMode
52
+
53
+ # Row index constants for SQLite query results
54
+ _COL_FILE_PATH = 0
55
+ _COL_LINE_NUMBER = 1
56
+ _COL_COLUMN = 2
57
+ _COL_VARIABLE_NAME = 3
58
+ _COL_STRING_SET_HASH = 4
59
+ _COL_STRING_VALUES = 5
60
+ _COL_PATTERN_TYPE = 6
61
+ _COL_DETAILS = 7
62
+
63
+ # Schema SQL for table creation
64
+ _CREATE_TABLE_SQL = """CREATE TABLE IF NOT EXISTS string_validations (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ file_path TEXT NOT NULL,
67
+ line_number INTEGER NOT NULL,
68
+ column_number INTEGER NOT NULL,
69
+ variable_name TEXT,
70
+ string_set_hash INTEGER NOT NULL,
71
+ string_values TEXT NOT NULL,
72
+ pattern_type TEXT NOT NULL,
73
+ details TEXT NOT NULL,
74
+ UNIQUE(file_path, line_number, column_number)
75
+ )"""
76
+
77
+ _CREATE_HASH_INDEX_SQL = (
78
+ "CREATE INDEX IF NOT EXISTS idx_string_hash ON string_validations(string_set_hash)"
79
+ )
80
+
81
+ _CREATE_FILE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_file_path ON string_validations(file_path)"
82
+
83
+ # Function calls table schema
84
+ _CREATE_FUNCTION_CALLS_TABLE_SQL = """CREATE TABLE IF NOT EXISTS function_calls (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ file_path TEXT NOT NULL,
87
+ line_number INTEGER NOT NULL,
88
+ column_number INTEGER NOT NULL,
89
+ function_name TEXT NOT NULL,
90
+ param_index INTEGER NOT NULL,
91
+ string_value TEXT NOT NULL
92
+ )"""
93
+
94
+ _CREATE_FUNCTION_PARAM_INDEX_SQL = (
95
+ "CREATE INDEX IF NOT EXISTS idx_function_param ON function_calls(function_name, param_index)"
96
+ )
97
+
98
+ _CREATE_FUNCTION_FILE_INDEX_SQL = (
99
+ "CREATE INDEX IF NOT EXISTS idx_function_file ON function_calls(file_path)"
100
+ )
101
+
102
+ # String comparisons table schema (for scattered comparison detection)
103
+ _CREATE_COMPARISONS_TABLE_SQL = """CREATE TABLE IF NOT EXISTS string_comparisons (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ file_path TEXT NOT NULL,
106
+ line_number INTEGER NOT NULL,
107
+ column_number INTEGER NOT NULL,
108
+ variable_name TEXT NOT NULL,
109
+ compared_value TEXT NOT NULL,
110
+ operator TEXT NOT NULL
111
+ )"""
112
+
113
+ _CREATE_COMPARISONS_VAR_INDEX_SQL = (
114
+ "CREATE INDEX IF NOT EXISTS idx_comparison_var ON string_comparisons(variable_name)"
115
+ )
116
+
117
+ _CREATE_COMPARISONS_FILE_INDEX_SQL = (
118
+ "CREATE INDEX IF NOT EXISTS idx_comparison_file ON string_comparisons(file_path)"
119
+ )
120
+
121
+ # Row index constants for function_calls query results
122
+ _CALL_COL_FILE_PATH = 0
123
+ _CALL_COL_LINE_NUMBER = 1
124
+ _CALL_COL_COLUMN = 2
125
+ _CALL_COL_FUNCTION_NAME = 3
126
+ _CALL_COL_PARAM_INDEX = 4
127
+ _CALL_COL_STRING_VALUE = 5
128
+
129
+ # Row index constants for string_comparisons query results
130
+ _COMP_COL_FILE_PATH = 0
131
+ _COMP_COL_LINE_NUMBER = 1
132
+ _COMP_COL_COLUMN = 2
133
+ _COMP_COL_VARIABLE_NAME = 3
134
+ _COMP_COL_COMPARED_VALUE = 4
135
+ _COMP_COL_OPERATOR = 5
136
+
137
+
138
+ @dataclass
139
+ class StoredFunctionCall:
140
+ """Represents a function call with a string argument stored in SQLite.
141
+
142
+ Captures information about a function or method call where a string literal
143
+ is passed as an argument, enabling cross-file analysis to detect limited
144
+ value sets that should be enums.
145
+ """
146
+
147
+ file_path: Path
148
+ """Path to the file containing the call."""
149
+
150
+ line_number: int
151
+ """Line number where the call occurs (1-indexed)."""
152
+
153
+ column: int
154
+ """Column number where the call starts (0-indexed)."""
155
+
156
+ function_name: str
157
+ """Fully qualified function name (e.g., 'process' or 'obj.method')."""
158
+
159
+ param_index: int
160
+ """Index of the parameter receiving the string value (0-indexed)."""
161
+
162
+ string_value: str
163
+ """The string literal value passed to the function."""
164
+
165
+
166
+ @dataclass
167
+ class StoredComparison:
168
+ """Represents a string comparison stored in SQLite.
169
+
170
+ Captures information about a comparison like `if (env == "production")` to
171
+ enable cross-file analysis for detecting scattered string comparisons that
172
+ suggest missing enums.
173
+ """
174
+
175
+ file_path: Path
176
+ """Path to the file containing the comparison."""
177
+
178
+ line_number: int
179
+ """Line number where the comparison occurs (1-indexed)."""
180
+
181
+ column: int
182
+ """Column number where the comparison starts (0-indexed)."""
183
+
184
+ variable_name: str
185
+ """Variable name being compared (e.g., 'env' or 'self.status')."""
186
+
187
+ compared_value: str
188
+ """The string literal value being compared to."""
189
+
190
+ operator: str
191
+ """The comparison operator ('==', '!=', '===', '!==')."""
192
+
193
+
194
+ def _row_to_comparison(row: tuple) -> StoredComparison:
195
+ """Convert a database row tuple to StoredComparison.
196
+
197
+ Args:
198
+ row: Tuple from SQLite query result
199
+
200
+ Returns:
201
+ StoredComparison instance
202
+ """
203
+ return StoredComparison(
204
+ file_path=Path(row[_COMP_COL_FILE_PATH]),
205
+ line_number=row[_COMP_COL_LINE_NUMBER],
206
+ column=row[_COMP_COL_COLUMN],
207
+ variable_name=row[_COMP_COL_VARIABLE_NAME],
208
+ compared_value=row[_COMP_COL_COMPARED_VALUE],
209
+ operator=row[_COMP_COL_OPERATOR],
210
+ )
211
+
212
+
213
+ def _row_to_pattern(row: tuple) -> StoredPattern:
214
+ """Convert a database row tuple to StoredPattern.
215
+
216
+ Args:
217
+ row: Tuple from SQLite query result
218
+
219
+ Returns:
220
+ StoredPattern instance
221
+ """
222
+ return StoredPattern(
223
+ file_path=Path(row[_COL_FILE_PATH]),
224
+ line_number=row[_COL_LINE_NUMBER],
225
+ column=row[_COL_COLUMN],
226
+ variable_name=row[_COL_VARIABLE_NAME],
227
+ string_set_hash=row[_COL_STRING_SET_HASH],
228
+ string_values=json.loads(row[_COL_STRING_VALUES]),
229
+ pattern_type=row[_COL_PATTERN_TYPE],
230
+ details=row[_COL_DETAILS],
231
+ )
232
+
233
+
234
+ def _row_to_function_call(row: tuple) -> StoredFunctionCall:
235
+ """Convert a database row tuple to StoredFunctionCall.
236
+
237
+ Args:
238
+ row: Tuple from SQLite query result
239
+
240
+ Returns:
241
+ StoredFunctionCall instance
242
+ """
243
+ return StoredFunctionCall(
244
+ file_path=Path(row[_CALL_COL_FILE_PATH]),
245
+ line_number=row[_CALL_COL_LINE_NUMBER],
246
+ column=row[_CALL_COL_COLUMN],
247
+ function_name=row[_CALL_COL_FUNCTION_NAME],
248
+ param_index=row[_CALL_COL_PARAM_INDEX],
249
+ string_value=row[_CALL_COL_STRING_VALUE],
250
+ )
251
+
252
+
253
+ @dataclass
254
+ class StoredPattern: # pylint: disable=too-many-instance-attributes
255
+ """Represents a stringly-typed pattern stored in SQLite.
256
+
257
+ Captures all information needed to detect cross-file duplicates and generate
258
+ violations with meaningful context.
259
+ """
260
+
261
+ file_path: Path
262
+ """Path to the file containing the pattern."""
263
+
264
+ line_number: int
265
+ """Line number where the pattern occurs (1-indexed)."""
266
+
267
+ column: int
268
+ """Column number where the pattern starts (0-indexed)."""
269
+
270
+ variable_name: str | None
271
+ """Variable name involved in the pattern, if identifiable."""
272
+
273
+ string_set_hash: int
274
+ """Hash of the normalized string values for cross-file matching."""
275
+
276
+ string_values: list[str]
277
+ """Sorted list of string values in the pattern."""
278
+
279
+ pattern_type: str
280
+ """Type of pattern: membership_validation, equality_chain, etc."""
281
+
282
+ details: str
283
+ """Human-readable description of the detected pattern."""
284
+
285
+
286
+ class StringlyTypedStorage: # thailint: ignore[srp]
287
+ """SQLite-backed storage for stringly-typed pattern detection.
288
+
289
+ Stores patterns from analyzed files and provides queries to find patterns
290
+ that appear across multiple files, enabling cross-file duplicate detection.
291
+ """
292
+
293
+ def __init__(self, storage_mode: str = "memory") -> None:
294
+ """Initialize storage with SQLite database.
295
+
296
+ Args:
297
+ storage_mode: Storage mode - "memory" (default) or "tempfile"
298
+ """
299
+ self._storage_mode = storage_mode
300
+ self._tempfile = None
301
+
302
+ # Create SQLite connection based on storage mode
303
+ if storage_mode == StorageMode.MEMORY:
304
+ self._db = sqlite3.connect(":memory:")
305
+ elif storage_mode == StorageMode.TEMPFILE:
306
+ self._tempfile = tempfile.NamedTemporaryFile(suffix=".db", delete=True) # pylint: disable=consider-using-with
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, json_group_array(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
+ return [(row[0], row[1], set(json.loads(row[2]))) for row in cursor.fetchall()]
474
+
475
+ def get_calls_by_function(
476
+ self, function_name: str, param_index: int
477
+ ) -> list[StoredFunctionCall]:
478
+ """Get all calls for a specific function and parameter.
479
+
480
+ Args:
481
+ function_name: Name of the function
482
+ param_index: Index of the parameter
483
+
484
+ Returns:
485
+ List of StoredFunctionCall instances for this function+param
486
+ """
487
+ cursor = self._db.execute(
488
+ """SELECT file_path, line_number, column_number, function_name,
489
+ param_index, string_value
490
+ FROM function_calls
491
+ WHERE function_name = ? AND param_index = ?
492
+ ORDER BY file_path, line_number""",
493
+ (function_name, param_index),
494
+ )
495
+
496
+ return [_row_to_function_call(row) for row in cursor.fetchall()]
497
+
498
+ def get_all_function_calls(self) -> list[StoredFunctionCall]:
499
+ """Get all stored function calls.
500
+
501
+ Returns:
502
+ List of all StoredFunctionCall instances in storage
503
+ """
504
+ cursor = self._db.execute(
505
+ """SELECT file_path, line_number, column_number, function_name,
506
+ param_index, string_value
507
+ FROM function_calls
508
+ ORDER BY file_path, line_number"""
509
+ )
510
+
511
+ return [_row_to_function_call(row) for row in cursor.fetchall()]
512
+
513
+ def add_comparison(self, comparison: StoredComparison) -> None:
514
+ """Add a single comparison to storage.
515
+
516
+ Args:
517
+ comparison: StoredComparison instance to store
518
+ """
519
+ self.add_comparisons([comparison])
520
+
521
+ def add_comparisons(self, comparisons: list[StoredComparison]) -> None:
522
+ """Add multiple comparisons to storage in a batch.
523
+
524
+ Args:
525
+ comparisons: List of StoredComparison instances to store
526
+ """
527
+ if not comparisons:
528
+ return
529
+
530
+ for comparison in comparisons:
531
+ self._db.execute(
532
+ """INSERT INTO string_comparisons
533
+ (file_path, line_number, column_number, variable_name,
534
+ compared_value, operator)
535
+ VALUES (?, ?, ?, ?, ?, ?)""",
536
+ (
537
+ str(comparison.file_path),
538
+ comparison.line_number,
539
+ comparison.column,
540
+ comparison.variable_name,
541
+ comparison.compared_value,
542
+ comparison.operator,
543
+ ),
544
+ )
545
+
546
+ self._db.commit()
547
+
548
+ def get_variables_with_multiple_values(
549
+ self, min_values: int = 2, min_files: int = 1
550
+ ) -> list[tuple[str, set[str]]]:
551
+ """Get variables compared to multiple unique string values.
552
+
553
+ Finds variables that are compared to at least min_values unique strings,
554
+ suggesting they should be enums.
555
+
556
+ Args:
557
+ min_values: Minimum unique values to consider (default: 2)
558
+ min_files: Minimum files the pattern must appear in (default: 1)
559
+
560
+ Returns:
561
+ List of (variable_name, unique_values) tuples
562
+ """
563
+ cursor = self._db.execute(
564
+ """SELECT variable_name, json_group_array(DISTINCT compared_value)
565
+ FROM string_comparisons
566
+ GROUP BY variable_name
567
+ HAVING COUNT(DISTINCT compared_value) >= ?
568
+ AND COUNT(DISTINCT file_path) >= ?""",
569
+ (min_values, min_files),
570
+ )
571
+
572
+ return [(row[0], set(json.loads(row[1]))) for row in cursor.fetchall()]
573
+
574
+ def get_comparisons_by_variable(self, variable_name: str) -> list[StoredComparison]:
575
+ """Get all comparisons for a specific variable.
576
+
577
+ Args:
578
+ variable_name: Name of the variable
579
+
580
+ Returns:
581
+ List of StoredComparison instances for this variable
582
+ """
583
+ cursor = self._db.execute(
584
+ """SELECT file_path, line_number, column_number, variable_name,
585
+ compared_value, operator
586
+ FROM string_comparisons
587
+ WHERE variable_name = ?
588
+ ORDER BY file_path, line_number""",
589
+ (variable_name,),
590
+ )
591
+
592
+ return [_row_to_comparison(row) for row in cursor.fetchall()]
593
+
594
+ def get_all_comparisons(self) -> list[StoredComparison]:
595
+ """Get all stored comparisons.
596
+
597
+ Returns:
598
+ List of all StoredComparison instances in storage
599
+ """
600
+ cursor = self._db.execute(
601
+ """SELECT file_path, line_number, column_number, variable_name,
602
+ compared_value, operator
603
+ FROM string_comparisons
604
+ ORDER BY file_path, line_number"""
605
+ )
606
+
607
+ return [_row_to_comparison(row) for row in cursor.fetchall()]
608
+
609
+ def clear(self) -> None:
610
+ """Clear all stored patterns, function calls, and comparisons."""
611
+ self._db.execute("DELETE FROM string_validations")
612
+ self._db.execute("DELETE FROM function_calls")
613
+ self._db.execute("DELETE FROM string_comparisons")
614
+ self._db.commit()
615
+
616
+ def close(self) -> None:
617
+ """Close database connection and cleanup tempfile if used."""
618
+ self._db.close()
619
+ if self._tempfile:
620
+ 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
+ ]