Caleb Porzio 3 тижнів тому
батько
коміт
39b3f34b7f

+ 5 - 1
packages/csp/src/evaluator.js

@@ -31,7 +31,11 @@ function generateEvaluator(el, expression, dataStack) {
 
         let evaluate = generateRuntimeFunction(expression)
 
-        let returnValue = evaluate(completeScope)
+        let returnValue = evaluate({
+            scope: completeScope,
+            allowGlobal: true,
+            forceBindingRootScopeToFunctions: true,
+        })
 
         if (shouldAutoEvaluateFunctions && typeof returnValue === 'function') {
             let nextReturnValue = returnValue.apply(returnValue, params)

+ 73 - 56
packages/csp/src/parser.js

@@ -642,7 +642,7 @@ class Parser {
 }
 
 class Evaluator {
-    evaluate(node, scope = {}, context = null) {
+    evaluate({ node, scope = {}, context = null, allowGlobal = false, forceBindingRootScopeToFunctions = true }) {
         switch (node.type) {
             case 'Literal':
                 return node.value;
@@ -657,81 +657,97 @@ class Evaluator {
                     }
                     return value;
                 }
-                
+
                 // Fallback to globals - let CSP catch dangerous ones at runtime
-                if (typeof globalThis[node.name] !== 'undefined') {
+                if (allowGlobal && typeof globalThis[node.name] !== 'undefined') {
                     const value = globalThis[node.name];
                     if (typeof value === 'function') {
                         return value.bind(globalThis);
                     }
                     return value;
                 }
-                
+
                 throw new Error(`Undefined variable: ${node.name}`);
 
             case 'MemberExpression':
-                const object = this.evaluate(node.object, scope, context);
+                const object = this.evaluate({ node: node.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                 if (object == null) {
                     throw new Error('Cannot read property of null or undefined');
                 }
 
                 let memberValue;
                 if (node.computed) {
-                    const property = this.evaluate(node.property, scope, context);
+                    const property = this.evaluate({ node: node.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                     memberValue = object[property];
                 } else {
                     memberValue = object[node.property.name];
                 }
 
-                // If the accessed value is a function, bind it to its object to preserve 'this' context
+                // If the accessed value is a function, bind it based on forceBindingRootScopeToFunctions flag
                 if (typeof memberValue === 'function') {
-                    return memberValue.bind(object);
+                    if (forceBindingRootScopeToFunctions) {
+                        return memberValue.bind(scope);
+                    } else {
+                        return memberValue.bind(object);
+                    }
                 }
 
                 return memberValue;
 
             case 'CallExpression':
-                const callee = this.evaluate(node.callee, scope, context);
-                if (typeof callee !== 'function') {
-                    throw new Error('Value is not a function');
-                }
-
-                const args = node.arguments.map(arg => this.evaluate(arg, scope, context));
+                const args = node.arguments.map(arg => this.evaluate({ node: arg, scope, context, allowGlobal, forceBindingRootScopeToFunctions }));
 
-                // Determine the correct 'this' context
-                let thisContext = context;
                 if (node.callee.type === 'MemberExpression') {
-                    thisContext = this.evaluate(node.callee.object, scope, context);
-                } else if (node.callee.type === 'Identifier' && context !== null) {
-                    // For direct function calls, use provided context if available
-                    // Check scope first, then globals
-                    let originalFunction = scope[node.callee.name];
-                    if (!originalFunction) {
-                        originalFunction = globalThis[node.callee.name];
-                    }
-                    if (typeof originalFunction === 'function') {
-                        return originalFunction.apply(context, args);
-                    }
-                } else if (node.callee.type === 'MemberExpression' && context !== null) {
-                    // For member expression calls with explicit context, 
-                    // get the original function and apply the explicit context
-                    const obj = this.evaluate(node.callee.object, scope, context);
-                    let originalFunction;
+                    // For member expressions, get the object and function separately to preserve context
+                    const obj = this.evaluate({ node: node.callee.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
+                    let func;
                     if (node.callee.computed) {
-                        const prop = this.evaluate(node.callee.property, scope, context);
-                        originalFunction = obj[prop];
+                        const prop = this.evaluate({ node: node.callee.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
+                        func = obj[prop];
                     } else {
-                        originalFunction = obj[node.callee.property.name];
+                        func = obj[node.callee.property.name];
                     }
-                    if (typeof originalFunction === 'function') {
-                        return originalFunction.apply(context, args);
+
+                    if (typeof func !== 'function') {
+                        throw new Error('Value is not a function');
                     }
-                }
 
-                return callee.apply(thisContext, args);
+                    // For member expressions, always use the object as the 'this' context
+                    return func.apply(obj, args);
+                } else {
+                    // For direct function calls (identifiers), get the original function and apply context
+                    if (node.callee.type === 'Identifier') {
+                        const name = node.callee.name;
+                        let func;
+                        if (name in scope) {
+                            func = scope[name];
+                        } else if (allowGlobal && typeof globalThis[name] !== 'undefined') {
+                            func = globalThis[name];
+                        } else {
+                            throw new Error(`Undefined variable: ${name}`);
+                        }
+
+                        if (typeof func !== 'function') {
+                            throw new Error('Value is not a function');
+                        }
+
+                        // For direct calls, use provided context or the scope
+                        const thisContext = context !== null ? context : scope;
+                        return func.apply(thisContext, args);
+                    } else {
+                        // For other expressions
+                        const callee = this.evaluate({ node: node.callee, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
+                        if (typeof callee !== 'function') {
+                            throw new Error('Value is not a function');
+                        }
+
+                        // For other expressions, use provided context
+                        return callee.apply(context, args);
+                    }
+                }
 
             case 'UnaryExpression':
-                const argument = this.evaluate(node.argument, scope, context);
+                const argument = this.evaluate({ node: node.argument, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                 switch (node.operator) {
                     case '!': return !argument;
                     case '-': return -argument;
@@ -756,9 +772,9 @@ class Evaluator {
 
                     return node.prefix ? scope[name] : oldValue;
                 } else if (node.argument.type === 'MemberExpression') {
-                    const obj = this.evaluate(node.argument.object, scope, context);
+                    const obj = this.evaluate({ node: node.argument.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                     const prop = node.argument.computed
-                        ? this.evaluate(node.argument.property, scope, context)
+                        ? this.evaluate({ node: node.argument.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
                         : node.argument.property.name;
 
                     const oldValue = obj[prop];
@@ -773,8 +789,8 @@ class Evaluator {
                 throw new Error('Invalid update expression target');
 
             case 'BinaryExpression':
-                const left = this.evaluate(node.left, scope, context);
-                const right = this.evaluate(node.right, scope, context);
+                const left = this.evaluate({ node: node.left, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
+                const right = this.evaluate({ node: node.right, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
 
                 switch (node.operator) {
                     case '+': return left + right;
@@ -797,21 +813,21 @@ class Evaluator {
                 }
 
             case 'ConditionalExpression':
-                const test = this.evaluate(node.test, scope, context);
+                const test = this.evaluate({ node: node.test, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                 return test
-                    ? this.evaluate(node.consequent, scope, context)
-                    : this.evaluate(node.alternate, scope, context);
+                    ? this.evaluate({ node: node.consequent, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
+                    : this.evaluate({ node: node.alternate, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
 
             case 'AssignmentExpression':
-                const value = this.evaluate(node.right, scope, context);
+                const value = this.evaluate({ node: node.right, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
 
                 if (node.left.type === 'Identifier') {
                     scope[node.left.name] = value;
                     return value;
                 } else if (node.left.type === 'MemberExpression') {
-                    const obj = this.evaluate(node.left.object, scope, context);
+                    const obj = this.evaluate({ node: node.left.object, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                     if (node.left.computed) {
-                        const prop = this.evaluate(node.left.property, scope, context);
+                        const prop = this.evaluate({ node: node.left.property, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                         obj[prop] = value;
                     } else {
                         obj[node.left.property.name] = value;
@@ -821,17 +837,17 @@ class Evaluator {
                 throw new Error('Invalid assignment target');
 
             case 'ArrayExpression':
-                return node.elements.map(el => this.evaluate(el, scope, context));
+                return node.elements.map(el => this.evaluate({ node: el, scope, context, allowGlobal, forceBindingRootScopeToFunctions }));
 
             case 'ObjectExpression':
                 const result = {};
                 for (const prop of node.properties) {
                     const key = prop.computed
-                        ? this.evaluate(prop.key, scope, context)
+                        ? this.evaluate({ node: prop.key, scope, context, allowGlobal, forceBindingRootScopeToFunctions })
                         : prop.key.type === 'Identifier'
                             ? prop.key.name
-                            : this.evaluate(prop.key, scope, context);
-                    const value = this.evaluate(prop.value, scope, context);
+                            : this.evaluate({ node: prop.key, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
+                    const value = this.evaluate({ node: prop.value, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
                     result[key] = value;
                 }
                 return result;
@@ -850,9 +866,10 @@ export function generateRuntimeFunction(expression) {
         const ast = parser.parse();
         const evaluator = new Evaluator();
 
-        return function(scope = {}, context = null) {
+        return function(options = {}) {
+            const { scope = {}, context = null, allowGlobal = false, forceBindingRootScopeToFunctions = false } = options;
             // Use the scope directly - mutations are expected for assignments
-            return evaluator.evaluate(ast, scope, context);
+            return evaluator.evaluate({ node: ast, scope, context, allowGlobal, forceBindingRootScopeToFunctions });
         };
     } catch (error) {
         throw new Error(`CSP Parser Error: ${error.message}`);

+ 17 - 4
tests/cypress/integration/plugins/csp-compatibility.spec.js

@@ -24,22 +24,35 @@ test.csp('Can use components and basic expressions with CSP-compatible build',
 test.csp('Supports nested properties',
     [html`
         <div x-data="test">
+            <button @click="foo.change" id="1">Change Foo</button>
             <span x-text="foo.bar"></span>
 
-            <button @click="foo.change">Change Foo</button>
+            <button @click="bar" id="2">Change Bar</button>
+            <article x-ref="target"></article>
         </div>
     `,
     `
         Alpine.data('test', () => ({
             foo: {
                 bar: 'baz',
-                change() { this.bar = 'qux' },
-            }
+
+                change() {
+                    this.foo.bar = 'qux'
+
+                    this.$refs.target.innerHTML = 'test2'
+                },
+            },
+            bar() {
+                this.$refs.target.innerHTML = 'test'
+            },
         }))
     `],
     ({ get }) => {
         get('span').should(haveText('baz'))
-        get('button').click()
+        get('#1').click()
         get('span').should(haveText('qux'))
+        get('article').should(haveText('test2'))
+        get('#2').click()
+        get('article').should(haveText('test'))
     }
 )

+ 101 - 65
tests/vitest/csp-parser.spec.js

@@ -30,23 +30,44 @@ describe('CSP Parser', () => {
     describe('Variable Access', () => {
         it('should access simple variables', () => {
             const scope = { foo: 'bar', count: 5 };
-            expect(generateRuntimeFunction('foo')(scope)).toBe('bar');
-            expect(generateRuntimeFunction('count')(scope)).toBe(5);
+            expect(generateRuntimeFunction('foo')({ scope })).toBe('bar');
+            expect(generateRuntimeFunction('count')({ scope })).toBe(5);
         });
 
         it('should throw on undefined variables', () => {
             expect(() => generateRuntimeFunction('nonExistent')()).toThrow('Undefined variable');
         });
 
-        it('should access global variables', () => {
-            expect(generateRuntimeFunction('console')()).toBe(console);
-            expect(generateRuntimeFunction('Math')()).toBe(Math);
-            expect(generateRuntimeFunction('JSON')()).toBe(JSON);
+        it('should not access global variables by default when called with no parameters', () => {
+            expect(() => generateRuntimeFunction('console')()).toThrow('Undefined variable: console');
+            expect(() => generateRuntimeFunction('Math')()).toThrow('Undefined variable: Math');
+            expect(() => generateRuntimeFunction('JSON')()).toThrow('Undefined variable: JSON');
         });
 
         it('should prefer scope over globals', () => {
             const scope = { console: 'local console' };
-            expect(generateRuntimeFunction('console')(scope)).toBe('local console');
+            expect(generateRuntimeFunction('console')({ scope })).toBe('local console');
+        });
+
+        it('should access global variables when allowGlobal is true', () => {
+            expect(generateRuntimeFunction('console')({ allowGlobal: true })).toBe(console);
+            expect(generateRuntimeFunction('Math')({ allowGlobal: true })).toBe(Math);
+            expect(generateRuntimeFunction('JSON')({ allowGlobal: true })).toBe(JSON);
+        });
+
+        it('should not access global variables when allowGlobal is false', () => {
+            expect(() => generateRuntimeFunction('console')({ allowGlobal: false })).toThrow('Undefined variable: console');
+            expect(() => generateRuntimeFunction('Math')({ allowGlobal: false })).toThrow('Undefined variable: Math');
+            expect(() => generateRuntimeFunction('JSON')({ allowGlobal: false })).toThrow('Undefined variable: JSON');
+        });
+
+        it('should default allowGlobal to false when empty object is passed', () => {
+            expect(() => generateRuntimeFunction('console')({})).toThrow('Undefined variable: console');
+        });
+
+        it('should prefer local scope over globals even when allowGlobal is true', () => {
+            const scope = { console: 'local console' };
+            expect(generateRuntimeFunction('console')({ scope, allowGlobal: true })).toBe('local console');
         });
     });
 
@@ -56,9 +77,9 @@ describe('CSP Parser', () => {
                 user: { name: 'John', age: 30 },
                 nested: { deep: { value: 'found' } }
             };
-            expect(generateRuntimeFunction('user.name')(scope)).toBe('John');
-            expect(generateRuntimeFunction('user.age')(scope)).toBe(30);
-            expect(generateRuntimeFunction('nested.deep.value')(scope)).toBe('found');
+            expect(generateRuntimeFunction('user.name')({ scope })).toBe('John');
+            expect(generateRuntimeFunction('user.age')({ scope })).toBe(30);
+            expect(generateRuntimeFunction('nested.deep.value')({ scope })).toBe('found');
         });
 
         it('should access properties with bracket notation', () => {
@@ -66,9 +87,9 @@ describe('CSP Parser', () => {
                 obj: { foo: 'bar', 'with-dash': 'works' },
                 key: 'foo'
             };
-            expect(generateRuntimeFunction('obj["foo"]')(scope)).toBe('bar');
-            expect(generateRuntimeFunction('obj["with-dash"]')(scope)).toBe('works');
-            expect(generateRuntimeFunction('obj[key]')(scope)).toBe('bar');
+            expect(generateRuntimeFunction('obj["foo"]')({ scope })).toBe('bar');
+            expect(generateRuntimeFunction('obj["with-dash"]')({ scope })).toBe('works');
+            expect(generateRuntimeFunction('obj[key]')({ scope })).toBe('bar');
         });
 
         it('should handle computed property access', () => {
@@ -76,13 +97,13 @@ describe('CSP Parser', () => {
                 arr: [1, 2, 3],
                 index: 1
             };
-            expect(generateRuntimeFunction('arr[index]')(scope)).toBe(2);
-            expect(generateRuntimeFunction('arr[0]')(scope)).toBe(1);
+            expect(generateRuntimeFunction('arr[index]')({ scope })).toBe(2);
+            expect(generateRuntimeFunction('arr[0]')({ scope })).toBe(1);
         });
 
         it('should throw on null/undefined property access', () => {
             const scope = { nullValue: null };
-            expect(() => generateRuntimeFunction('nullValue.prop')(scope)).toThrow('Cannot read property');
+            expect(() => generateRuntimeFunction('nullValue.prop')({ scope })).toThrow('Cannot read property');
         });
     });
 
@@ -92,8 +113,8 @@ describe('CSP Parser', () => {
                 getValue: () => 42,
                 getText: function() { return 'hello'; }
             };
-            expect(generateRuntimeFunction('getValue()')(scope)).toBe(42);
-            expect(generateRuntimeFunction('getText()')(scope)).toBe('hello');
+            expect(generateRuntimeFunction('getValue()')({ scope })).toBe(42);
+            expect(generateRuntimeFunction('getText()')({ scope })).toBe('hello');
         });
 
         it('should call functions with arguments', () => {
@@ -101,8 +122,8 @@ describe('CSP Parser', () => {
                 add: (a, b) => a + b,
                 greet: (name) => `Hello, ${name}!`
             };
-            expect(generateRuntimeFunction('add(2, 3)')(scope)).toBe(5);
-            expect(generateRuntimeFunction('greet("World")')(scope)).toBe('Hello, World!');
+            expect(generateRuntimeFunction('add(2, 3)')({ scope })).toBe(5);
+            expect(generateRuntimeFunction('greet("World")')({ scope })).toBe('Hello, World!');
         });
 
         it('should call methods on objects', () => {
@@ -113,8 +134,8 @@ describe('CSP Parser', () => {
                     add: function(n) { return this.value + n; }
                 }
             };
-            expect(generateRuntimeFunction('obj.getValue()')(scope)).toBe(10);
-            expect(generateRuntimeFunction('obj.add(5)')(scope)).toBe(15);
+            expect(generateRuntimeFunction('obj.getValue()')({ scope })).toBe(10);
+            expect(generateRuntimeFunction('obj.add(5)')({ scope })).toBe(15);
         });
 
         it('should preserve this context in method calls', () => {
@@ -124,7 +145,7 @@ describe('CSP Parser', () => {
                     increment: function() { this.count++; return this.count; }
                 }
             };
-            expect(generateRuntimeFunction('counter.increment()')(scope)).toBe(1);
+            expect(generateRuntimeFunction('counter.increment()')({ scope })).toBe(1);
             expect(scope.counter.count).toBe(1);
         });
 
@@ -136,13 +157,13 @@ describe('CSP Parser', () => {
                     }
                 }
             };
-            expect(generateRuntimeFunction('api.users.get(1)')(scope)).toEqual({ id: 1, name: 'User1' });
+            expect(generateRuntimeFunction('api.users.get(1)')({ scope })).toEqual({ id: 1, name: 'User1' });
         });
 
         it('should call global functions', () => {
-            expect(generateRuntimeFunction('parseInt("42")')()).toBe(42);
-            expect(generateRuntimeFunction('Math.max(1, 2, 3)')()).toBe(3);
-            expect(generateRuntimeFunction('JSON.stringify({a: 1})')()).toBe('{"a":1}');
+            expect(generateRuntimeFunction('parseInt("42")')({ allowGlobal: true })).toBe(42);
+            expect(generateRuntimeFunction('Math.max(1, 2, 3)')({ allowGlobal: true })).toBe(3);
+            expect(generateRuntimeFunction('JSON.stringify({a: 1})')({ allowGlobal: true })).toBe('{"a":1}');
         });
 
         it('should call methods with scope', () => {
@@ -153,7 +174,22 @@ describe('CSP Parser', () => {
                 }
             };
 
-            let fn = generateRuntimeFunction('foo.change')(scope)
+            let fn = generateRuntimeFunction('foo.change')({ scope, forceBindingRootScopeToFunctions: false })
+
+            fn.apply(fn, [])
+
+            expect(scope.foo.bar).toEqual('qux');
+        });
+
+        it('should call methods with root scope instead of nested scope if forceBindingRootScopeToFunctions is true', () => {
+            const scope = {
+                foo: {
+                    bar: 'baz',
+                    change() { this.foo.bar = 'qux' }
+                }
+            };
+
+            let fn = generateRuntimeFunction('foo.change')({ scope, forceBindingRootScopeToFunctions: true })
 
             fn.apply(fn, [])
 
@@ -174,7 +210,7 @@ describe('CSP Parser', () => {
 
         it('should parse arrays with variables', () => {
             const scope = { a: 1, b: 2, c: 3 };
-            expect(generateRuntimeFunction('[a, b, c]')(scope)).toEqual([1, 2, 3]);
+            expect(generateRuntimeFunction('[a, b, c]')({ scope })).toEqual([1, 2, 3]);
         });
 
         it('should parse nested arrays', () => {
@@ -200,7 +236,7 @@ describe('CSP Parser', () => {
 
         it('should parse objects with variable values', () => {
             const scope = { value: 'test', num: 100 };
-            expect(generateRuntimeFunction('{ prop: value, count: num }')(scope)).toEqual({
+            expect(generateRuntimeFunction('{ prop: value, count: num }')({ scope })).toEqual({
                 prop: 'test',
                 count: 100
             });
@@ -242,7 +278,7 @@ describe('CSP Parser', () => {
         it('should handle string concatenation', () => {
             expect(generateRuntimeFunction('"Hello" + " " + "World"')()).toBe('Hello World');
             const scope = { name: 'John' };
-            expect(generateRuntimeFunction('"Hello, " + name')(scope)).toBe('Hello, John');
+            expect(generateRuntimeFunction('"Hello, " + name')({ scope })).toBe('Hello, John');
         });
 
         it('should respect operator precedence', () => {
@@ -297,8 +333,8 @@ describe('CSP Parser', () => {
 
         it('should handle complex logical expressions', () => {
             const scope = { a: true, b: false, c: true };
-            expect(generateRuntimeFunction('a && (b || c)')(scope)).toBe(true);
-            expect(generateRuntimeFunction('!a || (b && c)')(scope)).toBe(false);
+            expect(generateRuntimeFunction('a && (b || c)')({ scope })).toBe(true);
+            expect(generateRuntimeFunction('!a || (b && c)')({ scope })).toBe(false);
         });
     });
 
@@ -307,7 +343,7 @@ describe('CSP Parser', () => {
             expect(generateRuntimeFunction('-5')()).toBe(-5);
             expect(generateRuntimeFunction('-(2 + 3)')()).toBe(-5);
             const scope = { value: 10 };
-            expect(generateRuntimeFunction('-value')(scope)).toBe(-10);
+            expect(generateRuntimeFunction('-value')({ scope })).toBe(-10);
         });
 
         it('should handle unary plus', () => {
@@ -325,27 +361,27 @@ describe('CSP Parser', () => {
 
         it('should handle ternary with expressions', () => {
             const scope = { age: 20 };
-            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor"')(scope)).toBe('adult');
+            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor"')({ scope })).toBe('adult');
             scope.age = 15;
-            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor"')(scope)).toBe('minor');
+            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor"')({ scope })).toBe('minor');
         });
 
         it('should handle nested ternary', () => {
             const scope = { score: 85 };
-            expect(generateRuntimeFunction('score >= 90 ? "A" : score >= 80 ? "B" : "C"')(scope)).toBe('B');
+            expect(generateRuntimeFunction('score >= 90 ? "A" : score >= 80 ? "B" : "C"')({ scope })).toBe('B');
         });
     });
 
     describe('Assignment Operators', () => {
         it('should handle simple assignment', () => {
             const scope = { x: 0 };
-            expect(generateRuntimeFunction('x = 5')(scope)).toBe(5);
+            expect(generateRuntimeFunction('x = 5')({ scope })).toBe(5);
             expect(scope.x).toBe(5);
         });
 
         it('should handle property assignment', () => {
             const scope = { obj: { prop: 0 } };
-            expect(generateRuntimeFunction('obj.prop = 10')(scope)).toBe(10);
+            expect(generateRuntimeFunction('obj.prop = 10')({ scope })).toBe(10);
             expect(scope.obj.prop).toBe(10);
         });
 
@@ -354,13 +390,13 @@ describe('CSP Parser', () => {
                 obj: { foo: 0 },
                 key: 'foo'
             };
-            expect(generateRuntimeFunction('obj[key] = 20')(scope)).toBe(20);
+            expect(generateRuntimeFunction('obj[key] = 20')({ scope })).toBe(20);
             expect(scope.obj.foo).toBe(20);
         });
 
         it('should handle chained assignment', () => {
             const scope = { a: 0, b: 0 };
-            expect(generateRuntimeFunction('a = b = 5')(scope)).toBe(5);
+            expect(generateRuntimeFunction('a = b = 5')({ scope })).toBe(5);
             expect(scope.a).toBe(5);
             expect(scope.b).toBe(5);
         });
@@ -369,33 +405,33 @@ describe('CSP Parser', () => {
     describe('Increment/Decrement Operators', () => {
         it('should handle prefix increment', () => {
             const scope = { x: 5 };
-            expect(generateRuntimeFunction('++x')(scope)).toBe(6);
+            expect(generateRuntimeFunction('++x')({ scope })).toBe(6);
             expect(scope.x).toBe(6);
         });
 
         it('should handle postfix increment', () => {
             const scope = { x: 5 };
-            expect(generateRuntimeFunction('x++')(scope)).toBe(5);
+            expect(generateRuntimeFunction('x++')({ scope })).toBe(5);
             expect(scope.x).toBe(6);
         });
 
         it('should handle prefix decrement', () => {
             const scope = { x: 5 };
-            expect(generateRuntimeFunction('--x')(scope)).toBe(4);
+            expect(generateRuntimeFunction('--x')({ scope })).toBe(4);
             expect(scope.x).toBe(4);
         });
 
         it('should handle postfix decrement', () => {
             const scope = { x: 5 };
-            expect(generateRuntimeFunction('x--')(scope)).toBe(5);
+            expect(generateRuntimeFunction('x--')({ scope })).toBe(5);
             expect(scope.x).toBe(4);
         });
 
         it('should handle increment on properties', () => {
             const scope = { obj: { count: 10 } };
-            expect(generateRuntimeFunction('obj.count++')(scope)).toBe(10);
+            expect(generateRuntimeFunction('obj.count++')({ scope })).toBe(10);
             expect(scope.obj.count).toBe(11);
-            expect(generateRuntimeFunction('++obj.count')(scope)).toBe(12);
+            expect(generateRuntimeFunction('++obj.count')({ scope })).toBe(12);
             expect(scope.obj.count).toBe(12);
         });
     });
@@ -412,8 +448,8 @@ describe('CSP Parser', () => {
                 user: { role: 'admin', active: true },
                 permissions: ['read', 'write', 'delete']
             };
-            expect(generateRuntimeFunction('user.role === "admin" && user.active')(scope)).toBe(true);
-            expect(generateRuntimeFunction('user.role === "user" || user.active')(scope)).toBe(true);
+            expect(generateRuntimeFunction('user.role === "admin" && user.active')({ scope })).toBe(true);
+            expect(generateRuntimeFunction('user.role === "user" || user.active')({ scope })).toBe(true);
         });
 
         it('should handle method calls with complex arguments', () => {
@@ -425,8 +461,8 @@ describe('CSP Parser', () => {
                 x: 2,
                 y: 3
             };
-            expect(generateRuntimeFunction('math.add(x * 2, y + 1)')(scope)).toBe(8);
-            expect(generateRuntimeFunction('math.multiply(math.add(x, y), 2)')(scope)).toBe(10);
+            expect(generateRuntimeFunction('math.add(x * 2, y + 1)')({ scope })).toBe(8);
+            expect(generateRuntimeFunction('math.multiply(math.add(x, y), 2)')({ scope })).toBe(10);
         });
     });
 
@@ -436,7 +472,7 @@ describe('CSP Parser', () => {
             const scope = {
                 getValue: function() { return this.value; }
             };
-            expect(generateRuntimeFunction('getValue()')(scope, context)).toBe(42);
+            expect(generateRuntimeFunction('getValue()')({ scope, context })).toBe(42);
         });
 
         it('should preserve method context over provided context', () => {
@@ -447,7 +483,7 @@ describe('CSP Parser', () => {
                     getValue: function() { return this.value; }
                 }
             };
-            expect(generateRuntimeFunction('obj.getValue()')(scope, context)).toBe(42);
+            expect(generateRuntimeFunction('obj.getValue()')({ scope, context })).toBe(42);
         });
     });
 
@@ -485,7 +521,7 @@ describe('CSP Parser', () => {
             const scope = {
                 a: { b: { c: { d: { e: 'deep' } } } }
             };
-            expect(generateRuntimeFunction('a.b.c.d.e')(scope)).toBe('deep');
+            expect(generateRuntimeFunction('a.b.c.d.e')({ scope })).toBe('deep');
         });
 
         it('should handle complex nested structures', () => {
@@ -498,8 +534,8 @@ describe('CSP Parser', () => {
                 },
                 index: 1
             };
-            expect(generateRuntimeFunction('data.users[index].name')(scope)).toBe('Bob');
-            expect(generateRuntimeFunction('data.users[0].scores[2]')(scope)).toBe(88);
+            expect(generateRuntimeFunction('data.users[index].name')({ scope })).toBe('Bob');
+            expect(generateRuntimeFunction('data.users[0].scores[2]')({ scope })).toBe(88);
         });
     });
 
@@ -584,7 +620,7 @@ describe('CSP Parser', () => {
         it('should not support dynamic code execution', () => {
             // eval is now accessible as a global, but the CSP will catch it at runtime
             // Here we test that eval runs but the string code isn't defined
-            expect(() => generateRuntimeFunction('eval("code")')()).toThrow('code is not defined');
+            expect(() => generateRuntimeFunction('eval("code")')({ allowGlobal: true })).toThrow('code is not defined');
 
             // new operator is not supported by parser
             expect(() => generateRuntimeFunction('new Function("code")')).toThrow();
@@ -605,7 +641,7 @@ describe('CSP Parser', () => {
             expect(generateRuntimeFunction('false || true;')()).toBe(true);
 
             const scope = { name: 'world' };
-            expect(generateRuntimeFunction('"hello " + name;')(scope)).toBe('hello world');
+            expect(generateRuntimeFunction('"hello " + name;')({ scope })).toBe('hello world');
         });
 
         it('should handle function calls with trailing semicolons', () => {
@@ -617,27 +653,27 @@ describe('CSP Parser', () => {
                 }
             };
 
-            expect(generateRuntimeFunction('getValue();')(scope)).toBe(42);
-            expect(generateRuntimeFunction('obj.method();')(scope)).toBe('test');
+            expect(generateRuntimeFunction('getValue();')({ scope })).toBe(42);
+            expect(generateRuntimeFunction('obj.method();')({ scope })).toBe('test');
         });
 
         it('should handle assignments with trailing semicolons', () => {
             const scope = { x: 0, obj: { prop: 5 } };
 
-            expect(generateRuntimeFunction('x = 10;')(scope)).toBe(10);
+            expect(generateRuntimeFunction('x = 10;')({ scope })).toBe(10);
             expect(scope.x).toBe(10);
 
-            expect(generateRuntimeFunction('obj.prop = 20;')(scope)).toBe(20);
+            expect(generateRuntimeFunction('obj.prop = 20;')({ scope })).toBe(20);
             expect(scope.obj.prop).toBe(20);
         });
 
         it('should handle increment/decrement with trailing semicolons', () => {
             const scope = { count: 5 };
 
-            expect(generateRuntimeFunction('++count;')(scope)).toBe(6);
+            expect(generateRuntimeFunction('++count;')({ scope })).toBe(6);
             expect(scope.count).toBe(6);
 
-            expect(generateRuntimeFunction('count--;')(scope)).toBe(6);
+            expect(generateRuntimeFunction('count--;')({ scope })).toBe(6);
             expect(scope.count).toBe(5);
         });
 
@@ -646,7 +682,7 @@ describe('CSP Parser', () => {
             expect(generateRuntimeFunction('false ? 1 : 2;')()).toBe(2);
 
             const scope = { age: 25 };
-            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor";')(scope)).toBe('adult');
+            expect(generateRuntimeFunction('age >= 18 ? "adult" : "minor";')({ scope })).toBe('adult');
         });
 
         it('should work without semicolons (backward compatibility)', () => {