Skip to content

Commit 5a54416

Browse files
authored
Merge pull request #21 from hyperweb-io/feat/parse-multi-files
Add multi-file contract parsing
2 parents 4469af1 + e46c29e commit 5a54416

13 files changed

+5946
-2466
lines changed

packages/parse/__tests__/ContractAnalyzer.multiFile.test.ts

Lines changed: 1191 additions & 0 deletions
Large diffs are not rendered by default.

packages/parse/__tests__/ContractAnalyzer.multiFileSchema.test.ts

Lines changed: 1079 additions & 0 deletions
Large diffs are not rendered by default.

packages/parse/__tests__/ContractAnalyzer.test.ts renamed to packages/parse/__tests__/ContractAnalyzer.singleFile.test.ts

Lines changed: 235 additions & 268 deletions
Large diffs are not rendered by default.
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
import { ContractAnalyzer } from '../src/ContractAnalyzer';
2+
3+
describe('ContractAnalyzer - Single File with Schema', () => {
4+
let analyzer: ContractAnalyzer;
5+
6+
beforeEach(() => {
7+
analyzer = new ContractAnalyzer();
8+
});
9+
10+
it('should handle array types', () => {
11+
const code = `
12+
export default class Contract {
13+
foo(items: string[]): string[] {
14+
return items;
15+
}
16+
}
17+
`;
18+
19+
const result = analyzer.analyzeWithSchema(code);
20+
expect(result.queries).toEqual([
21+
{
22+
name: 'foo',
23+
params: [
24+
{
25+
name: 'items',
26+
schema: { type: 'array', items: { type: 'string' } },
27+
},
28+
],
29+
returnSchema: { type: 'array', items: { type: 'string' } },
30+
},
31+
]);
32+
expect(result.mutations).toEqual([]);
33+
});
34+
35+
it('should handle Set types', () => {
36+
const code = `
37+
export default class Contract {
38+
getSet(s: Set<number>): Set<number> {
39+
return s;
40+
}
41+
}
42+
`;
43+
44+
const result = analyzer.analyzeWithSchema(code);
45+
expect(result.queries).toEqual([
46+
{
47+
name: 'getSet',
48+
params: [{ name: 's', schema: { type: 'array', items: { type: 'number' } } }],
49+
returnSchema: { type: 'array', items: { type: 'number' } },
50+
},
51+
]);
52+
expect(result.mutations).toEqual([]);
53+
});
54+
55+
it('should handle Map types', () => {
56+
const code = `
57+
export default class Contract {
58+
getMap(m: Map<string, number>): Map<string, number> {
59+
return m;
60+
}
61+
}
62+
`;
63+
64+
const result = analyzer.analyzeWithSchema(code);
65+
expect(result.queries).toEqual([
66+
{
67+
name: 'getMap',
68+
params: [
69+
{
70+
name: 'm',
71+
schema: {
72+
type: 'object',
73+
additionalProperties: { type: 'number' },
74+
},
75+
},
76+
],
77+
returnSchema: {
78+
type: 'object',
79+
additionalProperties: { type: 'number' },
80+
},
81+
},
82+
]);
83+
expect(result.mutations).toEqual([]);
84+
});
85+
86+
it('should handle inline object types with arrays', () => {
87+
const code = `
88+
export default class Contract {
89+
transform(data: { a: number; b: string[] }): { a: number; b: string[] } {
90+
return data;
91+
}
92+
}
93+
`;
94+
95+
const result = analyzer.analyzeWithSchema(code);
96+
expect(result.queries).toEqual([
97+
{
98+
name: 'transform',
99+
params: [
100+
{
101+
name: 'data',
102+
schema: {
103+
type: 'object',
104+
properties: {
105+
a: { type: 'number' },
106+
b: { type: 'array', items: { type: 'string' } },
107+
},
108+
required: ['a', 'b'],
109+
},
110+
},
111+
],
112+
returnSchema: {
113+
type: 'object',
114+
properties: {
115+
a: { type: 'number' },
116+
b: { type: 'array', items: { type: 'string' } },
117+
},
118+
required: ['a', 'b'],
119+
},
120+
},
121+
]);
122+
expect(result.mutations).toEqual([]);
123+
});
124+
125+
it('should handle class property arrow functions', () => {
126+
const code = `
127+
export default class Contract {
128+
state: any;
129+
foo = (x: number) => {
130+
return this.state.value;
131+
}
132+
}
133+
`;
134+
135+
const result = analyzer.analyzeWithSchema(code);
136+
expect(result.queries).toEqual([
137+
{
138+
name: 'foo',
139+
params: [{ name: 'x', schema: { type: 'number' } }],
140+
returnSchema: {},
141+
},
142+
]);
143+
expect(result.mutations).toEqual([]);
144+
});
145+
146+
it('should handle optional chaining on state', () => {
147+
const code = `
148+
export default class Contract {
149+
state: any;
150+
getVal() {
151+
return this.state?.foo;
152+
}
153+
}
154+
`;
155+
156+
const result = analyzer.analyzeWithSchema(code);
157+
expect(result.queries).toEqual([
158+
{
159+
name: 'getVal',
160+
params: [],
161+
returnSchema: {},
162+
},
163+
]);
164+
expect(result.mutations).toEqual([]);
165+
});
166+
167+
it('should handle destructured parameters', () => {
168+
const code = `
169+
export default class Contract {
170+
diff({ a, b }: { a: number; b: number }) {
171+
return a - b;
172+
}
173+
}
174+
`;
175+
176+
const result = analyzer.analyzeWithSchema(code);
177+
expect(result.queries).toEqual([
178+
{
179+
name: 'diff',
180+
params: [
181+
{
182+
name: '{ a, b }: { a: number; b: number }',
183+
schema: {
184+
type: 'object',
185+
properties: { a: { type: 'number' }, b: { type: 'number' } },
186+
required: ['a', 'b'],
187+
},
188+
},
189+
],
190+
returnSchema: {},
191+
},
192+
]);
193+
expect(result.mutations).toEqual([]);
194+
});
195+
196+
it('should handle default and rest parameters', () => {
197+
const code = `
198+
export default class Contract {
199+
sum(x = 1, ...rest: number[]): number {
200+
return rest.reduce((u, v) => u + v, x);
201+
}
202+
}
203+
`;
204+
205+
const result = analyzer.analyzeWithSchema(code);
206+
expect(result.queries).toEqual([
207+
{
208+
name: 'sum',
209+
params: [
210+
{ name: 'x', schema: {} },
211+
{
212+
name: 'rest',
213+
schema: { type: 'array', items: { type: 'number' } },
214+
},
215+
],
216+
returnSchema: { type: 'number' },
217+
},
218+
]);
219+
expect(result.mutations).toEqual([]);
220+
});
221+
222+
it('should handle tuple types', () => {
223+
const code = `
224+
export default class Contract {
225+
pair(p: [number, string]): [number, string] {
226+
return p;
227+
}
228+
}
229+
`;
230+
231+
const result = analyzer.analyzeWithSchema(code);
232+
expect(result.queries).toEqual([
233+
{
234+
name: 'pair',
235+
params: [
236+
{
237+
name: 'p',
238+
schema: {
239+
type: 'array',
240+
items: { anyOf: [{ type: 'number' }, { type: 'string' }] },
241+
},
242+
},
243+
],
244+
returnSchema: {
245+
type: 'array',
246+
items: { anyOf: [{ type: 'number' }, { type: 'string' }] },
247+
},
248+
},
249+
]);
250+
expect(result.mutations).toEqual([]);
251+
});
252+
253+
it('should handle index signature object types', () => {
254+
const code = `
255+
export default class Contract {
256+
lookup(m: { [key: string]: number }) {
257+
return m['foo'];
258+
}
259+
}
260+
`;
261+
262+
const result = analyzer.analyzeWithSchema(code);
263+
expect(result.queries).toEqual([
264+
{
265+
name: 'lookup',
266+
params: [
267+
{
268+
name: 'm',
269+
schema: {
270+
type: 'object',
271+
additionalProperties: { type: 'number' },
272+
},
273+
},
274+
],
275+
returnSchema: {},
276+
},
277+
]);
278+
expect(result.mutations).toEqual([]);
279+
});
280+
281+
it('should handle intersections and unions', () => {
282+
const code = `
283+
type A = { x: string };
284+
type B = { y: number };
285+
export default class Contract {
286+
either(x: string | number): string | number {
287+
return x;
288+
}
289+
both(x: A & B): A & B {
290+
return x;
291+
}
292+
}
293+
`;
294+
295+
const result = analyzer.analyzeWithSchema(code);
296+
expect(result.queries).toEqual([
297+
{
298+
name: 'either',
299+
params: [
300+
{
301+
name: 'x',
302+
schema: { anyOf: [{ type: 'string' }, { type: 'number' }] },
303+
},
304+
],
305+
returnSchema: { anyOf: [{ type: 'string' }, { type: 'number' }] },
306+
},
307+
{
308+
name: 'both',
309+
params: [{ name: 'x', schema: {} }],
310+
returnSchema: {},
311+
},
312+
]);
313+
expect(result.mutations).toEqual([]);
314+
});
315+
316+
it('should handle literal types', () => {
317+
const code = `
318+
export default class Contract {
319+
mode(m: 'on' | 'off'): 'on' | 'off' {
320+
return m;
321+
}
322+
}
323+
`;
324+
325+
const result = analyzer.analyzeWithSchema(code);
326+
expect(result.queries).toEqual([
327+
{
328+
name: 'mode',
329+
params: [{ name: 'm', schema: { type: 'string', enum: ['on', 'off'] } }],
330+
returnSchema: { type: 'string', enum: ['on', 'off'] },
331+
},
332+
]);
333+
expect(result.mutations).toEqual([]);
334+
});
335+
336+
it('should handle generic Array<T> types', () => {
337+
const code = `
338+
export default class Contract {
339+
wrapList(a: Array<string>): Array<string> {
340+
return a;
341+
}
342+
}
343+
`;
344+
345+
const result = analyzer.analyzeWithSchema(code);
346+
expect(result.queries).toEqual([
347+
{
348+
name: 'wrapList',
349+
params: [{ name: 'a', schema: { type: 'array', items: { type: 'string' } } }],
350+
returnSchema: { type: 'array', items: { type: 'string' } },
351+
},
352+
]);
353+
expect(result.mutations).toEqual([]);
354+
});
355+
356+
it('should handle function type parameters', () => {
357+
const code = `
358+
export default class Contract {
359+
call(cb: (x: number) => boolean): boolean {
360+
return cb(1);
361+
}
362+
}
363+
`;
364+
365+
const result = analyzer.analyzeWithSchema(code);
366+
expect(result.queries).toEqual([
367+
{
368+
name: 'call',
369+
params: [{ name: 'cb', schema: {} }],
370+
returnSchema: { type: 'boolean' },
371+
},
372+
]);
373+
expect(result.mutations).toEqual([]);
374+
});
375+
});

packages/parse/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"keywords": [],
3333
"dependencies": {
34+
"@babel/generator": "^7.27.3",
3435
"@babel/parser": "^7.24.0",
3536
"@babel/traverse": "^7.24.0",
3637
"@babel/types": "^7.24.0"

0 commit comments

Comments
 (0)