Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c1d720a

Browse files
committedMay 31, 2025
fix mutations
1 parent fa01341 commit c1d720a

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed
 

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,106 @@ describe('ContractAnalyzer', () => {
280280
expect(result.mutations).toEqual([]);
281281
});
282282

283+
describe('advanced mutation detection', () => {
284+
it('should detect increment and decrement mutations', () => {
285+
const code = `
286+
export default class Contract {
287+
inc() { this.state.count++; }
288+
dec() { --this.state.count; }
289+
postDec() { this.state.count--; }
290+
preInc() { ++this.state.count; }
291+
}
292+
`;
293+
const result = analyzer.analyzeFromCode(code);
294+
expect(result.mutations.map(m => m.name)).toEqual(['inc','dec','postDec','preInc']);
295+
expect(result.queries).toEqual([]);
296+
});
297+
it('should detect compound assignment mutations', () => {
298+
const code = `
299+
export default class Contract {
300+
add() { this.state.total += 5; }
301+
append() { this.state.name += ' Smith'; }
302+
mul() { this.state.count *= 2; }
303+
or() { this.state.flag |= 1; }
304+
}
305+
`;
306+
const result = analyzer.analyzeFromCode(code);
307+
expect(result.mutations.map(m => m.name)).toEqual(['add','append','mul','or']);
308+
expect(result.queries).toEqual([]);
309+
});
310+
it('should detect object property mutations', () => {
311+
const code = `
312+
export default class Contract {
313+
setName() { this.state.user.name = 'Dan'; }
314+
setTheme() { this.state.settings.theme = 'dark'; }
315+
incAge() { this.state.profile.age++; }
316+
}
317+
`;
318+
const result = analyzer.analyzeFromCode(code);
319+
expect(result.mutations.map(m => m.name)).toEqual(['setName','setTheme','incAge']);
320+
expect(result.queries).toEqual([]);
321+
});
322+
it('should detect array operation mutations', () => {
323+
const code = `
324+
export default class Contract {
325+
pushItem() { this.state.items.push('item'); }
326+
popHist() { this.state.history.pop(); }
327+
doSplice() { this.state.list.splice(1, 2); }
328+
shiftQueue() { this.state.queue.shift(); }
329+
}
330+
`;
331+
const result = analyzer.analyzeFromCode(code);
332+
expect(result.mutations.map(m => m.name)).toEqual(['pushItem','popHist','doSplice','shiftQueue']);
333+
expect(result.queries).toEqual([]);
334+
});
335+
it('should detect method-based and assign mutations', () => {
336+
const code = `
337+
export default class Contract {
338+
setSomething() { this.state.setSomething('value'); }
339+
assignConfig() { Object.assign(this.state.config, { debug: true }); }
340+
customUpdate() { this.state.data.update({ key: 'val' }); }
341+
}
342+
`;
343+
const result = analyzer.analyzeFromCode(code);
344+
expect(result.mutations.map(m => m.name)).toEqual(['setSomething','assignConfig','customUpdate']);
345+
expect(result.queries).toEqual([]);
346+
});
347+
it('should detect destructuring and spread mutations', () => {
348+
const code = `
349+
export default class Contract {
350+
reset() { this.state = { ...this.state, newProp: true }; }
351+
destr() { ({ a: this.state.a } = source); }
352+
spreadCoords() { this.state.coords = [...this.state.coords, [x, y]]; }
353+
}
354+
`;
355+
const result = analyzer.analyzeFromCode(code);
356+
expect(result.mutations.map(m => m.name)).toEqual(['reset','destr','spreadCoords']);
357+
expect(result.queries).toEqual([]);
358+
});
359+
it('should detect chained and nested mutations', () => {
360+
const code = `
361+
export default class Contract {
362+
incNested() { this.state.obj.nested.count += 1; }
363+
statInc() { this.state.stats.increment('xp'); }
364+
}
365+
`;
366+
const result = analyzer.analyzeFromCode(code);
367+
expect(result.mutations.map(m => m.name)).toEqual(['incNested','statInc']);
368+
expect(result.queries).toEqual([]);
369+
});
370+
it('should detect assignment of function calls and negation', () => {
371+
const code = `
372+
export default class Contract {
373+
assignCall() { this.state.value = getNewValue(); }
374+
assignNot() { this.state.flag = !this.state.flag; }
375+
}
376+
`;
377+
const result = analyzer.analyzeFromCode(code);
378+
expect(result.mutations.map(m => m.name)).toEqual(['assignCall','assignNot']);
379+
expect(result.queries).toEqual([]);
380+
});
381+
});
382+
283383
describe('analyzeWithSchema', () => {
284384
it('should handle array types', () => {
285385
const code = `

‎packages/parse/src/ContractAnalyzer.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export class ContractAnalyzer {
129129
queries: MethodInfo[],
130130
mutations: MethodInfo[]
131131
): void {
132+
const analyzer = this;
132133
parentPath.traverse({
133134
ClassMethod(methodPath: NodePath<t.ClassMethod>) {
134135
// Skip static methods and constructors
@@ -209,6 +210,35 @@ export class ContractAnalyzer {
209210
hasReturn = true;
210211
}
211212
},
213+
AssignmentExpression(assignPath: NodePath<t.AssignmentExpression>) {
214+
if (analyzer.containsStateMember(assignPath.node.left as t.Node)) {
215+
writesState = true;
216+
}
217+
},
218+
UpdateExpression(updatePath: NodePath<t.UpdateExpression>) {
219+
if (analyzer.isStateMemberExpression(updatePath.node.argument as t.Node)) {
220+
writesState = true;
221+
}
222+
},
223+
CallExpression(callPath: NodePath<t.CallExpression>) {
224+
const callee = callPath.node.callee;
225+
if (t.isMemberExpression(callee)) {
226+
// method calls on state (e.g., this.state.setX())
227+
if (analyzer.isStateMemberExpression(callee.object)) {
228+
writesState = true;
229+
}
230+
// Object.assign(this.state.prop, ...)
231+
if (
232+
t.isIdentifier(callee.object) && callee.object.name === 'Object' &&
233+
t.isIdentifier(callee.property) && callee.property.name === 'assign'
234+
) {
235+
const target = callPath.node.arguments[0];
236+
if (target && analyzer.isStateMemberExpression(target as t.Node)) {
237+
writesState = true;
238+
}
239+
}
240+
}
241+
},
212242
});
213243

214244
const methodInfo: MethodInfo = {
@@ -480,6 +510,33 @@ export class ContractAnalyzer {
480510
ReturnStatement(retPath: NodePath<t.ReturnStatement>) {
481511
if (retPath.node.argument) hasRet = true;
482512
},
513+
AssignmentExpression(assignPath: NodePath<t.AssignmentExpression>) {
514+
if (self.containsStateMember(assignPath.node.left as t.Node)) {
515+
writes = true;
516+
}
517+
},
518+
UpdateExpression(updatePath: NodePath<t.UpdateExpression>) {
519+
if (self.isStateMemberExpression(updatePath.node.argument as t.Node)) {
520+
writes = true;
521+
}
522+
},
523+
CallExpression(callPath: NodePath<t.CallExpression>) {
524+
const callee = callPath.node.callee;
525+
if (t.isMemberExpression(callee)) {
526+
if (self.isStateMemberExpression(callee.object)) {
527+
writes = true;
528+
}
529+
if (
530+
t.isIdentifier(callee.object) && callee.object.name === 'Object' &&
531+
t.isIdentifier(callee.property) && callee.property.name === 'assign'
532+
) {
533+
const target = callPath.node.arguments[0];
534+
if (target && self.isStateMemberExpression(target as t.Node)) {
535+
writes = true;
536+
}
537+
}
538+
}
539+
},
483540
});
484541
const info: SchemaMethodInfo = { name: methodName, params, returnSchema };
485542
if (writes) mutations.push(info);
@@ -560,11 +617,82 @@ export class ContractAnalyzer {
560617
ReturnStatement(retPath: NodePath<t.ReturnStatement>) {
561618
if (retPath.node.argument) hasRet = true;
562619
},
620+
AssignmentExpression(assignPath: NodePath<t.AssignmentExpression>) {
621+
if (self.containsStateMember(assignPath.node.left as t.Node)) {
622+
writes = true;
623+
}
624+
},
625+
UpdateExpression(updatePath: NodePath<t.UpdateExpression>) {
626+
if (self.isStateMemberExpression(updatePath.node.argument as t.Node)) {
627+
writes = true;
628+
}
629+
},
630+
CallExpression(callPath: NodePath<t.CallExpression>) {
631+
const callee = callPath.node.callee;
632+
if (t.isMemberExpression(callee)) {
633+
if (self.isStateMemberExpression(callee.object)) {
634+
writes = true;
635+
}
636+
if (
637+
t.isIdentifier(callee.object) && callee.object.name === 'Object' &&
638+
t.isIdentifier(callee.property) && callee.property.name === 'assign'
639+
) {
640+
const target = callPath.node.arguments[0];
641+
if (target && self.isStateMemberExpression(target as t.Node)) {
642+
writes = true;
643+
}
644+
}
645+
}
646+
},
563647
});
564648
const info: SchemaMethodInfo = { name: methodName, params, returnSchema };
565649
if (writes) mutations.push(info);
566650
else if (reads || hasRet) queries.push(info);
567651
},
568652
});
569653
}
654+
655+
// Helper to detect if a MemberExpression references this.state at any depth
656+
private isStateMemberExpression(node: t.Node): boolean {
657+
if (!t.isMemberExpression(node)) return false;
658+
const { object, property } = node;
659+
if (t.isThisExpression(object) && t.isIdentifier(property) && property.name === 'state') {
660+
return true;
661+
}
662+
if (t.isMemberExpression(object)) {
663+
return this.isStateMemberExpression(object);
664+
}
665+
return false;
666+
}
667+
668+
// Helper to detect state member usage in assignment patterns or member expressions
669+
private containsStateMember(node: t.Node): boolean {
670+
if (t.isMemberExpression(node) && this.isStateMemberExpression(node)) {
671+
return true;
672+
}
673+
if (t.isObjectPattern(node)) {
674+
for (const prop of node.properties) {
675+
if (t.isRestElement(prop) && this.containsStateMember(prop.argument as t.Node)) {
676+
return true;
677+
}
678+
if (t.isObjectProperty(prop) && this.containsStateMember(prop.value as t.Node)) {
679+
return true;
680+
}
681+
}
682+
}
683+
if (t.isArrayPattern(node)) {
684+
for (const elem of node.elements) {
685+
if (elem && this.containsStateMember(elem as t.Node)) {
686+
return true;
687+
}
688+
}
689+
}
690+
if (t.isAssignmentPattern(node)) {
691+
return this.containsStateMember(node.left as t.Node);
692+
}
693+
if (t.isRestElement(node)) {
694+
return this.containsStateMember(node.argument as t.Node);
695+
}
696+
return false;
697+
}
570698
}

0 commit comments

Comments
 (0)
Please sign in to comment.