thailint 0.5.0__py3-none-any.whl → 0.15.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +1 -0
- src/analyzers/__init__.py +4 -3
- src/analyzers/ast_utils.py +54 -0
- src/analyzers/rust_base.py +155 -0
- src/analyzers/rust_context.py +141 -0
- src/analyzers/typescript_base.py +4 -0
- src/cli/__init__.py +30 -0
- src/cli/__main__.py +22 -0
- src/cli/config.py +480 -0
- src/cli/config_merge.py +241 -0
- src/cli/linters/__init__.py +67 -0
- src/cli/linters/code_patterns.py +270 -0
- src/cli/linters/code_smells.py +342 -0
- src/cli/linters/documentation.py +83 -0
- src/cli/linters/performance.py +287 -0
- src/cli/linters/shared.py +331 -0
- src/cli/linters/structure.py +327 -0
- src/cli/linters/structure_quality.py +328 -0
- src/cli/main.py +120 -0
- src/cli/utils.py +395 -0
- src/cli_main.py +37 -0
- src/config.py +38 -25
- src/core/base.py +7 -2
- src/core/cli_utils.py +19 -2
- src/core/config_parser.py +5 -2
- src/core/constants.py +54 -0
- src/core/linter_utils.py +95 -6
- src/core/python_lint_rule.py +101 -0
- src/core/registry.py +1 -1
- src/core/rule_discovery.py +147 -84
- src/core/types.py +13 -0
- src/core/violation_builder.py +78 -15
- src/core/violation_utils.py +69 -0
- src/formatters/__init__.py +22 -0
- src/formatters/sarif.py +202 -0
- src/linter_config/directive_markers.py +109 -0
- src/linter_config/ignore.py +254 -395
- src/linter_config/loader.py +45 -12
- src/linter_config/pattern_utils.py +65 -0
- src/linter_config/rule_matcher.py +89 -0
- src/linters/collection_pipeline/__init__.py +90 -0
- src/linters/collection_pipeline/any_all_analyzer.py +281 -0
- src/linters/collection_pipeline/ast_utils.py +40 -0
- src/linters/collection_pipeline/config.py +75 -0
- src/linters/collection_pipeline/continue_analyzer.py +94 -0
- src/linters/collection_pipeline/detector.py +360 -0
- src/linters/collection_pipeline/filter_map_analyzer.py +402 -0
- src/linters/collection_pipeline/linter.py +420 -0
- src/linters/collection_pipeline/suggestion_builder.py +130 -0
- src/linters/cqs/__init__.py +54 -0
- src/linters/cqs/config.py +55 -0
- src/linters/cqs/function_analyzer.py +201 -0
- src/linters/cqs/input_detector.py +139 -0
- src/linters/cqs/linter.py +159 -0
- src/linters/cqs/output_detector.py +84 -0
- src/linters/cqs/python_analyzer.py +54 -0
- src/linters/cqs/types.py +82 -0
- src/linters/cqs/typescript_cqs_analyzer.py +61 -0
- src/linters/cqs/typescript_function_analyzer.py +192 -0
- src/linters/cqs/typescript_input_detector.py +203 -0
- src/linters/cqs/typescript_output_detector.py +117 -0
- src/linters/cqs/violation_builder.py +94 -0
- src/linters/dry/base_token_analyzer.py +16 -9
- src/linters/dry/block_filter.py +120 -20
- src/linters/dry/block_grouper.py +4 -0
- src/linters/dry/cache.py +104 -10
- src/linters/dry/cache_query.py +4 -0
- src/linters/dry/config.py +54 -11
- src/linters/dry/constant.py +92 -0
- src/linters/dry/constant_matcher.py +223 -0
- src/linters/dry/constant_violation_builder.py +98 -0
- src/linters/dry/duplicate_storage.py +5 -4
- src/linters/dry/file_analyzer.py +4 -2
- src/linters/dry/inline_ignore.py +7 -16
- src/linters/dry/linter.py +183 -48
- src/linters/dry/python_analyzer.py +60 -439
- src/linters/dry/python_constant_extractor.py +100 -0
- src/linters/dry/single_statement_detector.py +417 -0
- src/linters/dry/token_hasher.py +116 -112
- src/linters/dry/typescript_analyzer.py +68 -382
- src/linters/dry/typescript_constant_extractor.py +138 -0
- src/linters/dry/typescript_statement_detector.py +255 -0
- src/linters/dry/typescript_value_extractor.py +70 -0
- src/linters/dry/violation_builder.py +4 -0
- src/linters/dry/violation_filter.py +5 -4
- src/linters/dry/violation_generator.py +71 -14
- src/linters/file_header/atemporal_detector.py +68 -50
- src/linters/file_header/base_parser.py +93 -0
- src/linters/file_header/bash_parser.py +66 -0
- src/linters/file_header/config.py +90 -16
- src/linters/file_header/css_parser.py +70 -0
- src/linters/file_header/field_validator.py +36 -33
- src/linters/file_header/linter.py +140 -144
- src/linters/file_header/markdown_parser.py +130 -0
- src/linters/file_header/python_parser.py +14 -58
- src/linters/file_header/typescript_parser.py +73 -0
- src/linters/file_header/violation_builder.py +13 -12
- src/linters/file_placement/config_loader.py +3 -1
- src/linters/file_placement/directory_matcher.py +4 -0
- src/linters/file_placement/linter.py +66 -34
- src/linters/file_placement/pattern_matcher.py +41 -6
- src/linters/file_placement/pattern_validator.py +31 -12
- src/linters/file_placement/rule_checker.py +12 -7
- src/linters/lazy_ignores/__init__.py +43 -0
- src/linters/lazy_ignores/config.py +74 -0
- src/linters/lazy_ignores/directive_utils.py +164 -0
- src/linters/lazy_ignores/header_parser.py +177 -0
- src/linters/lazy_ignores/linter.py +158 -0
- src/linters/lazy_ignores/matcher.py +168 -0
- src/linters/lazy_ignores/python_analyzer.py +209 -0
- src/linters/lazy_ignores/rule_id_utils.py +180 -0
- src/linters/lazy_ignores/skip_detector.py +298 -0
- src/linters/lazy_ignores/types.py +71 -0
- src/linters/lazy_ignores/typescript_analyzer.py +146 -0
- src/linters/lazy_ignores/violation_builder.py +135 -0
- src/linters/lbyl/__init__.py +31 -0
- src/linters/lbyl/config.py +63 -0
- src/linters/lbyl/linter.py +67 -0
- src/linters/lbyl/pattern_detectors/__init__.py +53 -0
- src/linters/lbyl/pattern_detectors/base.py +63 -0
- src/linters/lbyl/pattern_detectors/dict_key_detector.py +107 -0
- src/linters/lbyl/pattern_detectors/division_check_detector.py +232 -0
- src/linters/lbyl/pattern_detectors/file_exists_detector.py +220 -0
- src/linters/lbyl/pattern_detectors/hasattr_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/isinstance_detector.py +119 -0
- src/linters/lbyl/pattern_detectors/len_check_detector.py +173 -0
- src/linters/lbyl/pattern_detectors/none_check_detector.py +146 -0
- src/linters/lbyl/pattern_detectors/string_validator_detector.py +145 -0
- src/linters/lbyl/python_analyzer.py +215 -0
- src/linters/lbyl/violation_builder.py +354 -0
- src/linters/magic_numbers/context_analyzer.py +227 -225
- src/linters/magic_numbers/linter.py +28 -82
- src/linters/magic_numbers/python_analyzer.py +4 -16
- src/linters/magic_numbers/typescript_analyzer.py +9 -12
- src/linters/magic_numbers/typescript_ignore_checker.py +81 -0
- src/linters/method_property/__init__.py +49 -0
- src/linters/method_property/config.py +138 -0
- src/linters/method_property/linter.py +414 -0
- src/linters/method_property/python_analyzer.py +473 -0
- src/linters/method_property/violation_builder.py +119 -0
- src/linters/nesting/linter.py +24 -16
- src/linters/nesting/python_analyzer.py +4 -0
- src/linters/nesting/typescript_analyzer.py +6 -12
- src/linters/nesting/violation_builder.py +1 -0
- src/linters/performance/__init__.py +91 -0
- src/linters/performance/config.py +43 -0
- src/linters/performance/constants.py +49 -0
- src/linters/performance/linter.py +149 -0
- src/linters/performance/python_analyzer.py +365 -0
- src/linters/performance/regex_analyzer.py +312 -0
- src/linters/performance/regex_linter.py +139 -0
- src/linters/performance/typescript_analyzer.py +236 -0
- src/linters/performance/violation_builder.py +160 -0
- src/linters/print_statements/config.py +7 -12
- src/linters/print_statements/linter.py +26 -43
- src/linters/print_statements/python_analyzer.py +91 -93
- src/linters/print_statements/typescript_analyzer.py +15 -25
- src/linters/print_statements/violation_builder.py +12 -14
- src/linters/srp/class_analyzer.py +11 -7
- src/linters/srp/heuristics.py +56 -22
- src/linters/srp/linter.py +15 -16
- src/linters/srp/python_analyzer.py +55 -20
- src/linters/srp/typescript_metrics_calculator.py +110 -50
- src/linters/stateless_class/__init__.py +25 -0
- src/linters/stateless_class/config.py +58 -0
- src/linters/stateless_class/linter.py +349 -0
- src/linters/stateless_class/python_analyzer.py +290 -0
- src/linters/stringly_typed/__init__.py +36 -0
- src/linters/stringly_typed/config.py +189 -0
- src/linters/stringly_typed/context_filter.py +451 -0
- src/linters/stringly_typed/function_call_violation_builder.py +135 -0
- src/linters/stringly_typed/ignore_checker.py +100 -0
- src/linters/stringly_typed/ignore_utils.py +51 -0
- src/linters/stringly_typed/linter.py +376 -0
- src/linters/stringly_typed/python/__init__.py +33 -0
- src/linters/stringly_typed/python/analyzer.py +348 -0
- src/linters/stringly_typed/python/call_tracker.py +175 -0
- src/linters/stringly_typed/python/comparison_tracker.py +257 -0
- src/linters/stringly_typed/python/condition_extractor.py +134 -0
- src/linters/stringly_typed/python/conditional_detector.py +179 -0
- src/linters/stringly_typed/python/constants.py +21 -0
- src/linters/stringly_typed/python/match_analyzer.py +94 -0
- src/linters/stringly_typed/python/validation_detector.py +189 -0
- src/linters/stringly_typed/python/variable_extractor.py +96 -0
- src/linters/stringly_typed/storage.py +620 -0
- src/linters/stringly_typed/storage_initializer.py +45 -0
- src/linters/stringly_typed/typescript/__init__.py +28 -0
- src/linters/stringly_typed/typescript/analyzer.py +157 -0
- src/linters/stringly_typed/typescript/call_tracker.py +335 -0
- src/linters/stringly_typed/typescript/comparison_tracker.py +378 -0
- src/linters/stringly_typed/violation_generator.py +419 -0
- src/orchestrator/core.py +252 -14
- src/orchestrator/language_detector.py +5 -3
- src/templates/thailint_config_template.yaml +196 -0
- src/utils/project_root.py +3 -0
- thailint-0.15.3.dist-info/METADATA +187 -0
- thailint-0.15.3.dist-info/RECORD +226 -0
- thailint-0.15.3.dist-info/entry_points.txt +4 -0
- src/cli.py +0 -1665
- thailint-0.5.0.dist-info/METADATA +0 -1286
- thailint-0.5.0.dist-info/RECORD +0 -96
- thailint-0.5.0.dist-info/entry_points.txt +0 -4
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/WHEEL +0 -0
- {thailint-0.5.0.dist-info → thailint-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,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
|
+
]
|