@mondaydotcomorg/z2h-cli 0.30.0 → 0.31.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/dist/backend/handlers.d.ts +3 -0
- package/dist/backend/handlers.d.ts.map +1 -1
- package/dist/backend/handlers.js +7 -4
- package/dist/backend/parser/__tests__/ast.test.d.ts.map +1 -0
- package/dist/backend/parser/__tests__/parse.test.d.ts.map +1 -0
- package/dist/backend/parser/api-signatures.d.ts +4 -0
- package/dist/backend/parser/api-signatures.d.ts.map +1 -0
- package/dist/backend/parser/api-signatures.js +31 -0
- package/dist/backend/parser/api-usage.d.ts +5 -0
- package/dist/backend/parser/api-usage.d.ts.map +1 -0
- package/dist/backend/parser/api-usage.js +61 -0
- package/dist/backend/parser/bindings.d.ts +5 -0
- package/dist/backend/parser/bindings.d.ts.map +1 -0
- package/dist/backend/parser/bindings.js +84 -0
- package/dist/backend/parser/handler-declaration.d.ts.map +1 -0
- package/dist/backend/parser/member-path.d.ts +3 -0
- package/dist/backend/parser/member-path.d.ts.map +1 -0
- package/dist/backend/parser/member-path.js +31 -0
- package/dist/backend/parser/messages.d.ts.map +1 -0
- package/dist/backend/parser/parse.d.ts.map +1 -0
- package/dist/backend/{parse.js → parser/parse.js} +9 -9
- package/dist/backend/parser/static-value.d.ts +6 -0
- package/dist/backend/parser/static-value.d.ts.map +1 -0
- package/dist/backend/parser/static-value.js +117 -0
- package/dist/backend/parser/types.d.ts +53 -0
- package/dist/backend/parser/types.d.ts.map +1 -0
- package/dist/backend/parser/types.js +1 -0
- package/dist/backend/parser/utils.d.ts +19 -0
- package/dist/backend/parser/utils.d.ts.map +1 -0
- package/dist/backend/parser/utils.js +97 -0
- package/dist/backend/validate.d.ts +3 -3
- package/dist/backend/validate.d.ts.map +1 -1
- package/dist/backend/validate.js +18 -18
- package/dist/commands/backend.d.ts +2 -13
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +30 -21
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +2 -8
- package/dist/esm/backend/handlers.d.ts +3 -0
- package/dist/esm/backend/handlers.d.ts.map +1 -1
- package/dist/esm/backend/handlers.mjs +7 -4
- package/dist/esm/backend/parser/__tests__/ast.test.d.ts.map +1 -0
- package/dist/esm/backend/parser/__tests__/parse.test.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-signatures.d.ts +4 -0
- package/dist/esm/backend/parser/api-signatures.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-signatures.mjs +24 -0
- package/dist/esm/backend/parser/api-usage.d.ts +5 -0
- package/dist/esm/backend/parser/api-usage.d.ts.map +1 -0
- package/dist/esm/backend/parser/api-usage.mjs +58 -0
- package/dist/esm/backend/parser/bindings.d.ts +5 -0
- package/dist/esm/backend/parser/bindings.d.ts.map +1 -0
- package/dist/esm/backend/parser/bindings.mjs +81 -0
- package/dist/esm/backend/parser/handler-declaration.d.ts.map +1 -0
- package/dist/esm/backend/parser/member-path.d.ts +3 -0
- package/dist/esm/backend/parser/member-path.d.ts.map +1 -0
- package/dist/esm/backend/parser/member-path.mjs +29 -0
- package/dist/esm/backend/parser/messages.d.ts.map +1 -0
- package/dist/esm/backend/parser/parse.d.ts.map +1 -0
- package/dist/esm/backend/{parse.mjs → parser/parse.mjs} +1 -1
- package/dist/esm/backend/parser/static-value.d.ts +6 -0
- package/dist/esm/backend/parser/static-value.d.ts.map +1 -0
- package/dist/esm/backend/parser/static-value.mjs +114 -0
- package/dist/esm/backend/parser/types.d.ts +53 -0
- package/dist/esm/backend/parser/types.d.ts.map +1 -0
- package/dist/esm/backend/parser/types.mjs +1 -0
- package/dist/esm/backend/parser/utils.d.ts +19 -0
- package/dist/esm/backend/parser/utils.d.ts.map +1 -0
- package/dist/esm/backend/parser/utils.mjs +88 -0
- package/dist/esm/backend/validate.d.ts +3 -3
- package/dist/esm/backend/validate.d.ts.map +1 -1
- package/dist/esm/backend/validate.mjs +8 -8
- package/dist/esm/commands/backend.d.ts +2 -13
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +31 -22
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +1 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +3 -8
- package/dist/esm/templates/app/usage.mjs +5 -0
- package/dist/esm/util/machine-identity.d.ts +2 -3
- package/dist/esm/util/machine-identity.d.ts.map +1 -1
- package/dist/esm/util/machine-identity.mjs +7 -4
- package/dist/templates/app/usage.js +7 -0
- package/dist/util/machine-identity.d.ts +2 -3
- package/dist/util/machine-identity.d.ts.map +1 -1
- package/dist/util/machine-identity.js +11 -3
- package/handler-api/signatures.json +26 -0
- package/package.json +1 -1
- package/src/backend/__tests__/handlers.test.ts +13 -2
- package/src/backend/__tests__/validate.test.ts +54 -0
- package/src/backend/handlers.ts +10 -5
- package/src/backend/parser/__tests__/ast.test.ts +252 -0
- package/src/backend/{__tests__ → parser/__tests__}/parse.test.ts +2 -2
- package/src/backend/parser/api-signatures.ts +23 -0
- package/src/backend/parser/api-usage.ts +73 -0
- package/src/backend/parser/bindings.ts +87 -0
- package/src/backend/parser/member-path.ts +31 -0
- package/src/backend/{parse.ts → parser/parse.ts} +1 -1
- package/src/backend/parser/static-value.ts +145 -0
- package/src/backend/parser/types.ts +66 -0
- package/src/backend/parser/utils.ts +106 -0
- package/src/backend/validate.ts +12 -8
- package/src/commands/__tests__/backend-deploy.test.ts +70 -5
- package/src/commands/__tests__/backend-validate.test.ts +11 -1
- package/src/commands/__tests__/create.test.ts +1 -0
- package/src/commands/backend.ts +31 -20
- package/src/commands/create.ts +1 -0
- package/src/commands/deploy.ts +3 -9
- package/src/templates/app/usage.json +1 -0
- package/src/util/machine-identity.ts +7 -3
- package/dist/backend/__tests__/ast.test.d.ts.map +0 -1
- package/dist/backend/__tests__/parse.test.d.ts.map +0 -1
- package/dist/backend/ast.d.ts +0 -39
- package/dist/backend/ast.d.ts.map +0 -1
- package/dist/backend/ast.js +0 -149
- package/dist/backend/handler-declaration.d.ts.map +0 -1
- package/dist/backend/messages.d.ts.map +0 -1
- package/dist/backend/parse.d.ts.map +0 -1
- package/dist/esm/backend/__tests__/ast.test.d.ts.map +0 -1
- package/dist/esm/backend/__tests__/parse.test.d.ts.map +0 -1
- package/dist/esm/backend/ast.d.ts +0 -39
- package/dist/esm/backend/ast.d.ts.map +0 -1
- package/dist/esm/backend/ast.mjs +0 -144
- package/dist/esm/backend/handler-declaration.d.ts.map +0 -1
- package/dist/esm/backend/messages.d.ts.map +0 -1
- package/dist/esm/backend/parse.d.ts.map +0 -1
- package/src/backend/__tests__/ast.test.ts +0 -53
- package/src/backend/ast.ts +0 -201
- /package/dist/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
- /package/dist/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
- /package/dist/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
- /package/dist/backend/{handler-declaration.js → parser/handler-declaration.js} +0 -0
- /package/dist/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
- /package/dist/backend/{messages.js → parser/messages.js} +0 -0
- /package/dist/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
- /package/dist/esm/backend/{__tests__ → parser/__tests__}/ast.test.d.ts +0 -0
- /package/dist/esm/backend/{__tests__ → parser/__tests__}/parse.test.d.ts +0 -0
- /package/dist/esm/backend/{handler-declaration.d.ts → parser/handler-declaration.d.ts} +0 -0
- /package/dist/esm/backend/{handler-declaration.mjs → parser/handler-declaration.mjs} +0 -0
- /package/dist/esm/backend/{messages.d.ts → parser/messages.d.ts} +0 -0
- /package/dist/esm/backend/{messages.mjs → parser/messages.mjs} +0 -0
- /package/dist/esm/backend/{parse.d.ts → parser/parse.d.ts} +0 -0
- /package/src/backend/{handler-declaration.ts → parser/handler-declaration.ts} +0 -0
- /package/src/backend/{messages.ts → parser/messages.ts} +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { parse, type ExpressionStatement, type Program } from 'acorn';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { collectApiUsages } from '../api-usage';
|
|
4
|
+
import { collectBindings } from '../bindings';
|
|
5
|
+
import { createPathResolver } from '../member-path';
|
|
6
|
+
|
|
7
|
+
const program = (source: string): Program => parse(source, { ecmaVersion: 'latest', sourceType: 'script' });
|
|
8
|
+
|
|
9
|
+
const SIGNATURES = { 'snowflake.query': ['sql', 'params', 'options'] };
|
|
10
|
+
|
|
11
|
+
const queryCalls = (source: string) => {
|
|
12
|
+
const p = program(source);
|
|
13
|
+
const bindings = collectBindings(p);
|
|
14
|
+
return collectApiUsages(p, bindings, createPathResolver(bindings, 'ctx'), source, SIGNATURES).snowflake.query;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const querySql = (source: string) => queryCalls(source).map(call => call.sql);
|
|
18
|
+
|
|
19
|
+
const resolveLast = (source: string, contextParam?: string) => {
|
|
20
|
+
const p = program(source);
|
|
21
|
+
const last = p.body[p.body.length - 1] as ExpressionStatement;
|
|
22
|
+
return createPathResolver(collectBindings(p), contextParam).pathOf(last.expression);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('createPathResolver', () => {
|
|
26
|
+
it('returns the literal chain when nothing is aliased', () => {
|
|
27
|
+
expect(resolveLast('ctx.api.v1.db.get;')).toBe('ctx.api.v1.db.get');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('treats computed string literals as static keys', () => {
|
|
31
|
+
expect(resolveLast('ctx.api.v1["db"].get;')).toBe('ctx.api.v1.db.get');
|
|
32
|
+
expect(resolveLast("const { ['db']: store } = ctx.api.v1; store.get;")).toBe('ctx.api.v1.db.get');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns undefined for dynamic links and non-identifier roots', () => {
|
|
36
|
+
expect(resolveLast('ctx.api.v1[db].get;')).toBeUndefined();
|
|
37
|
+
expect(resolveLast('fn().db;')).toBeUndefined();
|
|
38
|
+
expect(resolveLast('"x";')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rewrites the handler context parameter to ctx', () => {
|
|
42
|
+
expect(resolveLast('context.api.v1.db.get;', 'context')).toBe('ctx.api.v1.db.get');
|
|
43
|
+
expect(resolveLast('const sf = context.api.v1.snowflake; sf.query;', 'context')).toBe('ctx.api.v1.snowflake.query');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('follows const aliases, including destructuring and chains', () => {
|
|
47
|
+
expect(resolveLast('const sf = ctx.api.v1.snowflake; sf.query;')).toBe('ctx.api.v1.snowflake.query');
|
|
48
|
+
expect(resolveLast('const { v1 } = ctx.api; v1.db;')).toBe('ctx.api.v1.db');
|
|
49
|
+
expect(resolveLast('const { v1: { db: store } } = ctx.api; store.get;')).toBe('ctx.api.v1.db.get');
|
|
50
|
+
expect(resolveLast('const a = ctx.api; const b = a.v1; const c = b.db; c;')).toBe('ctx.api.v1.db');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('leaves unpinnable names as written', () => {
|
|
54
|
+
expect(resolveLast('let sf = ctx.api.v1.snowflake; sf.query;')).toBe('sf.query');
|
|
55
|
+
expect(resolveLast('const sf = ctx.api.v1.snowflake; sf = 1; sf.query;')).toBe('sf.query');
|
|
56
|
+
expect(resolveLast('const [sf] = ctx.api; sf.query;')).toBe('sf.query');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resolves a const that a function param of the same name shadows only inside that function', () => {
|
|
60
|
+
expect(resolveLast('const sf = ctx.api.v1.snowflake; function f(sf) {} sf.query;')).toBe(
|
|
61
|
+
'ctx.api.v1.snowflake.query'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('resolves a const that a block-scoped const of the same name shadows only inside that block', () => {
|
|
66
|
+
expect(resolveLast('const sf = ctx.api.v1.snowflake; { const sf = 1; } sf.query;')).toBe(
|
|
67
|
+
'ctx.api.v1.snowflake.query'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('resolves through a const alias even when a catch param reuses the name', () => {
|
|
72
|
+
expect(resolveLast('const sf = ctx.api.v1.snowflake; try {} catch (sf) {} sf.query;')).toBe(
|
|
73
|
+
'ctx.api.v1.snowflake.query'
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not loop on circular aliases', () => {
|
|
78
|
+
expect(resolveLast('const a = b.x; const b = a.y; a.z;')).toBe('a.y.x.z');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('collectApiUsages — argument names and values', () => {
|
|
83
|
+
it('keys arguments by the documented parameter names, falling back to source text for dynamic object property values', () => {
|
|
84
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { x: ctx.input.x });')).toEqual([
|
|
85
|
+
{ sql: 'SELECT 1', params: { x: 'ctx.input.x' } },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('falls back to positional keys past the signature or on an undocumented method', () => {
|
|
90
|
+
const source = 'ctx.api.v1.snowflake.query("SELECT 1", p, o, "extra"); ctx.api.v1.other.thing("a", "b");';
|
|
91
|
+
const p = program(source);
|
|
92
|
+
const bindings = collectBindings(p);
|
|
93
|
+
const usages = collectApiUsages(p, bindings, createPathResolver(bindings, 'ctx'), source, SIGNATURES);
|
|
94
|
+
expect(usages.snowflake.query[0]).toEqual({ sql: 'SELECT 1', params: 'p', options: 'o', arg3: 'extra' });
|
|
95
|
+
expect(usages.other.thing).toEqual([{ arg0: 'a', arg1: 'b' }]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('resolves string literals, `+` concatenation, and a static numeric argument', () => {
|
|
99
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT " + \'1\', { x: ctx.input.x }, 5);')).toEqual([
|
|
100
|
+
{ sql: 'SELECT 1', params: { x: 'ctx.input.x' }, options: 5 },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('follows a const SQL variable and inlines the constants its template references', () => {
|
|
105
|
+
const source = [
|
|
106
|
+
'const WHERE = " WHERE x = :x";',
|
|
107
|
+
'const sql = `SELECT 1${WHERE}`;',
|
|
108
|
+
'ctx.api.v1.snowflake.query(sql, params);',
|
|
109
|
+
].join('\n');
|
|
110
|
+
expect(queryCalls(source)).toEqual([{ sql: 'SELECT 1 WHERE x = :x', params: 'params' }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('picks the declaration in scope at each call when the same name is declared per branch', () => {
|
|
114
|
+
const source = [
|
|
115
|
+
'const sql = "SELECT outer";',
|
|
116
|
+
'if (a) { const sql = "SELECT a"; withCache(() => ctx.api.v1.snowflake.query(sql)); }',
|
|
117
|
+
'if (b) { const sql = "SELECT b"; ctx.api.v1.snowflake.query(sql); }',
|
|
118
|
+
'ctx.api.v1.snowflake.query(sql);',
|
|
119
|
+
].join('\n');
|
|
120
|
+
expect(querySql(source)).toEqual(['SELECT a', 'SELECT b', 'SELECT outer']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('leaves a template hole unresolved when it resolves to a structured value ($or), rather than gluing branches', () => {
|
|
124
|
+
const source = [
|
|
125
|
+
'const clause = custom ? " AND a" : " AND b";',
|
|
126
|
+
'ctx.api.v1.snowflake.query(`SELECT 1${clause}`);',
|
|
127
|
+
].join('\n');
|
|
128
|
+
expect(querySql(source)).toEqual(['SELECT 1${clause}']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('keeps template holes it cannot resolve as written', () => {
|
|
132
|
+
const source = ['ctx.api.v1.snowflake.query(`SELECT 1 LIMIT ${ctx.input.n}`);'].join('\n');
|
|
133
|
+
expect(querySql(source)).toEqual(['SELECT 1 LIMIT ${ctx.input.n}']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('leaves a conditional unresolved when either branch is dynamic', () => {
|
|
137
|
+
const source = [
|
|
138
|
+
'const clause = custom ? " AND a" : ctx.input.clause;',
|
|
139
|
+
'ctx.api.v1.snowflake.query(`SELECT 1${clause}`);',
|
|
140
|
+
].join('\n');
|
|
141
|
+
expect(querySql(source)).toEqual(['SELECT 1${clause}']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('resolves a logical expression whose sides both resolve, recording both alternatives', () => {
|
|
145
|
+
const source = [
|
|
146
|
+
'const OVERRIDE = "SELECT override";',
|
|
147
|
+
'const sql = OVERRIDE || "SELECT default";',
|
|
148
|
+
'ctx.api.v1.snowflake.query(sql);',
|
|
149
|
+
].join('\n');
|
|
150
|
+
expect(querySql(source)).toEqual([{ $or: ['SELECT override', 'SELECT default'] }]);
|
|
151
|
+
expect(querySql('const sql = "SELECT a" ?? "SELECT b"; ctx.api.v1.snowflake.query(sql);')).toEqual([
|
|
152
|
+
{ $or: ['SELECT a', 'SELECT b'] },
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("falls back to the const declaration's init source text when a logical expression has a dynamic side", () => {
|
|
157
|
+
const source = 'const sql = ctx.input.override || fetchSql(); ctx.api.v1.snowflake.query(sql);';
|
|
158
|
+
expect(querySql(source)).toEqual(['ctx.input.override || fetchSql()']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('resolves numbers, booleans, and null literals', () => {
|
|
162
|
+
const source = 'ctx.api.v1.snowflake.query("SELECT 1", null, 5); ctx.api.v1.other.thing(true, false);';
|
|
163
|
+
const p = program(source);
|
|
164
|
+
const bindings = collectBindings(p);
|
|
165
|
+
const usages = collectApiUsages(p, bindings, createPathResolver(bindings, 'ctx'), source, SIGNATURES);
|
|
166
|
+
expect(usages.snowflake.query).toEqual([{ sql: 'SELECT 1', params: null, options: 5 }]);
|
|
167
|
+
expect(usages.other.thing).toEqual([{ arg0: true, arg1: false }]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('resolves an array whose every element resolves', () => {
|
|
171
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", [1, "a", true]);')).toEqual([
|
|
172
|
+
{ sql: 'SELECT 1', params: [1, 'a', true] },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('falls back to source text for an array element that does not resolve, and for a hole', () => {
|
|
177
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", [1, ctx.input.x, , ...rest]);')).toEqual([
|
|
178
|
+
{ sql: 'SELECT 1', params: [1, 'ctx.input.x', null, '...rest'] },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('resolves an object literal whose every property resolves', () => {
|
|
183
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { a: 1, b: "x" });')).toEqual([
|
|
184
|
+
{ sql: 'SELECT 1', params: { a: 1, b: 'x' } },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('falls back to source text for a dynamic property value or a computed key, and marks a spread instead of dropping it', () => {
|
|
189
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { a: ctx.input.x });')).toEqual([
|
|
190
|
+
{ sql: 'SELECT 1', params: { a: 'ctx.input.x' } },
|
|
191
|
+
]);
|
|
192
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { [k]: 1 });')).toEqual([
|
|
193
|
+
{ sql: 'SELECT 1', params: { k: 1 } },
|
|
194
|
+
]);
|
|
195
|
+
expect(queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { a: 1, ...rest });')).toEqual([
|
|
196
|
+
{ sql: 'SELECT 1', params: { a: 1, '...rest': 'rest' } },
|
|
197
|
+
]);
|
|
198
|
+
expect(
|
|
199
|
+
queryCalls('ctx.api.v1.snowflake.query("SELECT 1", { tz, ...windowBindParams(timeRange), excludeInternal });')
|
|
200
|
+
).toEqual([
|
|
201
|
+
{
|
|
202
|
+
sql: 'SELECT 1',
|
|
203
|
+
params: {
|
|
204
|
+
tz: 'tz',
|
|
205
|
+
'...windowBindParams(timeRange)': 'windowBindParams(timeRange)',
|
|
206
|
+
excludeInternal: 'excludeInternal',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('follows a const array/object through an identifier, same as strings', () => {
|
|
213
|
+
const source = ['const rows = [1, 2, 3];', 'ctx.api.v1.snowflake.query("SELECT 1", rows);'].join('\n');
|
|
214
|
+
expect(queryCalls(source)).toEqual([{ sql: 'SELECT 1', params: [1, 2, 3] }]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('leaves a template hole unresolved when it resolves to a non-scalar value (array)', () => {
|
|
218
|
+
const source = [
|
|
219
|
+
'const params = custom ? [1, 2] : [3, 4];',
|
|
220
|
+
'ctx.api.v1.snowflake.query(`SELECT 1 -- ${params}`);',
|
|
221
|
+
].join('\n');
|
|
222
|
+
expect(querySql(source)).toEqual(['SELECT 1 -- ${params}']);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('falls back to source text for non-const, reassigned, destructured and circular bindings', () => {
|
|
226
|
+
expect(queryCalls('let sql = "SELECT 1"; ctx.api.v1.snowflake.query(sql);')).toEqual([{ sql: 'sql' }]);
|
|
227
|
+
expect(queryCalls('const sql = "SELECT 1"; sql = "x"; ctx.api.v1.snowflake.query(sql);')).toEqual([{ sql: 'sql' }]);
|
|
228
|
+
expect(queryCalls('const { sql } = q; ctx.api.v1.snowflake.query(sql);')).toEqual([{ sql: 'sql' }]);
|
|
229
|
+
expect(queryCalls('const a = b; const b = a; ctx.api.v1.snowflake.query(a);')).toEqual([{ sql: 'b' }]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('resolves a const despite a same-named function param in a different scope', () => {
|
|
233
|
+
const source = [
|
|
234
|
+
'const sql = "SELECT 1";',
|
|
235
|
+
'function helper(sql) { return sql; }',
|
|
236
|
+
'ctx.api.v1.snowflake.query(sql);',
|
|
237
|
+
].join('\n');
|
|
238
|
+
expect(querySql(source)).toEqual(['SELECT 1']);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('does not resolve a tagged template (the tag function return is unknown)', () => {
|
|
242
|
+
expect(queryCalls('ctx.api.v1.snowflake.query(sql`SELECT 1`);')).toEqual([{ sql: 'sql`SELECT 1`' }]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('records a destructured method call (const { query } = ctx.api.v1.snowflake)', () => {
|
|
246
|
+
const source = ['const { query } = ctx.api.v1.snowflake;', 'query("SELECT 1", { x: 1 });'].join('\n');
|
|
247
|
+
const p = program(source);
|
|
248
|
+
const bindings = collectBindings(p);
|
|
249
|
+
const usages = collectApiUsages(p, bindings, createPathResolver(bindings, 'ctx'), source, SIGNATURES);
|
|
250
|
+
expect(usages.snowflake.query).toEqual([{ sql: 'SELECT 1', params: { x: 1 } }]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { trackEvent } from '
|
|
2
|
+
import { trackEvent } from '../../../util/tracker';
|
|
3
3
|
import { parseOrThrow } from '../parse';
|
|
4
4
|
|
|
5
|
-
vi.mock('
|
|
5
|
+
vi.mock('../../../util/tracker', () => ({
|
|
6
6
|
trackEvent: vi.fn().mockResolvedValue(undefined),
|
|
7
7
|
trackDuration: vi.fn(),
|
|
8
8
|
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { ApiSignatures } from './types';
|
|
5
|
+
|
|
6
|
+
let cached: ApiSignatures | undefined;
|
|
7
|
+
|
|
8
|
+
/** Derived from the runner's channel docs by `scripts/generate-handler-api.mjs` at build time. */
|
|
9
|
+
export function loadApiSignatures(): ApiSignatures {
|
|
10
|
+
if (cached) {
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
const root = path.dirname(createRequire(import.meta.url).resolve('@mondaydotcomorg/z2h-cli/package.json'));
|
|
14
|
+
try {
|
|
15
|
+
cached = JSON.parse(readFileSync(path.join(root, 'handler-api', 'signatures.json'), 'utf8')) as ApiSignatures;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if ((err as { code?: string }).code !== 'ENOENT') {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
cached = {};
|
|
21
|
+
}
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { simple as walkSimple } from 'acorn-walk';
|
|
2
|
+
import { resolveOrSourceText } from './utils';
|
|
3
|
+
import { bestSourceText, staticValue } from './static-value';
|
|
4
|
+
import { loadApiSignatures } from './api-signatures';
|
|
5
|
+
import type { Program } from 'acorn';
|
|
6
|
+
import type { ApiCall, ApiSignatures, ApiUsage, Bindings, PathResolver } from './types';
|
|
7
|
+
|
|
8
|
+
// ctx.api.v1.<domain>.<method>(...) — the only API shape the runner exposes today
|
|
9
|
+
const API_PREFIX = ['ctx', 'api', 'v1'] as const;
|
|
10
|
+
const API_CALL_MIN_PARTS = API_PREFIX.length + 2;
|
|
11
|
+
|
|
12
|
+
function isApiPath(parts: string[]): boolean {
|
|
13
|
+
return parts[0] === API_PREFIX[0] && parts[1] === API_PREFIX[1] && parts[2] === API_PREFIX[2];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isApiCall(parts: string[]): boolean {
|
|
17
|
+
return isApiPath(parts) && parts.length >= API_CALL_MIN_PARTS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function collectApiDomains(program: Program, paths: PathResolver): Set<string> {
|
|
21
|
+
const apiDomains = new Set<string>();
|
|
22
|
+
walkSimple(program, {
|
|
23
|
+
MemberExpression(node) {
|
|
24
|
+
const parts = paths.pathOf(node)?.split('.') ?? [];
|
|
25
|
+
if (isApiPath(parts) && parts[3]) {
|
|
26
|
+
apiDomains.add(parts[3]);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return apiDomains;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function collectApiUsages(
|
|
34
|
+
program: Program,
|
|
35
|
+
bindings: Bindings,
|
|
36
|
+
paths: PathResolver,
|
|
37
|
+
source: string,
|
|
38
|
+
signatures: ApiSignatures = loadApiSignatures()
|
|
39
|
+
): ApiUsage {
|
|
40
|
+
const apiUsages: ApiUsage = {};
|
|
41
|
+
walkSimple(program, {
|
|
42
|
+
CallExpression(node) {
|
|
43
|
+
if (node.callee.type !== 'MemberExpression' && node.callee.type !== 'Identifier') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const parts = paths.pathOf(node.callee)?.split('.') ?? [];
|
|
47
|
+
if (!isApiCall(parts)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const [, , , domain, ...methodParts] = parts;
|
|
51
|
+
const method = methodParts.join('.');
|
|
52
|
+
const sigKey = `${domain}.${method}`;
|
|
53
|
+
const names = signatures[sigKey];
|
|
54
|
+
if (!names) {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.warn(`[ast] no signature for ${sigKey} — argument names will be positional`);
|
|
57
|
+
}
|
|
58
|
+
const call: ApiCall = {};
|
|
59
|
+
node.arguments.forEach((arg, i) => {
|
|
60
|
+
call[names?.[i] ?? `arg${i}`] = resolveOrSourceText(
|
|
61
|
+
arg,
|
|
62
|
+
source,
|
|
63
|
+
node => staticValue(node, bindings, source, new Set()),
|
|
64
|
+
node => bestSourceText(node, source, bindings)
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
const methods = (apiUsages[domain] ??= {});
|
|
68
|
+
const calls = (methods[method] ??= []);
|
|
69
|
+
calls.push(call);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return apiUsages;
|
|
73
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { fullAncestor as walkFullAncestor } from 'acorn-walk';
|
|
2
|
+
import { enclosingScope, patternBindings, patternNames } from './utils';
|
|
3
|
+
import type { AnyNode, Node, Pattern, Program } from 'acorn';
|
|
4
|
+
import type { Bindings, Declaration } from './types';
|
|
5
|
+
|
|
6
|
+
function functionBodyScope(node: AnyNode): Node {
|
|
7
|
+
if (
|
|
8
|
+
(node.type === 'FunctionDeclaration' ||
|
|
9
|
+
node.type === 'FunctionExpression' ||
|
|
10
|
+
node.type === 'ArrowFunctionExpression') &&
|
|
11
|
+
node.body.type === 'BlockStatement'
|
|
12
|
+
) {
|
|
13
|
+
return node.body;
|
|
14
|
+
}
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function collectBindings(program: Program): Bindings {
|
|
19
|
+
const declarations = new Map<string, Declaration[]>();
|
|
20
|
+
const unpinnable = new Set<string>();
|
|
21
|
+
const markUnpinnable = (pattern: Pattern) => patternNames(pattern).forEach(n => unpinnable.add(n));
|
|
22
|
+
|
|
23
|
+
walkFullAncestor(program, (node, _state, ancestors) => {
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case 'VariableDeclaration': {
|
|
26
|
+
const scope = enclosingScope(ancestors);
|
|
27
|
+
for (const declarator of node.declarations) {
|
|
28
|
+
for (const { name, path } of patternBindings(declarator.id, [])) {
|
|
29
|
+
const list = declarations.get(name) ?? [];
|
|
30
|
+
list.push({ kind: node.kind, init: declarator.init, path, scope });
|
|
31
|
+
declarations.set(name, list);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'AssignmentExpression':
|
|
37
|
+
markUnpinnable(node.left);
|
|
38
|
+
break;
|
|
39
|
+
case 'UpdateExpression':
|
|
40
|
+
if (node.argument.type === 'Identifier') {
|
|
41
|
+
unpinnable.add(node.argument.name);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case 'ForInStatement':
|
|
45
|
+
case 'ForOfStatement':
|
|
46
|
+
if (node.left.type !== 'VariableDeclaration') {
|
|
47
|
+
markUnpinnable(node.left);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
case 'FunctionDeclaration':
|
|
51
|
+
case 'FunctionExpression':
|
|
52
|
+
case 'ArrowFunctionExpression': {
|
|
53
|
+
const scope = functionBodyScope(node);
|
|
54
|
+
for (const param of node.params) {
|
|
55
|
+
for (const name of patternNames(param)) {
|
|
56
|
+
const list = declarations.get(name) ?? [];
|
|
57
|
+
list.push({ kind: 'param', init: undefined, path: undefined, scope });
|
|
58
|
+
declarations.set(name, list);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'CatchClause':
|
|
64
|
+
if (node.param) {
|
|
65
|
+
const scope = node.body;
|
|
66
|
+
for (const name of patternNames(node.param)) {
|
|
67
|
+
const list = declarations.get(name) ?? [];
|
|
68
|
+
list.push({ kind: 'param', init: undefined, path: undefined, scope });
|
|
69
|
+
declarations.set(name, list);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { declarations, unpinnable };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function declarationAt(bindings: Bindings, name: string, position: number): Declaration | undefined {
|
|
80
|
+
const best = (bindings.declarations.get(name) ?? [])
|
|
81
|
+
.filter(d => d.scope.start <= position && position < d.scope.end)
|
|
82
|
+
.sort((a, b) => b.scope.start - a.scope.start)[0];
|
|
83
|
+
if (!best || best.kind === 'param' || bindings.unpinnable.has(name)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { memberChain } from './utils';
|
|
2
|
+
import { declarationAt } from './bindings';
|
|
3
|
+
import type { AnyNode } from 'acorn';
|
|
4
|
+
import type { Bindings, PathResolver } from './types';
|
|
5
|
+
|
|
6
|
+
export function createPathResolver(bindings: Bindings, contextParam?: string): PathResolver {
|
|
7
|
+
function resolveChain(node: AnyNode, resolving: ReadonlySet<string>): string | undefined {
|
|
8
|
+
const chain = memberChain(node);
|
|
9
|
+
if (!chain) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return [resolveRoot(chain.root, node.start, resolving), ...chain.parts].join('.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveRoot(name: string, position: number, resolving: ReadonlySet<string>): string {
|
|
16
|
+
if (contextParam !== undefined && name === contextParam) {
|
|
17
|
+
return 'ctx';
|
|
18
|
+
}
|
|
19
|
+
if (resolving.has(name)) {
|
|
20
|
+
return name;
|
|
21
|
+
}
|
|
22
|
+
const decl = declarationAt(bindings, name, position);
|
|
23
|
+
if (!decl || decl.kind !== 'const' || !decl.init || !decl.path) {
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
const base = resolveChain(decl.init, new Set(resolving).add(name));
|
|
27
|
+
return base ? [base, ...decl.path].join('.') : name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { pathOf: node => resolveChain(node, new Set()) };
|
|
31
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse, type ExportNamedDeclaration, type ModuleDeclaration, type Program, type Statement } from 'acorn';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { trackEvent } from '
|
|
3
|
+
import { trackEvent } from '../../util/tracker';
|
|
4
4
|
import { exportsHandler } from './handler-declaration';
|
|
5
5
|
import { messages } from './messages';
|
|
6
6
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { displayText, propertyKey, resolveOrSourceText, sourceTextOf } from './utils';
|
|
2
|
+
import { declarationAt } from './bindings';
|
|
3
|
+
import type { AnyNode } from 'acorn';
|
|
4
|
+
import type { Bindings, JsonValue, Resolved } from './types';
|
|
5
|
+
|
|
6
|
+
type Inner = (child: AnyNode) => Resolved<JsonValue> | undefined;
|
|
7
|
+
|
|
8
|
+
function resolveLiteral(node: AnyNode & { type: 'Literal' }): Resolved<JsonValue> | undefined {
|
|
9
|
+
return typeof node.value === 'string' ||
|
|
10
|
+
typeof node.value === 'number' ||
|
|
11
|
+
typeof node.value === 'boolean' ||
|
|
12
|
+
node.value === null
|
|
13
|
+
? { value: node.value }
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Template holes only interpolate scalars; a resolved object/array (e.g. `$or`) is shown unresolved instead of glued inline. */
|
|
18
|
+
function resolveTemplate(
|
|
19
|
+
node: AnyNode & { type: 'TemplateLiteral' },
|
|
20
|
+
source: string,
|
|
21
|
+
inner: Inner
|
|
22
|
+
): Resolved<JsonValue> {
|
|
23
|
+
const text = node.quasis
|
|
24
|
+
.map((quasi, i) => {
|
|
25
|
+
const literalText = quasi.value.cooked ?? quasi.value.raw;
|
|
26
|
+
const hole = node.expressions[i];
|
|
27
|
+
if (!hole) {
|
|
28
|
+
return literalText;
|
|
29
|
+
}
|
|
30
|
+
const resolved = inner(hole);
|
|
31
|
+
const scalar =
|
|
32
|
+
resolved !== undefined &&
|
|
33
|
+
(typeof resolved.value === 'string' ||
|
|
34
|
+
typeof resolved.value === 'number' ||
|
|
35
|
+
typeof resolved.value === 'boolean' ||
|
|
36
|
+
resolved.value === null);
|
|
37
|
+
return literalText + (scalar ? displayText(resolved!.value) : `\${${sourceTextOf(hole, source)}}`);
|
|
38
|
+
})
|
|
39
|
+
.join('');
|
|
40
|
+
return { value: text };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Each element/property resolves independently, falling back to source text. A spread is kept
|
|
45
|
+
* visible under a `...`-prefixed key/value pair rather than silently dropped.
|
|
46
|
+
*/
|
|
47
|
+
function resolveContainer(
|
|
48
|
+
node: AnyNode & { type: 'ArrayExpression' | 'ObjectExpression' },
|
|
49
|
+
source: string,
|
|
50
|
+
bindings: Bindings,
|
|
51
|
+
inner: Inner
|
|
52
|
+
): Resolved<JsonValue> {
|
|
53
|
+
const fallbackText = (n: AnyNode) => bestSourceText(n, source, bindings);
|
|
54
|
+
if (node.type === 'ArrayExpression') {
|
|
55
|
+
return {
|
|
56
|
+
value: node.elements.map(element => (element ? resolveOrSourceText(element, source, inner, fallbackText) : null)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const result: { [key: string]: JsonValue } = {};
|
|
60
|
+
for (const prop of node.properties) {
|
|
61
|
+
if (prop.type === 'SpreadElement') {
|
|
62
|
+
const spreadText = sourceTextOf(prop.argument, source);
|
|
63
|
+
result[`...${spreadText}`] = spreadText;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (prop.type !== 'Property') {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const key = prop.computed
|
|
70
|
+
? sourceTextOf(prop.key, source)
|
|
71
|
+
: propertyKey(prop.key, false) ?? sourceTextOf(prop.key, source);
|
|
72
|
+
result[key] = resolveOrSourceText(prop.value, source, inner, fallbackText);
|
|
73
|
+
}
|
|
74
|
+
return { value: result };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function joinBranches(
|
|
78
|
+
left: Resolved<JsonValue> | undefined,
|
|
79
|
+
right: Resolved<JsonValue> | undefined
|
|
80
|
+
): Resolved<JsonValue> | undefined {
|
|
81
|
+
return left !== undefined && right !== undefined ? { value: { $or: [left.value, right.value] } } : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveIdentifier(
|
|
85
|
+
node: AnyNode & { type: 'Identifier' },
|
|
86
|
+
bindings: Bindings,
|
|
87
|
+
source: string,
|
|
88
|
+
resolving: ReadonlySet<string>
|
|
89
|
+
): Resolved<JsonValue> | undefined {
|
|
90
|
+
if (resolving.has(node.name)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const decl = declarationAt(bindings, node.name, node.start);
|
|
94
|
+
if (!decl || decl.kind !== 'const' || !decl.init || decl.path?.length !== 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return staticValue(decl.init, bindings, source, new Set(resolving).add(node.name));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Source text for a node that failed to resolve — the const declaration's init when the node names one, else the node itself. */
|
|
101
|
+
export function bestSourceText(node: AnyNode, source: string, bindings: Bindings): string {
|
|
102
|
+
if (node.type === 'Identifier') {
|
|
103
|
+
const decl = declarationAt(bindings, node.name, node.start);
|
|
104
|
+
if (decl && decl.kind === 'const' && decl.init && decl.path?.length === 0) {
|
|
105
|
+
return sourceTextOf(decl.init, source);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return sourceTextOf(node, source);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function staticValue(
|
|
112
|
+
node: AnyNode,
|
|
113
|
+
bindings: Bindings,
|
|
114
|
+
source: string,
|
|
115
|
+
resolving: ReadonlySet<string>
|
|
116
|
+
): Resolved<JsonValue> | undefined {
|
|
117
|
+
const inner: Inner = child => staticValue(child, bindings, source, resolving);
|
|
118
|
+
switch (node.type) {
|
|
119
|
+
case 'Literal':
|
|
120
|
+
return resolveLiteral(node);
|
|
121
|
+
case 'TemplateLiteral':
|
|
122
|
+
return resolveTemplate(node, source, inner);
|
|
123
|
+
case 'ArrayExpression':
|
|
124
|
+
case 'ObjectExpression':
|
|
125
|
+
return resolveContainer(node, source, bindings, inner);
|
|
126
|
+
case 'BinaryExpression': {
|
|
127
|
+
if (node.operator !== '+') {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const left = inner(node.left);
|
|
131
|
+
const right = inner(node.right);
|
|
132
|
+
return typeof left?.value === 'string' && typeof right?.value === 'string'
|
|
133
|
+
? { value: left.value + right.value }
|
|
134
|
+
: undefined;
|
|
135
|
+
}
|
|
136
|
+
case 'ConditionalExpression':
|
|
137
|
+
return joinBranches(inner(node.consequent), inner(node.alternate));
|
|
138
|
+
case 'LogicalExpression':
|
|
139
|
+
return joinBranches(inner(node.left), inner(node.right));
|
|
140
|
+
case 'Identifier':
|
|
141
|
+
return resolveIdentifier(node, bindings, source, resolving);
|
|
142
|
+
default:
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|