@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.
- package/README.md +297 -59
- package/dist/outcomes.js +376 -0
- package/dist/tools/ast.js +297 -0
- package/dist/tools/project.js +1209 -12
- package/package.json +10 -4
- package/rules/auth.yaml +126 -0
- package/rules/chat.yaml +876 -0
- package/rules/comments.yaml +906 -0
- package/rules/feed.yaml +2285 -30
- package/rules/live-data.yaml +60 -0
- package/rules/moderation.yaml +1286 -0
- package/rules/network.yaml +66 -0
- package/rules/push.yaml +467 -16
- package/rules/sdk-lifecycle.yaml +60 -0
- package/rules/security.yaml +60 -0
- package/skills/social-plus-vise/SKILL.md +148 -1
- package/dist/tools/patch.js +0 -67
|
@@ -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
|
+
}
|