diff --git a/src/expression/node/AccessorNode.js b/src/expression/node/AccessorNode.js index 8806699197..aa4d592e94 100644 --- a/src/expression/node/AccessorNode.js +++ b/src/expression/node/AccessorNode.js @@ -9,7 +9,6 @@ import { isParenthesisNode, isSymbolNode } from '../../utils/is.js' -import { getSafeProperty } from '../../utils/customs.js' import { factory } from '../../utils/factory.js' import { accessFactory } from './utils/access.js' @@ -61,17 +60,6 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({ this.index = index } - // readonly property name - get name () { - if (this.index) { - return (this.index.isObjectProperty()) - ? this.index.getObjectProperty() - : '' - } else { - return this.object.name || '' - } - } - static name = name get type () { return name } get isAccessorNode () { return true } @@ -93,19 +81,11 @@ export const createAccessorNode = /* #__PURE__ */ factory(name, dependencies, ({ const evalObject = this.object._compile(math, argNames) const evalIndex = this.index._compile(math, argNames) - if (this.index.isObjectProperty()) { - const prop = this.index.getObjectProperty() - return function evalAccessorNode (scope, args, context) { - // get a property from an object evaluated using the scope. - return getSafeProperty(evalObject(scope, args, context), prop) - } - } else { - return function evalAccessorNode (scope, args, context) { - const object = evalObject(scope, args, context) - // we pass just object here instead of context: - const index = evalIndex(scope, args, object) - return access(object, index) - } + return function evalAccessorNode (scope, args, context) { + const object = evalObject(scope, args, context) + // we pass just object here instead of context: + const index = evalIndex(scope, args, object) + return access(object, index) } } diff --git a/src/expression/node/utils/access.js b/src/expression/node/utils/access.js index 5c87041965..0a5f71a6d6 100644 --- a/src/expression/node/utils/access.js +++ b/src/expression/node/utils/access.js @@ -1,5 +1,6 @@ import { errorTransform } from '../../transform/utils/errorTransform.js' import { getSafeProperty } from '../../../utils/customs.js' +import { isDenseMatrix } from '../../../utils/is.js' export function accessFactory ({ subset }) { /** @@ -23,11 +24,23 @@ export function accessFactory ({ subset }) { // TODO: move getStringSubset into a separate util file, use that return subset(object, index) } else if (typeof object === 'object') { - if (!index.isObjectProperty()) { - throw new TypeError('Cannot apply a numeric index as object property') + if (index.isObjectProperty()) { + return getSafeProperty(object, index.getObjectProperty()) } - return getSafeProperty(object, index.getObjectProperty()) + if (index._dimensions.length > 1) { + throw new SyntaxError('Cannot apply multi-element matrix as object property') + } + + if (isDenseMatrix(index._dimensions[0])) { + const compiledIndex = index._dimensions[0].get([0]) + + // For some reason, the value in the generated Dense Matrix _data + // is always 1 less than the expected calculated value + return getSafeProperty(object, String(compiledIndex + 1)) + } + + throw new TypeError('Cannot apply unsupported value as object property') } else { throw new TypeError('Cannot apply index: unsupported type of object') } diff --git a/src/expression/node/utils/assign.js b/src/expression/node/utils/assign.js index 6e1b59b833..fdafa79599 100644 --- a/src/expression/node/utils/assign.js +++ b/src/expression/node/utils/assign.js @@ -1,5 +1,6 @@ import { errorTransform } from '../../transform/utils/errorTransform.js' import { setSafeProperty } from '../../../utils/customs.js' +import { isDenseMatrix } from '../../../utils/is.js' export function assignFactory ({ subset, matrix }) { /** @@ -33,11 +34,25 @@ export function assignFactory ({ subset, matrix }) { // TODO: move setStringSubset into a separate util file, use that return subset(object, index, value) } else if (typeof object === 'object') { - if (!index.isObjectProperty()) { - throw TypeError('Cannot apply a numeric index as object property') + if (index.isObjectProperty()) { + setSafeProperty(object, index.getObjectProperty(), value) + return object } - setSafeProperty(object, index.getObjectProperty(), value) - return object + + if (index._dimensions.length > 1) { + throw new SyntaxError('Cannot apply multi-element matrix as object property') + } + + if (isDenseMatrix(index._dimensions[0])) { + const compiledIndex = index._dimensions[0].get([0]) + + // For some reason, the value in the generated Dense Matrix _data + // is always 1 less than the expected calculated value + setSafeProperty(object, String(compiledIndex + 1), value) + return object + } + + throw new TypeError('Cannot apply unsupported value as object property') } else { throw new TypeError('Cannot apply index: unsupported type of object') } diff --git a/src/expression/parse.js b/src/expression/parse.js index b376025d87..3b130b1f4c 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -1615,11 +1615,18 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ // parse key if (state.token === '"' || state.token === "'") { key = parseStringToken(state, state.token) - } else if (state.tokenType === TOKENTYPE.SYMBOL || (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) { - key = state.token + } else if (state.tokenType === TOKENTYPE.SYMBOL || (state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS) || state.tokenType === TOKENTYPE.NUMBER) { + key = state.tokenType === TOKENTYPE.NUMBER ? String(Number(state.token)) : state.token getToken(state) + } else if (state.token === '-') { + const minusNode = parseUnary(state) + if (isOperatorNode(minusNode) && minusNode.args.length === 1 && isConstantNode(minusNode.args[0]) && typeof minusNode.args[0].value === 'number') { + key = `-${minusNode.args[0].value}` + } else { + throw createSyntaxError(state, 'Numeric literal expected after "-" as object key') + } } else { - throw createSyntaxError(state, 'Symbol or string expected as object key') + throw createSyntaxError(state, 'Symbol, numeric literal or string expected as object key') } // parse key/value separator diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index 09455c5dc0..db84f0d029 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -912,11 +912,61 @@ describe('parse', function () { assert.deepStrictEqual(scope, { obj: { foo: { bar: 2 } } }) }) - it('should throw an error when trying to apply a matrix index as object property', function () { - const scope = { a: {} } - assert.throws(function () { - parseAndEval('a[2] = 6', scope) - }, /Cannot apply a numeric index as object property/) + it('should coerce numbers to string when trying to apply a numeric key in an object expression', function () { + assert.deepStrictEqual(parseAndEval('{2: 6}'), { 2: 6 }) + assert.deepStrictEqual(parseAndEval('{-2: 6}'), { '-2': 6 }) + }) + + it('should coerce numbers to string when trying to access an object expression property with matrix index', function () { + assert.strictEqual(parseAndEval('{2: 6}[2]'), 6) + assert.strictEqual(parseAndEval('{2.5: 6}[2.5]'), 6) + assert.strictEqual(parseAndEval('{5: 16}[3]'), undefined) + }) + + it('should coerce numbers to string when trying to set an object property with matrix index', function () { + const scope = { obj: {} } + const res1 = parseAndEval('obj[2] = 6', scope) + const res2 = parseAndEval('obj[-2.5] = {4: "haha"}', scope) + assert.strictEqual(res1, 6) + assert.strictEqual(res2, { 4: 'haha' }) + assert.deepStrictEqual(scope, { obj: { 2: 6, '-2.5': { 4: 'haha' } } }) + }) + + it('should accept operations / expressions in a matrix when trying to access an object expression property', function () { + assert.strictEqual(parseAndEval('{2: 6}[2 * 1]'), 6) + assert.strictEqual(parseAndEval('{31: 7 - 4}[0.2 + 0.8]'), undefined) + assert.strictEqual(parseAndEval('{6: "haha"}[multiply(2, 3)]'), 'haha') + assert.strictEqual(parseAndEval('{-4: 11 * 4}[-4]'), 44) + }) + + it('should accept operations / expressions in a matrix when trying to set an object property', function () { + const scope = { obj: {} } + const res1 = parseAndEval('obj[2^2] = 6', scope) + const res2 = parseAndEval('obj[multiply(2, add(3,1))] = "haha"', scope) + assert.strictEqual(res1, 6) + assert.strictEqual(res2, 'haha') + assert.deepStrictEqual(scope, { obj: { 4: 6, 8: 'haha' } }) + }) + + it('should ignore leading zeros when trying to apply numeric keys in an object expression', function () { + assert.deepStrictEqual(parseAndEval('{02: 6}'), { 2: 6 }) + assert.deepStrictEqual(parseAndEval('{0070: 6}'), { 70: 6 }) + assert.deepStrictEqual(parseAndEval('{0.2: 6}'), { 0.2: 6 }) + assert.deepStrictEqual(parseAndEval('{0010.0501: "haha"}'), { 10.0501: 'haha' }) + }) + + it('should ignore leading zeros in a matrix index when trying to access an object expression property', function () { + assert.strictEqual(parseAndEval('{2: 6}[02]'), 6) + assert.strictEqual(parseAndEval('{70: 1 - 6}[0070]'), -5) + assert.strictEqual(parseAndEval('{0.2: 6}[000.2]'), 6) + assert.strictEqual(parseAndEval('{10.0501: "haha"}[0010.0501]'), 'haha') + }) + + it('should ignore leading zeros in a matrix index when trying to set an object property', function () { + const scope = { obj: {} } + const res = parseAndEval('obj[02] = 6', scope) + assert.strictEqual(res, 6) + assert.deepStrictEqual(scope, { obj: { 2: 6 } }) }) it('should set a nested matrix subset from an object property (1)', function () {