@mondaydotcomorg/atp-compiler 0.22.2 → 0.23.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,387 @@
1
+ import { describe, test, expect } from '@jest/globals';
2
+ import { analyzeApiCalls } from '../../src/api-call-analyzer';
3
+
4
+ describe('analyzeApiCalls', () => {
5
+ test('extracts a single api.<group>.<op>(...) call', () => {
6
+ const r = analyzeApiCalls(`return api.calendar.events_list({ calendarId: 'primary' });`);
7
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
8
+ expect(r.dynamicCallsDetected).toBe(false);
9
+ });
10
+
11
+ test('extracts nested await + multiple calls in the same group', () => {
12
+ const code = `
13
+ const c = await api.calendar.calendars_get({ calendarId: 'primary' });
14
+ return await api.calendar.events_list({ calendarId: c.id, maxResults: 10 });
15
+ `;
16
+ const r = analyzeApiCalls(code);
17
+ const sorted = r.apiCalls.slice().sort((a, b) => a.operationId.localeCompare(b.operationId));
18
+ expect(sorted).toEqual([
19
+ { apiGroup: 'calendar', operationId: 'calendars_get' },
20
+ { apiGroup: 'calendar', operationId: 'events_list' },
21
+ ]);
22
+ expect(r.dynamicCallsDetected).toBe(false);
23
+ });
24
+
25
+ test('extracts cross-group calls', () => {
26
+ const r = analyzeApiCalls(`
27
+ await api.calendar.events_list({});
28
+ await api.gmail.messages_list({});
29
+ `);
30
+ expect(r.apiCalls).toEqual(
31
+ expect.arrayContaining([
32
+ { apiGroup: 'calendar', operationId: 'events_list' },
33
+ { apiGroup: 'gmail', operationId: 'messages_list' },
34
+ ])
35
+ );
36
+ });
37
+
38
+ test('deduplicates repeated calls to the same api.<group>.<op>', () => {
39
+ const r = analyzeApiCalls(`
40
+ await api.calendar.events_list({ maxResults: 1 });
41
+ await api.calendar.events_list({ maxResults: 2 });
42
+ `);
43
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
44
+ });
45
+
46
+ test('flags dynamic dispatch via computed operation member', () => {
47
+ const r = analyzeApiCalls(`return api.calendar[fn]({});`);
48
+ expect(r.dynamicCallsDetected).toBe(true);
49
+ });
50
+
51
+ test('flags dynamic dispatch via computed group member', () => {
52
+ const r = analyzeApiCalls(`return api[g].events_list({});`);
53
+ expect(r.dynamicCallsDetected).toBe(true);
54
+ });
55
+
56
+ test('flags destructured api: const { calendar } = api', () => {
57
+ const r = analyzeApiCalls(`
58
+ const { calendar } = api;
59
+ return calendar.events_list({});
60
+ `);
61
+ expect(r.dynamicCallsDetected).toBe(true);
62
+ });
63
+
64
+ test('flags destructure-with-rename: const { calendar: c } = api', () => {
65
+ const r = analyzeApiCalls(`
66
+ const { calendar: c } = api;
67
+ return c.events_list({});
68
+ `);
69
+ expect(r.dynamicCallsDetected).toBe(true);
70
+ });
71
+
72
+ test('flags aliasing: const x = api.calendar', () => {
73
+ const r = analyzeApiCalls(`
74
+ const x = api.calendar;
75
+ return x.events_list({});
76
+ `);
77
+ expect(r.dynamicCallsDetected).toBe(true);
78
+ });
79
+
80
+ test('flags aliasing: const x = api', () => {
81
+ const r = analyzeApiCalls(`
82
+ const x = api;
83
+ return x.calendar.events_list({});
84
+ `);
85
+ expect(r.dynamicCallsDetected).toBe(true);
86
+ });
87
+
88
+ test('returns empty calls + no dynamic flag for trivial code', () => {
89
+ const r = analyzeApiCalls(`return 42;`);
90
+ expect(r.apiCalls).toEqual([]);
91
+ expect(r.dynamicCallsDetected).toBe(false);
92
+ });
93
+
94
+ test('fails closed with dynamicCallsDetected=true on syntax error', () => {
95
+ const r = analyzeApiCalls(`return api.calendar.events_list(`);
96
+ expect(r.dynamicCallsDetected).toBe(true);
97
+ expect(r.apiCalls).toEqual([]);
98
+ });
99
+
100
+ test('handles empty string input', () => {
101
+ const r = analyzeApiCalls('');
102
+ expect(r.apiCalls).toEqual([]);
103
+ expect(r.dynamicCallsDetected).toBe(false);
104
+ });
105
+
106
+ // ────────────────────────────────────────────────────────────────────────
107
+ // Dedup — same (group, op) through different syntactic paths
108
+ // ────────────────────────────────────────────────────────────────────────
109
+
110
+ describe('dedup', () => {
111
+ test('same op called in both branches of a ternary', () => {
112
+ const r = analyzeApiCalls(`
113
+ return flag
114
+ ? api.calendar.events_list({ a: 1 })
115
+ : api.calendar.events_list({ a: 2 });
116
+ `);
117
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
118
+ expect(r.dynamicCallsDetected).toBe(false);
119
+ });
120
+
121
+ test('same op called inside a loop is reported once', () => {
122
+ const r = analyzeApiCalls(`
123
+ for (const id of ids) {
124
+ await api.calendar.events_list({ calendarId: id });
125
+ }
126
+ `);
127
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
128
+ });
129
+
130
+ test('same op across try/catch/finally reports once', () => {
131
+ const r = analyzeApiCalls(`
132
+ try {
133
+ await api.calendar.events_list({});
134
+ } catch (e) {
135
+ await api.calendar.events_list({ retry: true });
136
+ } finally {
137
+ api.calendar.events_list({ cleanup: true });
138
+ }
139
+ `);
140
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
141
+ });
142
+
143
+ test('same op across multiple statements is reported once', () => {
144
+ const r = analyzeApiCalls(`
145
+ const a = await api.calendar.events_list({ maxResults: 1 });
146
+ const b = await api.calendar.events_list({ maxResults: 2 });
147
+ const c = await api.calendar.events_list({ maxResults: 3 });
148
+ `);
149
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
150
+ });
151
+ });
152
+
153
+ // ────────────────────────────────────────────────────────────────────────
154
+ // Complex control-flow / realistic code shapes
155
+ // ────────────────────────────────────────────────────────────────────────
156
+
157
+ describe('complex control flow', () => {
158
+ test('IIFE (immediately-invoked async function)', () => {
159
+ const r = analyzeApiCalls(`
160
+ return (async () => await api.calendar.events_list({}))();
161
+ `);
162
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
163
+ expect(r.dynamicCallsDetected).toBe(false);
164
+ });
165
+
166
+ test('Promise.all with multiple awaits', () => {
167
+ const r = analyzeApiCalls(`
168
+ return await Promise.all([
169
+ api.calendar.events_list({}),
170
+ api.gmail.messages_list({}),
171
+ api.drive.files_list({}),
172
+ ]);
173
+ `);
174
+ const sorted = r.apiCalls
175
+ .slice()
176
+ .sort((a, b) => (a.apiGroup + '.' + a.operationId).localeCompare(b.apiGroup + '.' + b.operationId));
177
+ expect(sorted).toEqual([
178
+ { apiGroup: 'calendar', operationId: 'events_list' },
179
+ { apiGroup: 'drive', operationId: 'files_list' },
180
+ { apiGroup: 'gmail', operationId: 'messages_list' },
181
+ ]);
182
+ });
183
+
184
+ test('arrow inside .then() chain', () => {
185
+ const r = analyzeApiCalls(`
186
+ return api.calendar.events_list({})
187
+ .then((list) => api.calendar.events_get({ eventId: list.items[0].id }))
188
+ .then((ev) => api.calendar.calendars_get({ calendarId: ev.calendarId }));
189
+ `);
190
+ expect(r.apiCalls.sort((a, b) => a.operationId.localeCompare(b.operationId))).toEqual([
191
+ { apiGroup: 'calendar', operationId: 'calendars_get' },
192
+ { apiGroup: 'calendar', operationId: 'events_get' },
193
+ { apiGroup: 'calendar', operationId: 'events_list' },
194
+ ]);
195
+ });
196
+
197
+ test('class method body', () => {
198
+ const r = analyzeApiCalls(`
199
+ class Scheduler {
200
+ async listEvents() {
201
+ return await api.calendar.events_list({ calendarId: 'primary' });
202
+ }
203
+ }
204
+ return new Scheduler().listEvents();
205
+ `);
206
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
207
+ });
208
+
209
+ test('nested try/catch/finally across multiple groups', () => {
210
+ const r = analyzeApiCalls(`
211
+ try {
212
+ await api.calendar.events_list({});
213
+ } catch (e) {
214
+ await api.gmail.messages_list({ label: 'errors' });
215
+ } finally {
216
+ api.drive.files_list({});
217
+ }
218
+ `);
219
+ expect(r.apiCalls.length).toBe(3);
220
+ });
221
+
222
+ test('switch statement with api calls per case', () => {
223
+ const r = analyzeApiCalls(`
224
+ switch (kind) {
225
+ case 'a': return api.calendar.events_list({});
226
+ case 'b': return api.calendar.events_get({ eventId: x });
227
+ default: return api.calendar.calendars_get({ calendarId: 'primary' });
228
+ }
229
+ `);
230
+ expect(r.apiCalls.length).toBe(3);
231
+ });
232
+ });
233
+
234
+ // ────────────────────────────────────────────────────────────────────────
235
+ // Multi-group realism — many sources + many ops
236
+ // ────────────────────────────────────────────────────────────────────────
237
+
238
+ describe('multi-group', () => {
239
+ test('realistic Google Suite workload (6 groups, 11 ops)', () => {
240
+ const r = analyzeApiCalls(`
241
+ const events = await api.calendar.events_list({ calendarId: 'primary' });
242
+ const cal = await api.calendar.calendars_get({ calendarId: 'primary' });
243
+ const messages = await api.gmail.messages_list({});
244
+ const thread = await api.gmail.threads_get({ id: '1' });
245
+ const sheet = await api.sheets.spreadsheets_get({ spreadsheetId: 'x' });
246
+ const values = await api.sheets.spreadsheets_values_get({ spreadsheetId: 'x', range: 'A1' });
247
+ const drive = await api.drive.files_list({});
248
+ const file = await api.drive.files_get({ fileId: 'x' });
249
+ const deck = await api.slides.presentations_get({ presentationId: 'x' });
250
+ const doc = await api.docs.documents_get({ documentId: 'x' });
251
+ const batch = await api.sheets.spreadsheets_batchUpdate({ spreadsheetId: 'x' });
252
+ return { events, cal, messages, thread, sheet, values, drive, file, deck, doc, batch };
253
+ `);
254
+
255
+ const groupsTouched = new Set(r.apiCalls.map((c) => c.apiGroup));
256
+ expect(groupsTouched).toEqual(new Set(['calendar', 'gmail', 'sheets', 'drive', 'slides', 'docs']));
257
+ expect(r.apiCalls).toHaveLength(11);
258
+ expect(r.dynamicCallsDetected).toBe(false);
259
+ });
260
+
261
+ test('mixed groups with some dedup', () => {
262
+ const r = analyzeApiCalls(`
263
+ await api.calendar.events_list({});
264
+ await api.gmail.messages_list({});
265
+ await api.calendar.events_list({}); // dup
266
+ await api.gmail.messages_list({}); // dup
267
+ await api.calendar.events_get({ eventId: 'x' });
268
+ `);
269
+ expect(r.apiCalls).toHaveLength(3);
270
+ expect(r.apiCalls.map((c) => c.apiGroup + '.' + c.operationId).sort()).toEqual([
271
+ 'calendar.events_get',
272
+ 'calendar.events_list',
273
+ 'gmail.messages_list',
274
+ ]);
275
+ });
276
+ });
277
+
278
+ // ────────────────────────────────────────────────────────────────────────
279
+ // Additional dynamic-dispatch escape patterns
280
+ // ────────────────────────────────────────────────────────────────────────
281
+
282
+ describe('dynamic dispatch — additional escapes', () => {
283
+ test('flags optional-chained operation member', () => {
284
+ const r = analyzeApiCalls(`return api.calendar?.events_list?.({});`);
285
+ // Detected statically OR dynamic — either outcome denies a grant
286
+ // that doesn't cover the target. This implementation DETECTS
287
+ // because the static path is resolvable; no dynamic flag needed.
288
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
289
+ });
290
+
291
+ test('flags .call() redirection as a regular static call', () => {
292
+ const r = analyzeApiCalls(`api.calendar.events_list.call(null, {});`);
293
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
294
+ });
295
+
296
+ test('flags .apply() redirection', () => {
297
+ const r = analyzeApiCalls(`api.calendar.events_list.apply(null, [{}]);`);
298
+ expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
299
+ });
300
+
301
+ test('flags .bind() + later call', () => {
302
+ const r = analyzeApiCalls(`
303
+ const boundList = api.calendar.events_list.bind(null);
304
+ return boundList({});
305
+ `);
306
+ // .bind still surfaces the underlying call target.
307
+ expect(r.apiCalls).toContainEqual({ apiGroup: 'calendar', operationId: 'events_list' });
308
+ });
309
+
310
+ test('flags Object.values(api) as dynamic', () => {
311
+ const r = analyzeApiCalls(`
312
+ const groups = Object.values(api);
313
+ return groups.map((g) => Object.keys(g));
314
+ `);
315
+ expect(r.dynamicCallsDetected).toBe(true);
316
+ });
317
+
318
+ test('flags Object.keys(api) as dynamic', () => {
319
+ const r = analyzeApiCalls(`return Object.keys(api);`);
320
+ expect(r.dynamicCallsDetected).toBe(true);
321
+ });
322
+
323
+ test('flags spread {...api} as dynamic', () => {
324
+ const r = analyzeApiCalls(`const x = { ...api }; return x;`);
325
+ expect(r.dynamicCallsDetected).toBe(true);
326
+ });
327
+
328
+ test('flags passing api as function argument', () => {
329
+ const r = analyzeApiCalls(`
330
+ const use = (a) => a.calendar.events_list({});
331
+ return use(api);
332
+ `);
333
+ expect(r.dynamicCallsDetected).toBe(true);
334
+ });
335
+
336
+ test('flags returning bare api', () => {
337
+ const r = analyzeApiCalls(`return api;`);
338
+ expect(r.dynamicCallsDetected).toBe(true);
339
+ });
340
+ });
341
+
342
+ // ────────────────────────────────────────────────────────────────────────
343
+ // Not-false-positive — identifiers named "api" that aren't the global
344
+ // ────────────────────────────────────────────────────────────────────────
345
+
346
+ describe('does not false-positive on unrelated "api" identifiers', () => {
347
+ test('this.api.foo.bar(...) is ignored (not the global api)', () => {
348
+ const r = analyzeApiCalls(`return this.api.foo.bar({});`);
349
+ expect(r.apiCalls).toEqual([]);
350
+ expect(r.dynamicCallsDetected).toBe(false);
351
+ });
352
+
353
+ test('someObj.api.foo.bar(...) is ignored', () => {
354
+ const r = analyzeApiCalls(`
355
+ const wrapper = { api: null };
356
+ return wrapper.api?.foo?.bar?.({});
357
+ `);
358
+ expect(r.apiCalls).toEqual([]);
359
+ expect(r.dynamicCallsDetected).toBe(false);
360
+ });
361
+
362
+ test('deep api.x.y.z(...) — not valid ATP syntax — silently ignored', () => {
363
+ // Sandbox's runtime namespace would throw on this; static analyzer
364
+ // ignores it so we don't over-flag invalid code as dynamic.
365
+ const r = analyzeApiCalls(`return api.x.y.z({});`);
366
+ expect(r.apiCalls).toEqual([]);
367
+ expect(r.dynamicCallsDetected).toBe(false);
368
+ });
369
+ });
370
+
371
+ // ────────────────────────────────────────────────────────────────────────
372
+ // Idempotency — deterministic output across calls
373
+ // ────────────────────────────────────────────────────────────────────────
374
+
375
+ describe('idempotency', () => {
376
+ test('calling analyzeApiCalls twice on same code yields identical output', () => {
377
+ const code = `
378
+ await api.calendar.events_list({});
379
+ await api.gmail.messages_list({});
380
+ const { calendar } = api;
381
+ `;
382
+ const a = analyzeApiCalls(code);
383
+ const b = analyzeApiCalls(code);
384
+ expect(a).toEqual(b);
385
+ });
386
+ });
387
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pre-dispatch static analysis of ATP agent code.
3
+ *
4
+ * Extracts every `api.<group>.<op>(...)` call chain from submitted code
5
+ * and flags patterns that defeat static analysis (dynamic dispatch,
6
+ * aliasing, destructuring).
7
+ *
8
+ * Intended caller: governance layers that need to know WHICH api groups
9
+ * and operations code will touch BEFORE dispatching it to the sandbox.
10
+ * Paired with runtime `filterApiGroups` enforcement in atp-server for
11
+ * defense-in-depth; the static pass catches unauthorized references
12
+ * up-front without paying sandbox startup cost.
13
+ *
14
+ * @example
15
+ * const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
16
+ * for (const call of apiCalls) {
17
+ * // check (call.apiGroup, call.operationId) against a grant
18
+ * }
19
+ * if (dynamicCallsDetected) {
20
+ * // deny unless the grant explicitly allows dynamic dispatch
21
+ * }
22
+ */
23
+ export interface DetectedApiCall {
24
+ apiGroup: string;
25
+ operationId: string;
26
+ }
27
+ export interface AnalysisResult {
28
+ /** Unique `(apiGroup, operationId)` pairs statically visible in the code. */
29
+ apiCalls: DetectedApiCall[];
30
+ /**
31
+ * True iff the code contains patterns we cannot statically resolve to a
32
+ * concrete `(apiGroup, operationId)` — e.g. `api[varName].fn(...)`,
33
+ * destructuring (`const { calendar } = api`), or aliasing
34
+ * (`const x = api.calendar`). Governance layers should fail-closed on
35
+ * this flag unless the caller's policy opts into dynamic dispatch.
36
+ */
37
+ dynamicCallsDetected: boolean;
38
+ }
39
+ /**
40
+ * Analyze agent code and return its statically-visible api.* call set plus a
41
+ * dynamic-dispatch flag. Pure function, no I/O, fail-closed on parse errors
42
+ * (returns `{ apiCalls: [], dynamicCallsDetected: true }`).
43
+ */
44
+ export declare function analyzeApiCalls(code: string): AnalysisResult;
45
+ //# sourceMappingURL=api-call-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-call-analyzer.d.ts","sourceRoot":"","sources":["../src/api-call-analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC9B,6EAA6E;IAC7E,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;;;;OAMG;IACH,oBAAoB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAgL5D"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Pre-dispatch static analysis of ATP agent code.
3
+ *
4
+ * Extracts every `api.<group>.<op>(...)` call chain from submitted code
5
+ * and flags patterns that defeat static analysis (dynamic dispatch,
6
+ * aliasing, destructuring).
7
+ *
8
+ * Intended caller: governance layers that need to know WHICH api groups
9
+ * and operations code will touch BEFORE dispatching it to the sandbox.
10
+ * Paired with runtime `filterApiGroups` enforcement in atp-server for
11
+ * defense-in-depth; the static pass catches unauthorized references
12
+ * up-front without paying sandbox startup cost.
13
+ *
14
+ * @example
15
+ * const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
16
+ * for (const call of apiCalls) {
17
+ * // check (call.apiGroup, call.operationId) against a grant
18
+ * }
19
+ * if (dynamicCallsDetected) {
20
+ * // deny unless the grant explicitly allows dynamic dispatch
21
+ * }
22
+ */
23
+ import { parse } from '@babel/parser';
24
+ // @babel/traverse exports default; interop handles the CJS/ESM quirk
25
+ // (see https://github.com/babel/babel/issues/13855).
26
+ import _traverse from '@babel/traverse';
27
+ import * as t from '@babel/types';
28
+ const traverse = (_traverse.default ??
29
+ _traverse);
30
+ /**
31
+ * Analyze agent code and return its statically-visible api.* call set plus a
32
+ * dynamic-dispatch flag. Pure function, no I/O, fail-closed on parse errors
33
+ * (returns `{ apiCalls: [], dynamicCallsDetected: true }`).
34
+ */
35
+ export function analyzeApiCalls(code) {
36
+ const calls = [];
37
+ const seen = new Set();
38
+ let dynamicCallsDetected = false;
39
+ let ast;
40
+ try {
41
+ ast = parse(code, {
42
+ sourceType: 'module',
43
+ allowReturnOutsideFunction: true,
44
+ plugins: ['typescript'],
45
+ });
46
+ }
47
+ catch {
48
+ // Fail-closed: syntax error → treat as dynamic so governance denies.
49
+ return { apiCalls: [], dynamicCallsDetected: true };
50
+ }
51
+ // Helper records a static api.<group>.<op>(...) call, or flips
52
+ // dynamicCallsDetected when the call expression escapes static resolution.
53
+ const tryRecordCall = (calleeNode) => {
54
+ let callee = calleeNode;
55
+ // Unwrap one `.call` / `.apply` / `.bind` redirection:
56
+ // api.calendar.events_list.call(null, {...})
57
+ // has callee = MemberExpression { object: api.calendar.events_list, property: 'call' }
58
+ if ((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
59
+ !callee.computed &&
60
+ t.isIdentifier(callee.property) &&
61
+ (callee.property.name === 'call' ||
62
+ callee.property.name === 'apply' ||
63
+ callee.property.name === 'bind')) {
64
+ callee = callee.object;
65
+ }
66
+ if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
67
+ return;
68
+ const groupExpr = callee.object;
69
+ if (!t.isMemberExpression(groupExpr) && !t.isOptionalMemberExpression(groupExpr))
70
+ return;
71
+ if (!t.isIdentifier(groupExpr.object, { name: 'api' }))
72
+ return;
73
+ // api[groupVar].op(...) or api.group[fnVar](...) → dynamic
74
+ if (groupExpr.computed || callee.computed) {
75
+ dynamicCallsDetected = true;
76
+ return;
77
+ }
78
+ const groupNode = groupExpr.property;
79
+ const opNode = callee.property;
80
+ if (!t.isIdentifier(groupNode) || !t.isIdentifier(opNode)) {
81
+ dynamicCallsDetected = true;
82
+ return;
83
+ }
84
+ const key = `${groupNode.name}.${opNode.name}`;
85
+ if (!seen.has(key)) {
86
+ seen.add(key);
87
+ calls.push({ apiGroup: groupNode.name, operationId: opNode.name });
88
+ }
89
+ };
90
+ try {
91
+ traverse(ast, {
92
+ // Assignments / destructures that alias `api` or `api.<group>` — any of
93
+ // these lets the code reach an api group via an opaque identifier later.
94
+ VariableDeclarator(path) {
95
+ const init = path.node.init;
96
+ if (!init)
97
+ return;
98
+ // const { calendar } = api
99
+ if (t.isObjectPattern(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
100
+ dynamicCallsDetected = true;
101
+ return;
102
+ }
103
+ // const x = api
104
+ if (t.isIdentifier(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
105
+ dynamicCallsDetected = true;
106
+ return;
107
+ }
108
+ // const x = api.<group> OR const x = api['<group>']
109
+ if (t.isIdentifier(path.node.id) &&
110
+ t.isMemberExpression(init) &&
111
+ t.isIdentifier(init.object, { name: 'api' })) {
112
+ dynamicCallsDetected = true;
113
+ }
114
+ },
115
+ // Any other mention of `api` that hands it off to an opaque consumer:
116
+ // fn(api) — alias escape via function argument
117
+ // Object.values(api) / keys(…) — enumeration
118
+ // { ...api } / [ ...api ] — spread
119
+ // return api — caller gets the alias
120
+ // api = x (reassignment) — later reads hit a different object
121
+ //
122
+ // The api.<group>.<op>(...) pattern is recognised by the CallExpression
123
+ // visitor below; skip it here via parent-shape whitelisting.
124
+ Identifier(path) {
125
+ if (path.node.name !== 'api')
126
+ return;
127
+ // Skip the PROPERTY position of a member expression — e.g.
128
+ // `this.api`, `window.api`, `someObj.api`. That's not the
129
+ // global `api` binding we care about.
130
+ if ((t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
131
+ path.parent.property === path.node &&
132
+ !path.parent.computed) {
133
+ return;
134
+ }
135
+ // `api.<group>` (non-computed member) — safe, handled by CallExpression.
136
+ if ((t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
137
+ path.parent.object === path.node &&
138
+ !path.parent.computed) {
139
+ return;
140
+ }
141
+ // Left-hand side of `const x = api` / `const { calendar } = api`
142
+ // — handled by VariableDeclarator above (dynamic flag set there).
143
+ if (t.isVariableDeclarator(path.parent) && path.parent.init === path.node) {
144
+ return;
145
+ }
146
+ // Skip declaration positions where `api` is a local binding name,
147
+ // not a reference:
148
+ // { api: value } — object property key
149
+ // function f(api) { ... } — param name
150
+ // class { api() {...} } — method name
151
+ // function api() {} — function name
152
+ // class api {} — class name
153
+ if ((t.isObjectProperty(path.parent) || t.isObjectMethod(path.parent)) &&
154
+ path.parent.key === path.node &&
155
+ !path.parent.computed) {
156
+ return;
157
+ }
158
+ if (t.isClassMethod(path.parent) && path.parent.key === path.node && !path.parent.computed) {
159
+ return;
160
+ }
161
+ if ((t.isFunctionDeclaration(path.parent) ||
162
+ t.isFunctionExpression(path.parent) ||
163
+ t.isClassDeclaration(path.parent) ||
164
+ t.isClassExpression(path.parent)) &&
165
+ path.parent.id === path.node) {
166
+ return;
167
+ }
168
+ if (path.parentPath?.isFunction() && path.listKey === 'params') {
169
+ return;
170
+ }
171
+ if (t.isImportSpecifier(path.parent) || t.isImportDefaultSpecifier(path.parent) || t.isImportNamespaceSpecifier(path.parent)) {
172
+ return;
173
+ }
174
+ // Anything else (`Object.values(api)`, `fn(api)`, `{ ...api }`,
175
+ // `return api`, `api = ...`) escapes static resolution.
176
+ dynamicCallsDetected = true;
177
+ },
178
+ CallExpression(path) {
179
+ tryRecordCall(path.node.callee);
180
+ },
181
+ OptionalCallExpression(path) {
182
+ tryRecordCall(path.node.callee);
183
+ },
184
+ });
185
+ }
186
+ catch {
187
+ // Visitor error → fail-closed (should be unreachable under our plugin set).
188
+ return { apiCalls: [], dynamicCallsDetected: true };
189
+ }
190
+ return { apiCalls: calls, dynamicCallsDetected };
191
+ }
192
+ //# sourceMappingURL=api-call-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-call-analyzer.js","sourceRoot":"","sources":["../src/api-call-analyzer.ts"],"names":[],"mappings":"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"}