Anonymous View
Skip to content

Commit 9930060

Browse files
committed
fix: optional property access support
1 parent b637563 commit 9930060

5 files changed

Lines changed: 104 additions & 3 deletions

File tree

packages/core/src/runtime/executeAgent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,12 @@ async function runMemberExpression(
522522
unsafe = unsafe || propertyResult.unsafe;
523523
}
524524

525-
const value = (objectResult.value as Record<string, unknown>)[property];
525+
let value: unknown;
526+
if (expr.optional && objectResult.value == null) {
527+
value = undefined;
528+
} else {
529+
value = (objectResult.value as Record<string, unknown>)[property];
530+
}
526531

527532
unsafe = unsafe || !isSafeValue(value);
528533

packages/core/src/runtime/tests/objects.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,64 @@ test('assign value to unsafe object', async () => {
436436
'Assigning to unsafe value is not allowed',
437437
);
438438
});
439+
440+
test('optional property access with value', async () => {
441+
const script = parseScript([
442+
//
443+
'const a = { b: 1 };',
444+
'a?.b;',
445+
]);
446+
447+
const agent = createAgent({ script });
448+
const result = await executeAgent({ agent });
449+
450+
const expectedStack = rootFrame({
451+
status: 'done',
452+
variables: { a: { b: 1 } },
453+
children: [
454+
completedFrame({
455+
node: 'var',
456+
children: [completedFrame({ node: 'literal', value: { b: 1 } })],
457+
}),
458+
completedFrame({
459+
node: 'member',
460+
value: 1,
461+
children: [completedFrame({ node: 'ident', value: { b: 1 } })],
462+
}),
463+
],
464+
});
465+
466+
expect(result).toEqual(agentResult({ ticks: 0 }));
467+
expect(agent.root).toEqual(expectedStack);
468+
expect(agent.status).toBe('done');
469+
});
470+
471+
test('optional property access without value', async () => {
472+
const script = parseScript([
473+
//
474+
'const a = null;',
475+
'a?.b;',
476+
]);
477+
478+
const agent = createAgent({ script });
479+
const result = await executeAgent({ agent });
480+
481+
const expectedStack = rootFrame({
482+
status: 'done',
483+
variables: { a: null },
484+
children: [
485+
completedFrame({
486+
node: 'var',
487+
}),
488+
completedFrame({
489+
node: 'member',
490+
value: undefined,
491+
children: [completedFrame({ node: 'ident', value: null })],
492+
}),
493+
],
494+
});
495+
496+
expect(result).toEqual(agentResult({ ticks: 0 }));
497+
expect(agent.root).toEqual(expectedStack);
498+
expect(agent.status).toBe('done');
499+
});

packages/parser/src/astTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ export interface MemberExpression extends AstNodeBase {
188188
* Object to access the property on.
189189
*/
190190
obj: Expression;
191+
/**
192+
* Whether the property access is optional.
193+
*/
194+
optional?: boolean;
191195
}
192196

193197
/**

packages/parser/src/parseScript.objects.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,21 @@ test('object prop assignment dynamic', () => {
141141

142142
expect(script).toEqual(expected);
143143
});
144+
145+
test('optional property access', () => {
146+
const code = 'a?.b';
147+
const script = parseScript(code);
148+
const expected: Script = {
149+
code,
150+
ast: [
151+
{
152+
type: 'member',
153+
prop: 'b',
154+
obj: { type: 'ident', name: 'a' },
155+
optional: true,
156+
},
157+
],
158+
};
159+
160+
expect(script).toEqual(expected);
161+
});

packages/parser/src/parseScript.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export function parseScript(code: string | string[]): Script {
4444
ast: parsed,
4545
};
4646
} catch (error) {
47+
if (error instanceof ParseError) {
48+
throw error;
49+
}
50+
4751
throw new ParseError('Failed to parse script', {
4852
cause: error,
4953
});
@@ -180,6 +184,7 @@ function parseExpression(expression: babel.Expression): Expression {
180184
};
181185

182186
case 'MemberExpression':
187+
case 'OptionalMemberExpression':
183188
return parseMemberExpression(expression);
184189

185190
case 'CallExpression': {
@@ -266,17 +271,25 @@ function parseExpression(expression: babel.Expression): Expression {
266271
});
267272
}
268273

269-
function parseMemberExpression(expression: babel.MemberExpression): MemberExpression {
274+
function parseMemberExpression(
275+
expression: babel.MemberExpression | babel.OptionalMemberExpression,
276+
): MemberExpression {
270277
const prop =
271278
!expression.computed && expression.property.type === 'Identifier'
272279
? expression.property.name
273280
: parseExpression(expression.property as babel.Expression);
274281

275-
return {
282+
const expr: MemberExpression = {
276283
type: 'member',
277284
prop,
278285
obj: parseExpression(expression.object),
279286
};
287+
288+
if (expression.optional) {
289+
expr.optional = true;
290+
}
291+
292+
return expr;
280293
}
281294

282295
function parseObjectExpression(

0 commit comments

Comments
 (0)