@amityco/social-plus-vise 0.4.0 → 0.8.0

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.
@@ -0,0 +1,297 @@
1
+ /**
2
+ * AST-based deterministic analysis helpers using tree-sitter.
3
+ *
4
+ * This module provides syntactic (not type-resolving) analysis for source files.
5
+ * It is an additive layer alongside the existing regex-based validators.
6
+ *
7
+ * Policy: When AST and regex disagree (e.g., regex matches a comment), the regex
8
+ * result wins for now. AST cleanup of comment false-matches is Phase 4 work.
9
+ *
10
+ * Scope: Single-file, single-step identifier resolution only.
11
+ * No cross-file imports, no type inference, no function boundary traversal.
12
+ */
13
+ import Parser from "tree-sitter";
14
+ import TypeScriptGrammars from "tree-sitter-typescript";
15
+ import KotlinGrammar from "tree-sitter-kotlin";
16
+ const { typescript: tsGrammar, tsx: tsxGrammar } = TypeScriptGrammars;
17
+ /**
18
+ * Strip comments from source code using tree-sitter AST.
19
+ * Replaces comment spans with whitespace (preserving line structure).
20
+ * Returns original source unchanged if parsing fails.
21
+ */
22
+ export function stripComments(language, source) {
23
+ try {
24
+ const tree = parse(language, source);
25
+ const commentRanges = [];
26
+ collectComments(tree.rootNode, commentRanges);
27
+ if (commentRanges.length === 0)
28
+ return source;
29
+ // Replace comment ranges with spaces (preserve newlines for line numbers)
30
+ const chars = source.split("");
31
+ for (const { start, end } of commentRanges) {
32
+ for (let i = start; i < end && i < chars.length; i++) {
33
+ if (chars[i] !== "\n")
34
+ chars[i] = " ";
35
+ }
36
+ }
37
+ return chars.join("");
38
+ }
39
+ catch {
40
+ return source;
41
+ }
42
+ }
43
+ function collectComments(node, out) {
44
+ if (node.type === "comment" || node.type === "line_comment" || node.type === "multiline_comment" || node.type === "block_comment") {
45
+ out.push({ start: node.startIndex, end: node.endIndex });
46
+ return;
47
+ }
48
+ for (let i = 0; i < node.childCount; i++) {
49
+ const child = node.child(i);
50
+ if (child)
51
+ collectComments(child, out);
52
+ }
53
+ }
54
+ // Parser instances are reusable — one per language.
55
+ const parsers = new Map();
56
+ function getParser(language) {
57
+ let parser = parsers.get(language);
58
+ if (!parser) {
59
+ parser = new Parser();
60
+ if (language === "tsx")
61
+ parser.setLanguage(tsxGrammar);
62
+ else if (language === "kotlin")
63
+ parser.setLanguage(KotlinGrammar);
64
+ else
65
+ parser.setLanguage(tsGrammar);
66
+ parsers.set(language, parser);
67
+ }
68
+ return parser;
69
+ }
70
+ /**
71
+ * Parse source content into a tree-sitter syntax tree.
72
+ */
73
+ export function parse(language, source) {
74
+ const parser = getParser(language);
75
+ return parser.parse(source);
76
+ }
77
+ /**
78
+ * Find all call expressions in the tree whose callee matches a pattern.
79
+ * Returns normalised callee strings and argument nodes.
80
+ */
81
+ export function findCallExpressions(tree, calleePattern) {
82
+ const results = [];
83
+ walkTree(tree.rootNode, (node) => {
84
+ if (node.type !== "call_expression")
85
+ return;
86
+ // TypeScript: uses field names "function" and "arguments"
87
+ const calleeNode = node.childForFieldName("function");
88
+ if (calleeNode) {
89
+ const callee = normaliseCallee(calleeNode);
90
+ if (!callee || !calleePattern.test(callee))
91
+ return;
92
+ const argListNode = node.childForFieldName("arguments");
93
+ const args = [];
94
+ if (argListNode) {
95
+ for (let i = 0; i < argListNode.namedChildCount; i++) {
96
+ const child = argListNode.namedChild(i);
97
+ if (child)
98
+ args.push(child);
99
+ }
100
+ }
101
+ results.push({ callee, node, args });
102
+ return;
103
+ }
104
+ // Kotlin: call_expression = navigation_expression + call_suffix
105
+ const navNode = node.namedChild(0);
106
+ const suffixNode = node.namedChild(1);
107
+ if (!navNode || !suffixNode || suffixNode.type !== "call_suffix")
108
+ return;
109
+ const callee = normaliseKotlinCallee(navNode);
110
+ if (!callee || !calleePattern.test(callee))
111
+ return;
112
+ // Extract args from call_suffix → value_arguments → value_argument nodes
113
+ const args = [];
114
+ const valArgsNode = suffixNode.namedChild(0);
115
+ if (valArgsNode && valArgsNode.type === "value_arguments") {
116
+ for (let i = 0; i < valArgsNode.namedChildCount; i++) {
117
+ const valArg = valArgsNode.namedChild(i);
118
+ if (valArg && valArg.type === "value_argument") {
119
+ // The actual expression is inside value_argument
120
+ const expr = valArg.namedChild(0);
121
+ if (expr)
122
+ args.push(expr);
123
+ }
124
+ }
125
+ }
126
+ results.push({ callee, node, args });
127
+ });
128
+ return results;
129
+ }
130
+ /**
131
+ * Resolve an AST node to its literal string value within the same file.
132
+ *
133
+ * Handles:
134
+ * - String literals directly → returns the string value
135
+ * - Identifiers that reference a const/let/var with a string literal initializer
136
+ * (single-step resolution only)
137
+ *
138
+ * Returns undefined if the value cannot be statically resolved.
139
+ */
140
+ export function resolveLiteralValue(node, tree) {
141
+ // Direct string literal
142
+ const directValue = extractStringLiteral(node);
143
+ if (directValue !== undefined)
144
+ return directValue;
145
+ // Identifier — try to resolve to declaration in same file
146
+ // TypeScript uses "identifier", Kotlin uses "simple_identifier"
147
+ if (node.type === "identifier" || node.type === "simple_identifier") {
148
+ const name = node.text;
149
+ return resolveIdentifierToLiteral(name, tree.rootNode);
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Pick a specific named property from an object argument node.
155
+ * E.g., from `{ userId: HARDCODED }`, pick the value node for "userId".
156
+ */
157
+ export function pickObjectProperty(objectNode, propertyName) {
158
+ if (objectNode.type !== "object")
159
+ return undefined;
160
+ for (let i = 0; i < objectNode.namedChildCount; i++) {
161
+ const prop = objectNode.namedChild(i);
162
+ if (!prop || prop.type !== "pair")
163
+ continue;
164
+ const key = prop.childForFieldName("key");
165
+ if (key && key.text === propertyName) {
166
+ return prop.childForFieldName("value") ?? undefined;
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+ // ── Internal helpers ──────────────────────────────────────────────────────────
172
+ function walkTree(node, visit) {
173
+ visit(node);
174
+ for (let i = 0; i < node.namedChildCount; i++) {
175
+ const child = node.namedChild(i);
176
+ if (child)
177
+ walkTree(child, visit);
178
+ }
179
+ }
180
+ function normaliseCallee(node) {
181
+ if (node.type === "identifier")
182
+ return node.text;
183
+ if (node.type === "member_expression") {
184
+ const obj = node.childForFieldName("object");
185
+ const prop = node.childForFieldName("property");
186
+ if (obj && prop) {
187
+ const objName = normaliseCallee(obj);
188
+ return objName ? `${objName}.${prop.text}` : prop.text;
189
+ }
190
+ }
191
+ return undefined;
192
+ }
193
+ function normaliseKotlinCallee(node) {
194
+ if (node.type === "simple_identifier")
195
+ return node.text;
196
+ if (node.type === "navigation_expression") {
197
+ // navigation_expression has children: expression + navigation_suffix
198
+ // e.g., AmityCoreClient.login → nav_expr(simple_id("AmityCoreClient"), nav_suffix(".login"))
199
+ const parts = [];
200
+ for (let i = 0; i < node.namedChildCount; i++) {
201
+ const child = node.namedChild(i);
202
+ if (!child)
203
+ continue;
204
+ if (child.type === "simple_identifier") {
205
+ parts.push(child.text);
206
+ }
207
+ else if (child.type === "navigation_suffix") {
208
+ // navigation_suffix contains a simple_identifier after the dot
209
+ const id = child.namedChild(0);
210
+ if (id)
211
+ parts.push(id.text);
212
+ }
213
+ else if (child.type === "call_expression") {
214
+ // Chained: obj.method1().method2 — extract the last part from the chain
215
+ const innerCallee = normaliseKotlinCallee(child.namedChild(0));
216
+ if (innerCallee)
217
+ parts.push(innerCallee);
218
+ }
219
+ else {
220
+ const sub = normaliseKotlinCallee(child);
221
+ if (sub)
222
+ parts.push(sub);
223
+ }
224
+ }
225
+ return parts.length > 0 ? parts.join(".") : undefined;
226
+ }
227
+ return node.text?.split(/\s/)[0];
228
+ }
229
+ function extractStringLiteral(node) {
230
+ if (node.type === "string") {
231
+ // tree-sitter-typescript: string nodes include the quotes — strip them
232
+ const text = node.text;
233
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
234
+ return text.slice(1, -1);
235
+ }
236
+ // Template literal with no interpolations
237
+ if (text.startsWith("`") && text.endsWith("`") && !text.includes("${")) {
238
+ return text.slice(1, -1);
239
+ }
240
+ }
241
+ // template_string without substitutions
242
+ if (node.type === "template_string" && node.namedChildCount === 0) {
243
+ const text = node.text;
244
+ if (text.startsWith("`") && text.endsWith("`")) {
245
+ return text.slice(1, -1);
246
+ }
247
+ }
248
+ // Kotlin: string_literal contains string_content child
249
+ if (node.type === "string_literal") {
250
+ const contentNode = node.namedChild(0);
251
+ if (contentNode && contentNode.type === "string_content") {
252
+ return contentNode.text;
253
+ }
254
+ // Simple case — strip quotes from text
255
+ const text = node.text;
256
+ if (text.startsWith('"') && text.endsWith('"')) {
257
+ return text.slice(1, -1);
258
+ }
259
+ }
260
+ return undefined;
261
+ }
262
+ function resolveIdentifierToLiteral(name, root) {
263
+ let result;
264
+ walkTree(root, (node) => {
265
+ if (result !== undefined)
266
+ return;
267
+ // TypeScript/JS: variable_declarator with name + value fields
268
+ if (node.type === "variable_declarator") {
269
+ const nameNode = node.childForFieldName("name");
270
+ if (!nameNode || nameNode.text !== name)
271
+ return;
272
+ const valueNode = node.childForFieldName("value");
273
+ if (!valueNode)
274
+ return;
275
+ const literal = extractStringLiteral(valueNode);
276
+ if (literal !== undefined)
277
+ result = literal;
278
+ return;
279
+ }
280
+ // Kotlin: property_declaration with variable_declaration + string_literal
281
+ if (node.type === "property_declaration") {
282
+ const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
283
+ if (!varDecl)
284
+ return;
285
+ const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
286
+ if (!idNode || idNode.text !== name)
287
+ return;
288
+ const strLit = node.namedChildren.find((c) => c.type === "string_literal");
289
+ if (!strLit)
290
+ return;
291
+ const literal = extractStringLiteral(strLit);
292
+ if (literal !== undefined)
293
+ result = literal;
294
+ }
295
+ });
296
+ return result;
297
+ }