static-eval icon indicating copy to clipboard operation
static-eval copied to clipboard

Differences in `ReturnStatement` evaluations

Open Cimbali opened this issue 4 years ago • 0 comments

Why does the following work:

[1, 2].map(function(x) { return x + 1 })

But not the following:

[{a: 1}, {a: 2}].map(function(x) { return x["a"] })

Since .map() does not seem to be an issue, and member expressions of course aren’t, I’m not sure why this fails. The actual error is TypeError: Cannot read properties of null (reading 'a')

The inner function ASTs are respectively:

ReturnStatement {
  type: 'ReturnStatement',
  argument: BinaryExpression {
    type: 'BinaryExpression',
    operator: '+',
    left: Identifier { type: 'Identifier', name: 'x' },
    right: Literal { type: 'Literal', value: 1, raw: '1' }
  }
}

And:

ReturnStatement {
  type: 'ReturnStatement',
  argument: ComputedMemberExpression {
    type: 'MemberExpression',
    computed: true,
    object: Identifier { type: 'Identifier', name: 'x' },
    property: Literal { type: 'Literal', value: 'a', raw: '"a"' }
  }
}

Statically evaluating a MemberExpression is not harder than a BinaryExpression, is it?

After looking at the code, it seems to me that in both cases the x statement is statically evaluated to null. In the BinaryExpression code, the binary evaluation returns null + 1 which is 1, and you then allow and execute the function.

However since null["a"] raises an error, the function is not allowed and executed. However x["a"] is knowable statically (it is either the property or undefined if it’s absent − if you call evaluate('x["b"]', {x: {a: 1}}), you get undefined, not an error), so the function should execute in this case as well.

There is a simple workaround which is to return (x || {a: ""})["a"] instead of x["a"], this avoids throwing an error by ensuring there is a fallback value to x.a if x is undefined.

Cimbali avatar Jan 18 '22 18:01 Cimbali