From 152e60f464fbedcd288b395e3fdd3bc4b13be618 Mon Sep 17 00:00:00 2001 From: Nico Rehwaldt Date: Wed, 1 Nov 2023 21:23:57 +0100 Subject: [PATCH] feat: stricly assert argument length Closes https://github.com/nikku/feelin/issues/12 BREAKING CHANGE: * functions with variable length arguments must use `...` to capture variable parts --- src/builtins.ts | 28 ++++++++++++++-------------- src/types.ts | 33 +++++++++++++++++++++++++++++++-- test/interpreter-spec.js | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/builtins.ts b/src/builtins.ts index 3ac5106..8e62b1a 100644 --- a/src/builtins.ts +++ b/src/builtins.ts @@ -375,25 +375,25 @@ const builtins = { return list.length; }, [ 'list' ]), - 'min': listFn(function(list) { + 'min': listFn(function(...list) { return list.reduce((min, el) => min === null ? el : Math.min(min, el), null); }, 'number'), - 'max': listFn(function(list) { + 'max': listFn(function(...list) { return list.reduce((max, el) => max === null ? el : Math.max(max, el), null); }, 'number'), - 'sum': listFn(function(list) { + 'sum': listFn(function(...list) { return sum(list); }, 'number'), - 'mean': listFn(function(list) { + 'mean': listFn(function(...list) { const s = sum(list); return s === null ? s : s / list.length; }, 'number'), - 'all': listFn(function(list) { + 'all': listFn(function(...list) { let nonBool = false; @@ -412,7 +412,7 @@ const builtins = { }, 'any?'), - 'any': listFn(function(list) { + 'any': listFn(function(...list) { let nonBool = false; @@ -478,7 +478,7 @@ const builtins = { }, []); }, [ 'list', 'any' ]), - 'union': listFn(function(lists) { + 'union': listFn(function(...lists) { return lists.reduce((result, list) => { @@ -507,7 +507,7 @@ const builtins = { return flatten(list); }, [ 'list' ]), - 'product': listFn(function(list) { + 'product': listFn(function(...list) { if (list.length === 0) { return null; @@ -518,7 +518,7 @@ const builtins = { }, 1); }, 'number'), - 'median': listFn(function(list) { + 'median': listFn(function(...list) { if (list.length === 0) { return null; @@ -527,7 +527,7 @@ const builtins = { return median(list); }, 'number'), - 'stddev': listFn(function(list) { + 'stddev': listFn(function(...list) { if (list.length < 2) { return null; @@ -536,7 +536,7 @@ const builtins = { return stddev(list); }, 'number'), - 'mode': listFn(function(list) { + 'mode': listFn(function(...list) { return mode(list); }, 'number'), @@ -730,11 +730,11 @@ const builtins = { return Object.entries(m).map(([ key, value ]) => ({ key, value })); }, [ 'context' ]), - 'context': listFn(function(_contexts) { + 'context': listFn(function(..._contexts) { throw notImplemented('context'); }, 'context'), - 'context merge': listFn(function(_contexts) { + 'context merge': listFn(function(..._contexts) { throw notImplemented('context merge'); }, 'context'), @@ -856,7 +856,7 @@ function listFn(fnDefinition, type, parameterNames = null) { return null; } - return fnDefinition(args); + return fnDefinition(...args); }; wrappedFn.$args = parameterNames || parseParameterNames(fnDefinition); diff --git a/src/types.ts b/src/types.ts index 711d064..d63a05c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -261,15 +261,44 @@ export class FunctionWrapper { if (isArray(contextOrArgs)) { params = contextOrArgs; + + // reject + if (params.length > this.parameterNames.length) { + + const lastParam = this.parameterNames[this.parameterNames.length - 1]; + + // strictly check for parameter count provided + // for non var-args functions + if (!lastParam || !lastParam.startsWith('...')) { + return null; + } + } } else { // strictly check for required parameter names, // and fail on wrong parameter name - if (Object.keys(contextOrArgs).some(key => !this.parameterNames.includes(key))) { + if (Object.keys(contextOrArgs).some( + key => !this.parameterNames.includes(key) && !this.parameterNames.includes(`...${key}`) + )) { return null; } - params = this.parameterNames.map(n => contextOrArgs[n]); + params = this.parameterNames.reduce((params, name) => { + + if (name.startsWith('...')) { + name = name.slice(3); + + const value = contextOrArgs[name]; + + if (!value) { + return params; + } else { + return [ ...params, ...value ]; + } + } + + return [ ...params, contextOrArgs[name] ]; + }, []); } return this.fn.call(null, ...params); diff --git a/test/interpreter-spec.js b/test/interpreter-spec.js index 2e6fe3f..a903ae7 100644 --- a/test/interpreter-spec.js +++ b/test/interpreter-spec.js @@ -169,6 +169,45 @@ describe('interpreter', function() { b: 7 }); + // call with wrong args + expr('foo(5)', null, { + foo: function() { + return 5; + } + }); + + // var args function + expr('foo(1, 2, 3)', [ [ 1, 2, 3 ] ], { + foo: function(...varArgs) { + return [ varArgs ]; + } + }); + + expr('foo(1)', [ 1, [] ], { + foo: function(firstArg, ...varArgs) { + return [ firstArg, varArgs ]; + } + }); + + expr('foo(firstArg: 1)', [ 1, [] ], { + foo: function(firstArg, ...varArgs) { + return [ firstArg, varArgs ]; + } + }); + + expr('foo(firstArg: 1, varArgs: null)', [ 1, [] ], { + foo: function(firstArg, ...varArgs) { + return [ firstArg, varArgs ]; + } + }); + + + expr('foo(varArgs: [ 1, 2, 3 ])', [ [ 1, 2, 3 ] ], { + foo: function(...varArgs) { + return [ varArgs ]; + } + }); + });