Skip to content

Commit

Permalink
Fix #2150, #660: give a clear error "Error: Undefined function ..." i…
Browse files Browse the repository at this point in the history
…nstead when

  evaluating a non-existing function, and expose internal functions `FunctionNode.onUndefinedFunction(name)` and `SymbolNode.onUndefinedSymbol(name)`
  • Loading branch information
josdejong committed Apr 10, 2021
1 parent 64a52a5 commit c6cbf55
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 10 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions src/expression/node/FunctionNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}))
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/expression/node/SymbolNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand All @@ -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)
}

Expand Down
6 changes: 6 additions & 0 deletions test/unit-tests/expression/node/FunctionNode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/unit-tests/expression/node/SymbolNode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
6 changes: 3 additions & 3 deletions test/unit-tests/expression/security.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down

0 comments on commit c6cbf55

Please sign in to comment.