Skip to content

Commit 48ba6ad

Browse files
authored
feat: support for optional chaining object?.key (#3547)
1 parent faf249b commit 48ba6ad

File tree

10 files changed

+496
-67
lines changed

10 files changed

+496
-67
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ kyle-compute <kyle@intelvis.ai>
278278
DongKwanKho <70864292+dodokw@users.noreply.github.com>
279279
Ikem Peter <ikempeter2020@gmail.com>
280280
Josh Kelley <joshkel@gmail.com>
281+
Nils Dietrich <nils.dietrich@enervance.de>
281282
Richard Taylor <richard.taylor@claconnect.com>
282283

283284
# Generated by tools/update-authors.js

docs/expressions/expression_trees.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,20 @@ Construction:
277277

278278
```
279279
new AccessorNode(object: Node, index: IndexNode)
280+
new AccessorNode(object: Node, index: IndexNode, optionalChaining: boolean)
280281
```
281282
283+
An optional property `optionalChaining` can be provided whether the accessor was
284+
written as optional-chaining using `a?.b`, or `a?.["b"]` with bracket notation.
285+
Default value is `false`. Forces evaluate to undefined if the given object is
286+
undefined or null.
287+
282288
Properties:
283289
284290
- `object: Node`
285291
- `index: IndexNode`
286292
- `name: string` (read-only) The function or method name. Returns an empty string when undefined.
293+
- `optionalChaining: boolean`
287294
288295
Examples:
289296

docs/expressions/syntax.md

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -55,54 +55,55 @@ interchangeably. For example, `x+y` will always evaluate identically to
5555
Functions below.
5656

5757

58-
Operator | Name | Syntax | Associativity | Example | Result
59-
----------- | -------------------------- | ---------- | ------------- | --------------------- | ---------------
60-
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
61-
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `[[1,2],[3,4]]`
62-
`{`, `}` | Object | `{...}` | None | `{a: 1, b: 2}` | `{a: 1, b: 2}`
63-
`,` | Parameter separator | `x, y` | Left to right | `max(2, 1, 5)` | `5`
64-
`.` | Property accessor | `obj.prop` | Left to right | `obj={a: 12}; obj.a` | `12`
65-
`;` | Statement separator | `x; y` | Left to right | `a=2; b=3; a*b` | `[6]`
66-
`;` | Row separator | `[x; y]` | Left to right | `[1,2;3,4]` | `[[1,2],[3,4]]`
67-
`\n` | Statement separator | `x \n y` | Left to right | `a=2 \n b=3 \n a*b` | `[2,3,6]`
68-
`+` | Add | `x + y` | Left to right | `4 + 5` | `9`
69-
`+` | Unary plus | `+y` | Right to left | `+4` | `4`
70-
`-` | Subtract | `x - y` | Left to right | `7 - 3` | `4`
71-
`-` | Unary minus | `-y` | Right to left | `-4` | `-4`
72-
`*` | Multiply | `x * y` | Left to right | `2 * 3` | `6`
73-
`.*` | Element-wise multiply | `x .* y` | Left to right | `[1,2,3] .* [1,2,3]` | `[1,4,9]`
74-
`/` | Divide | `x / y` | Left to right | `6 / 2` | `3`
75-
`./` | Element-wise divide | `x ./ y` | Left to right | `[9,6,4] ./ [3,2,2]` | `[3,3,2]`
76-
`%` | Percentage | `x%` | None | `8%` | `0.08`
77-
`%` | Addition with Percentage | `x + y%` | Left to right | `100 + 3%` | `103`
78-
`%` | Subtraction with Percentage| `x - y%` | Left to right | `100 - 3%` | `97`
79-
`%` `mod` | Modulus | `x % y` | Left to right | `8 % 3` | `2`
80-
`^` | Power | `x ^ y` | Right to left | `2 ^ 3` | `8`
81-
`.^` | Element-wise power | `x .^ y` | Right to left | `[2,3] .^ [3,3]` | `[8,27]`
82-
`'` | Transpose | `y'` | Left to right | `[[1,2],[3,4]]'` | `[[1,3],[2,4]]`
83-
`!` | Factorial | `y!` | Left to right | `5!` | `120`
84-
`&` | Bitwise and | `x & y` | Left to right | `5 & 3` | `1`
85-
`~` | Bitwise not | `~x` | Right to left | `~2` | `-3`
86-
<code>&#124;</code> | Bitwise or | <code>x &#124; y</code> | Left to right | <code>5 &#124; 3</code> | `7`
87-
<code>^&#124;</code> | Bitwise xor | <code>x ^&#124; y</code> | Left to right | <code>5 ^&#124; 2</code> | `7`
88-
`<<` | Left shift | `x << y` | Left to right | `4 << 1` | `8`
89-
`>>` | Right arithmetic shift | `x >> y` | Left to right | `8 >> 1` | `4`
90-
`>>>` | Right logical shift | `x >>> y` | Left to right | `-8 >>> 1` | `2147483644`
91-
`and` | Logical and | `x and y` | Left to right | `true and false` | `false`
92-
`not` | Logical not | `not y` | Right to left | `not true` | `false`
93-
`or` | Logical or | `x or y` | Left to right | `true or false` | `true`
94-
`xor` | Logical xor | `x xor y` | Left to right | `true xor true` | `false`
95-
`=` | Assignment | `x = y` | Right to left | `a = 5` | `5`
96-
`?` `:` | Conditional expression | `x ? y : z` | Right to left | `15 > 100 ? 1 : -1` | `-1`
97-
`??` | Nullish coalescing | `x ?? y` | Left to right | `null ?? 2` | `2`
98-
`:` | Range | `x : y` | Right to left | `1:4` | `[1,2,3,4]`
99-
`to`, `in` | Unit conversion | `x to y` | Left to right | `2 inch to cm` | `5.08 cm`
100-
`==` | Equal | `x == y` | Left to right | `2 == 4 - 2` | `true`
101-
`!=` | Unequal | `x != y` | Left to right | `2 != 3` | `true`
102-
`<` | Smaller | `x < y` | Left to right | `2 < 3` | `true`
103-
`>` | Larger | `x > y` | Left to right | `2 > 3` | `false`
104-
`<=` | Smallereq | `x <= y` | Left to right | `4 <= 3` | `false`
105-
`>=` | Largereq | `x >= y` | Left to right | `2 + 4 >= 6` | `true`
58+
Operator | Name | Syntax | Associativity | Example | Result
59+
----------- |-----------------------------|-------------| ------------- |-----------------------| ---------------
60+
`(`, `)` | Grouping | `(x)` | None | `2 * (3 + 4)` | `14`
61+
`[`, `]` | Matrix, Index | `[...]` | None | `[[1,2],[3,4]]` | `[[1,2],[3,4]]`
62+
`{`, `}` | Object | `{...}` | None | `{a: 1, b: 2}` | `{a: 1, b: 2}`
63+
`,` | Parameter separator | `x, y` | Left to right | `max(2, 1, 5)` | `5`
64+
`.` | Property accessor | `obj.prop` | Left to right | `obj={a: 12}; obj.a` | `12`
65+
`;` | Statement separator | `x; y` | Left to right | `a=2; b=3; a*b` | `[6]`
66+
`;` | Row separator | `[x; y]` | Left to right | `[1,2;3,4]` | `[[1,2],[3,4]]`
67+
`\n` | Statement separator | `x \n y` | Left to right | `a=2 \n b=3 \n a*b` | `[2,3,6]`
68+
`+` | Add | `x + y` | Left to right | `4 + 5` | `9`
69+
`+` | Unary plus | `+y` | Right to left | `+4` | `4`
70+
`-` | Subtract | `x - y` | Left to right | `7 - 3` | `4`
71+
`-` | Unary minus | `-y` | Right to left | `-4` | `-4`
72+
`*` | Multiply | `x * y` | Left to right | `2 * 3` | `6`
73+
`.*` | Element-wise multiply | `x .* y` | Left to right | `[1,2,3] .* [1,2,3]` | `[1,4,9]`
74+
`/` | Divide | `x / y` | Left to right | `6 / 2` | `3`
75+
`./` | Element-wise divide | `x ./ y` | Left to right | `[9,6,4] ./ [3,2,2]` | `[3,3,2]`
76+
`%` | Percentage | `x%` | None | `8%` | `0.08`
77+
`%` | Addition with Percentage | `x + y%` | Left to right | `100 + 3%` | `103`
78+
`%` | Subtraction with Percentage | `x - y%` | Left to right | `100 - 3%` | `97`
79+
`%` `mod` | Modulus | `x % y` | Left to right | `8 % 3` | `2`
80+
`^` | Power | `x ^ y` | Right to left | `2 ^ 3` | `8`
81+
`.^` | Element-wise power | `x .^ y` | Right to left | `[2,3] .^ [3,3]` | `[8,27]`
82+
`'` | Transpose | `y'` | Left to right | `[[1,2],[3,4]]'` | `[[1,3],[2,4]]`
83+
`!` | Factorial | `y!` | Left to right | `5!` | `120`
84+
`&` | Bitwise and | `x & y` | Left to right | `5 & 3` | `1`
85+
`~` | Bitwise not | `~x` | Right to left | `~2` | `-3`
86+
<code>&#124;</code> | Bitwise or | <code>x &#124; y</code> | Left to right | <code>5 &#124; 3</code> | `7`
87+
<code>^&#124;</code> | Bitwise xor | <code>x ^&#124; y</code> | Left to right | <code>5 ^&#124; 2</code> | `7`
88+
`<<` | Left shift | `x << y` | Left to right | `4 << 1` | `8`
89+
`>>` | Right arithmetic shift | `x >> y` | Left to right | `8 >> 1` | `4`
90+
`>>>` | Right logical shift | `x >>> y` | Left to right | `-8 >>> 1` | `2147483644`
91+
`and` | Logical and | `x and y` | Left to right | `true and false` | `false`
92+
`not` | Logical not | `not y` | Right to left | `not true` | `false`
93+
`or` | Logical or | `x or y` | Left to right | `true or false` | `true`
94+
`xor` | Logical xor | `x xor y` | Left to right | `true xor true` | `false`
95+
`=` | Assignment | `x = y` | Right to left | `a = 5` | `5`
96+
`?` `:` | Conditional expression | `x ? y : z` | Right to left | `15 > 100 ? 1 : -1` | `-1`
97+
`??` | Nullish coalescing | `x ?? y` | Left to right | `null ?? 2` | `2`
98+
`?.` | Optional chaining accessor | `obj?.prop` | Left to right | `obj={}; obj?.a` | `undefined`
99+
`:` | Range | `x : y` | Right to left | `1:4` | `[1,2,3,4]`
100+
`to`, `in` | Unit conversion | `x to y` | Left to right | `2 inch to cm` | `5.08 cm`
101+
`==` | Equal | `x == y` | Left to right | `2 == 4 - 2` | `true`
102+
`!=` | Unequal | `x != y` | Left to right | `2 != 3` | `true`
103+
`<` | Smaller | `x < y` | Left to right | `2 < 3` | `true`
104+
`>` | Larger | `x > y` | Left to right | `2 > 3` | `false`
105+
`<=` | Smallereq | `x <= y` | Left to right | `4 <= 3` | `false`
106+
`>=` | Largereq | `x >= y` | Left to right | `2 + 4 >= 6` | `true`
106107

107108

108109
## Precedence

src/expression/node/AccessorNode.js

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
4747
* @param {Node} object The object from which to retrieve
4848
* a property or subset.
4949
* @param {IndexNode} index IndexNode containing ranges
50+
* @param {boolean} [optionalChaining=false]
51+
* Optional property, if the accessor was written as optional-chaining
52+
* using `a?.b`, or `a?.["b"] with bracket notation.
53+
* Forces evaluate to undefined if the given object is undefined or null.
5054
*/
51-
constructor (object, index) {
55+
constructor (object, index, optionalChaining = false) {
5256
super()
5357
if (!isNode(object)) {
5458
throw new TypeError('Node expected for parameter "object"')
@@ -59,6 +63,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
5963

6064
this.object = object
6165
this.index = index
66+
this.optionalChaining = optionalChaining
6267
}
6368

6469
// readonly property name
@@ -93,15 +98,41 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
9398
const evalObject = this.object._compile(math, argNames)
9499
const evalIndex = this.index._compile(math, argNames)
95100

101+
const optionalChaining = this.optionalChaining
102+
const prevOptionalChaining = isAccessorNode(this.object) && this.object.optionalChaining
103+
96104
if (this.index.isObjectProperty()) {
97105
const prop = this.index.getObjectProperty()
98106
return function evalAccessorNode (scope, args, context) {
107+
const ctx = context || {}
108+
const object = evalObject(scope, args, ctx)
109+
110+
if (optionalChaining && object == null) {
111+
ctx.optionalShortCircuit = true
112+
return undefined
113+
}
114+
115+
if (prevOptionalChaining && ctx?.optionalShortCircuit) {
116+
return undefined
117+
}
118+
99119
// get a property from an object evaluated using the scope.
100-
return getSafeProperty(evalObject(scope, args, context), prop)
120+
return getSafeProperty(object, prop)
101121
}
102122
} else {
103123
return function evalAccessorNode (scope, args, context) {
104-
const object = evalObject(scope, args, context)
124+
const ctx = context || {}
125+
const object = evalObject(scope, args, ctx)
126+
127+
if (optionalChaining && object == null) {
128+
ctx.optionalShortCircuit = true
129+
return undefined
130+
}
131+
132+
if (prevOptionalChaining && ctx?.optionalShortCircuit) {
133+
return undefined
134+
}
135+
105136
// we pass just object here instead of context:
106137
const index = evalIndex(scope, args, object)
107138
return access(object, index)
@@ -127,7 +158,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
127158
map (callback) {
128159
return new AccessorNode(
129160
this._ifNode(callback(this.object, 'object', this)),
130-
this._ifNode(callback(this.index, 'index', this))
161+
this._ifNode(callback(this.index, 'index', this)),
162+
this.optionalChaining
131163
)
132164
}
133165

@@ -136,7 +168,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
136168
* @return {AccessorNode}
137169
*/
138170
clone () {
139-
return new AccessorNode(this.object, this.index)
171+
return new AccessorNode(this.object, this.index, this.optionalChaining)
140172
}
141173

142174
/**
@@ -149,8 +181,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
149181
if (needParenthesis(this.object)) {
150182
object = '(' + object + ')'
151183
}
152-
153-
return object + this.index.toString(options)
184+
const optionalChaining = this.optionalChaining ? (this.index.dotNotation ? '?' : '?.') : ''
185+
return object + optionalChaining + this.index.toString(options)
154186
}
155187

156188
/**
@@ -192,7 +224,8 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
192224
return {
193225
mathjs: name,
194226
object: this.object,
195-
index: this.index
227+
index: this.index,
228+
optionalChaining: this.optionalChaining
196229
}
197230
}
198231

@@ -205,7 +238,7 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({
205238
* @returns {AccessorNode}
206239
*/
207240
static fromJSON (json) {
208-
return new AccessorNode(json.object, json.index)
241+
return new AccessorNode(json.object, json.index, json.optionalChaining)
209242
}
210243
}
211244

src/expression/node/FunctionNode.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
137137
_compile (math, argNames) {
138138
// compile arguments
139139
const evalArgs = this.args.map((arg) => arg._compile(math, argNames))
140+
const fromOptionalChaining = isAccessorNode(this.fn) && this.fn.optionalChaining
140141

141142
if (isSymbolNode(this.fn)) {
142143
const name = this.fn.name
@@ -242,6 +243,12 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
242243

243244
return function evalFunctionNode (scope, args, context) {
244245
const object = evalObject(scope, args, context)
246+
247+
// Optional chaining: if the base object is nullish, short-circuit to undefined
248+
if (fromOptionalChaining && object == null) {
249+
return undefined
250+
}
251+
245252
const fn = getSafeMethod(object, prop)
246253

247254
if (fn?.rawArgs) {

0 commit comments

Comments
 (0)