diff --git a/HISTORY.md b/HISTORY.md index 0394794100..b42974e879 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,11 @@ - Fix #2133: strongly improved the performance of `isPrime`, see #2139. Thanks @Yaffle. +- Fix #2150: give a clear error "Error: Undefined function ..." instead when + evaluating a non-existing function. +- Fix #660: expose internal functions `FunctionNode.onUndefinedFunction(name)` + and `SymbolNode.onUndefinedSymbol(name)`, allowing to override the behavior. + By default, an Error is thrown. # 2021-03-10, version 9.3.0 diff --git a/src/expression/node/FunctionNode.js b/src/expression/node/FunctionNode.js index 5b71ef8323..13c1d7b140 100644 --- a/src/expression/node/FunctionNode.js +++ b/src/expression/node/FunctionNode.js @@ -87,29 +87,41 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({ const fn = name in math ? getSafeProperty(math, name) : undefined const isRaw = (typeof fn === 'function') && (fn.rawArgs === true) + function resolveFn (scope) { + return name in scope + ? getSafeProperty(scope, name) + : name in math + ? getSafeProperty(math, name) + : FunctionNode.onUndefinedFunction(name) + } + if (isRaw) { // pass unevaluated parameters (nodes) to the function // "raw" evaluation const rawArgs = this.args return function evalFunctionNode (scope, args, context) { - return (name in scope ? getSafeProperty(scope, name) : fn)(rawArgs, math, Object.assign({}, scope, args)) + const fn = resolveFn(scope) + return fn(rawArgs, math, Object.assign({}, scope, args)) } } else { // "regular" evaluation if (evalArgs.length === 1) { const evalArg0 = evalArgs[0] return function evalFunctionNode (scope, args, context) { - return (name in scope ? getSafeProperty(scope, name) : fn)(evalArg0(scope, args, context)) + const fn = resolveFn(scope) + return fn(evalArg0(scope, args, context)) } } else if (evalArgs.length === 2) { const evalArg0 = evalArgs[0] const evalArg1 = evalArgs[1] return function evalFunctionNode (scope, args, context) { - return (name in scope ? getSafeProperty(scope, name) : fn)(evalArg0(scope, args, context), evalArg1(scope, args, context)) + const fn = resolveFn(scope) + return fn(evalArg0(scope, args, context), evalArg1(scope, args, context)) } } else { return function evalFunctionNode (scope, args, context) { - return (name in scope ? getSafeProperty(scope, name) : fn).apply(null, map(evalArgs, function (evalArg) { + const fn = resolveFn(scope) + return fn.apply(null, map(evalArgs, function (evalArg) { return evalArg(scope, args, context) })) } @@ -187,6 +199,14 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({ return new FunctionNode(this.fn, this.args.slice(0)) } + /** + * Throws an error 'Undefined function {name}' + * @param {string} name + */ + FunctionNode.onUndefinedFunction = function (name) { + throw new Error('Undefined function ' + name) + } + // backup Node's toString function // @private const nodeToString = FunctionNode.prototype.toString diff --git a/src/expression/node/SymbolNode.js b/src/expression/node/SymbolNode.js index b3cc888641..ffcdab3f08 100644 --- a/src/expression/node/SymbolNode.js +++ b/src/expression/node/SymbolNode.js @@ -80,7 +80,7 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m ? getSafeProperty(scope, name) : isUnit ? new Unit(null, name) - : undef(name) + : SymbolNode.onUndefinedSymbol(name) } } } @@ -107,7 +107,7 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m * Throws an error 'Undefined symbol {name}' * @param {string} name */ - function undef (name) { + SymbolNode.onUndefinedSymbol = function (name) { throw new Error('Undefined symbol ' + name) } diff --git a/test/unit-tests/expression/node/FunctionNode.test.js b/test/unit-tests/expression/node/FunctionNode.test.js index a10ad970a9..a26c5bcfc7 100644 --- a/test/unit-tests/expression/node/FunctionNode.test.js +++ b/test/unit-tests/expression/node/FunctionNode.test.js @@ -53,6 +53,12 @@ describe('FunctionNode', function () { assert.strictEqual(n3.name, '') }) + it('should throw an error when evaluating an undefined function', function () { + const scope = {} + const s = new FunctionNode('foo', []) + assert.throws(function () { s.compile().evaluate(scope) }, /Error: Undefined function foo/) + }) + it('should compile a FunctionNode', function () { const s = new SymbolNode('sqrt') const c = new ConstantNode(4) diff --git a/test/unit-tests/expression/node/SymbolNode.test.js b/test/unit-tests/expression/node/SymbolNode.test.js index cebea67c73..b26856c9a7 100644 --- a/test/unit-tests/expression/node/SymbolNode.test.js +++ b/test/unit-tests/expression/node/SymbolNode.test.js @@ -32,7 +32,7 @@ describe('SymbolNode', function () { it('should throw an error when evaluating an undefined symbol', function () { const scope = {} const s = new SymbolNode('foo') - assert.throws(function () { s.compile().evaluate(scope) }, Error) + assert.throws(function () { s.compile().evaluate(scope) }, /Error: Undefined symbol foo/) }) it('should compile a SymbolNode', function () { diff --git a/test/unit-tests/expression/security.test.js b/test/unit-tests/expression/security.test.js index c8ca826a47..720ffa0e21 100644 --- a/test/unit-tests/expression/security.test.js +++ b/test/unit-tests/expression/security.test.js @@ -104,7 +104,7 @@ describe('security', function () { assert.throws(function () { const math2 = math.create() math2.evaluate('import({matrix:cos.constructor},{override:1});x=["console.log(\'hacked...\')"];x()') - }, /Error: No access to property "constructor"/) + }, /Error: Undefined function import/) }) it('should not allow calling Function via index retrieval', function () { @@ -297,13 +297,13 @@ describe('security', function () { math.evaluate('f=chain("a(){return evaluate;};function b").typed({"":f()=0}).done();' + 'g=f();' + "g(\"console.log('hacked...')\")") - }, /(is not a function)|(Object expected)/) + }, /Error: Undefined function chain/) }) it('should not allow using method chain (2)', function () { assert.throws(function () { math.evaluate("evilMath=chain().create().done();evilMath.import({\"_compile\":f(a,b,c)=\"evaluate\",\"isNode\":f()=true}); parse(\"(1)\").map(g(a,b,c)=evilMath.chain()).compile().evaluate()(\"console.log('hacked...')\")") - }, /(Cannot read property 'apply' of undefined)|(undefined has no properties)|(undefined is not an object)|(Unable to get property 'apply' of undefined or null reference)/) + }, /Error: Undefined function chain/) }) it('should not allow using method Chain', function () {