Skip to content
This repository has been archived by the owner on Jan 2, 2024. It is now read-only.

Commit

Permalink
feat: added current and global scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
Zvenigora1 committed Sep 8, 2022
1 parent f4e59ec commit adf61d6
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 11 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ _[jse-eval](https://github.com/6utt3rfly) was forked from [expression-eval](http
* [Node Types Supported](#node-types-supported)
- [Options](#options)
* [Case-insensitive evaluation](#case-insensitive-evaluation)
* [Blocklist](#blocklist)
* [Allowlist](#allowlist)
* [BlockList](#blocklist)
* [AllowList](#allowlist)
* [Function Bindings](#function-bindings)
* [Function Bindings with Scopes](#function-bindings-with-scopes)
- [Related Packages](#related-packages)
Expand Down Expand Up @@ -277,7 +277,7 @@ const fn = JseEval.compile('Foo.BAR + 10', options);
const value = fn({foo: {bar: 'baz'}}); // 'baz10'
```
### Blocklist
### BlockList
`blockList` prevents the execution of functions or the evaluation of variables, except those explictly specified. For example, blocklist may restrict the calling of the non-secure JavaScript `eval` function.
Expand All @@ -290,7 +290,7 @@ const ast = parse('eval("1+2")');
const value = eval(ast, {}, options); // error: Access to member "eval" from blockList disallowed
```
### Allowlist
### AllowList
`allowList` explictly permits the execution of functions or the evaluation of variables. For example, allowlist may restrict the calling of the non-secure JavaScript `eval` function.
Expand Down Expand Up @@ -335,6 +335,7 @@ const value2 = eval(ast2, context, options); // Miss Kitty says meow 3 times
### Function Bindings with Scopes
`Function Bindings` may be extended with `scopes`. Scopes faintly resemble namespaces and they allow to use the functions with the same name.
`CurrentScopeName` and `GlobalScopeName` allow to remove reference to object instance.
```javascript
import { parse, evaluate } from 'jse-eval';
Expand Down Expand Up @@ -375,7 +376,8 @@ const options = {
scopes: {
cat: { options: {functionBindings: {...catFunctionBindings}} },
dog: { options: {functionBindings: {...dogFunctionBindings}} }
}
},
currentScopeName: 'cat'
}

const ast = parse('cat.says()');
Expand All @@ -390,6 +392,10 @@ const value3 = eval(ast3, context, options); // Miss Kitty says meow 3 times
const ast4 = parse('dog.action(dog.num, "times")');
const value4 = eval(ast4, context, options); // Ralph says woof 5 times

const ast5 = parse('says()'); // reference to 'cat' is omitted because of currentScopeName
const value5 = eval(ast, context, options); // Cat Miss Kitty says meow


```
## Related Packages
Expand Down
50 changes: 44 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type EvalOptions = {
allowList?: string[];
functionBindings?: Record<string, FunctionBindings>;
scopes?: Record<string, Scope>;
currentScopeName?: string;
globalScopeName?: string;
}

const literals: Map<string, unknown> = new Map([
Expand Down Expand Up @@ -648,9 +650,9 @@ export default class ExpressionEval {

private static getValue(obj: ContextOrObject, name: string, options: EvalOptions): unknown {

const [, value] = ExpressionEval.getKeyValuePair(obj, name, options);
const [, value, scopeName] = ExpressionEval.getScopedKeyValuePair(obj, name, options);

const fn = ExpressionEval.getBindFunction(value, name, options);
const fn = ExpressionEval.getBindFunction(value, name, options, scopeName);
if (typeof fn === 'function') {
return fn;
}
Expand All @@ -672,10 +674,13 @@ export default class ExpressionEval {
}

private static getBindFunction(obj: unknown, name: string,
options: EvalOptions) {
options: EvalOptions, scopeName?: string) {

if (typeof obj === 'function' && options?.functionBindings) {
const [, value] = ExpressionEval.getKeyValuePair(options.functionBindings, name, options);
const scopedOptions = ExpressionEval.hasProperty(options?.scopes, scopeName)
? options.scopes[scopeName]?.options : options;

if (typeof obj === 'function' && scopedOptions?.functionBindings) {
const [, value] = ExpressionEval.getKeyValuePair(scopedOptions.functionBindings, name, options);
if (value) {
const bindings: FunctionBindings = value;
if (bindings?.thisRef || bindings?.arguments) {
Expand All @@ -686,6 +691,39 @@ export default class ExpressionEval {
}
}

private static hasProperty(obj?: ContextOrObject, name?: string): boolean {
if (obj && name) {
return Object.prototype.hasOwnProperty.call(obj, name);
}
}

private static getScopedKeyValuePair(obj: ContextOrObject, name: string | number,
options: EvalOptions): [string | number, unknown, string] {

const scopeNameList: string[] = [''];
const currentScope = ExpressionEval.hasProperty(obj, options.currentScopeName) &&
ExpressionEval.hasProperty(options?.scopes, options.currentScopeName);
const globalScope = ExpressionEval.hasProperty(obj, options.globalScopeName) &&
ExpressionEval.hasProperty(options?.scopes, options.globalScopeName);

if (currentScope) {
scopeNameList.push(options.currentScopeName);
}
if (globalScope) {
scopeNameList.push(options.globalScopeName);
}

for (const scopeName of scopeNameList) {
const scopedObject = (scopeName ? obj[scopeName] : obj) as ContextOrObject;
const [key, value] = this.getKeyValuePair(scopedObject, name, options);
if (typeof value !== 'undefined') {
return [key, value, scopeName];
}
}

return [name, undefined, ''];
}

private static getKeyValuePair(obj: ContextOrObject, name: string | number,
options: EvalOptions): [string | number, unknown] {

Expand All @@ -704,7 +742,7 @@ export default class ExpressionEval {
if (Array.isArray(keys)) {
const key = keys.find(key => key.localeCompare(name, 'en', { sensitivity: 'base' }) === 0);
if (key) {
const value = obj[key];
const value = currentObj[key];
return [key, value];
}
}
Expand Down
96 changes: 96 additions & 0 deletions tests/current-scope.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import tape from 'tape';
import JseEval from '../dist/jse-eval.module.js';
import {cloneDeep} from './utils.js';

const catObject = {
type: 'Cat',
name: 'Miss Kitty',
num: 3,
says: function() {return this.type + ' ' + this.name + ' says meow';},
action: function(args, n, t) {return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;},
}

const catFunctionBindings = {
says: {
thisRef: catObject
},
action: {
thisRef: catObject,
arguments: ['says', 'meow']
},
}

const dogObject = {
type: 'Dog',
name: 'Ralph',
num: 5,
says: function() {return this.type + ' ' + this.name + ' says woof';},
action: function(args, n, t) {return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;},
}

const dogFunctionBindings = {
says: {
thisRef: dogObject
},
action: {
thisRef: dogObject,
arguments: ['says', 'woof']
},
}

const counterObject = {
value: 0,
increment: function() {return ++this.value;}
}

const counterFunctionBindings = {
says: {
thisRef: counterObject
},
}

const context = {
cat: catObject,
dog: dogObject,
counter: counterObject
}

const scopeFixtures = [
{expr: 'cat.says()', context: null, expected: 'Cat Miss Kitty says meow'},
{expr: 'dog.says()', context: null, expected: 'Dog Ralph says woof'},
{expr: 'dog.type + " name is " + dog.name', context: null, expected: 'Dog name is Ralph'},
{expr: 'counter.increment()+counter.increment();counter.value', context: null, expected: 2},
{expr: 'cat.action(cat.num, "times")', context: null, expected: 'Miss Kitty says meow 3 times'},
{expr: 'dog.action(dog.num, "times")', context: null, expected: 'Ralph says woof 5 times'},
{expr: 'says()', context: null, expected: 'Cat Miss Kitty says meow'},
{expr: 'action(num, "times")', context: null, expected: 'Miss Kitty says meow 3 times'},
];

const scopeOptions = {
caseSensitive: false,
scopes: {
cat: {
options: {functionBindings: {...catFunctionBindings}}
},
dog: {
options: {functionBindings: {...dogFunctionBindings}}
}
},
currentScopeName: 'cat'
}


tape('currentScope', (t) => {

[...scopeFixtures].forEach((o) => {
const ctx = cloneDeep(o.context || context);
const options = cloneDeep(scopeOptions);
const ast = JseEval.jsep(o.expr);
const val = JseEval.evaluate(ast, ctx, options);

const compare = t[typeof o.expected === 'object' ? 'deepEqual' : 'equal'];
compare(val, o.expected, `${o.expr} (${val}) === ${o.expected}`);
});

t.end();
});
1 change: 1 addition & 0 deletions tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export * from './allow-list.test.js';
export * from './explicit-this.test.js';
export * from './function-bindings.test.js';
export * from './scope-function-bindings.test.js';
export * from './current-scope.test.js';

0 comments on commit adf61d6

Please sign in to comment.