Skip to content

Commit 85f5f8c

Browse files
authored
Merge pull request #41 from jmespath-community/fix-scope-null-handling
fix(scope): improve/propose variable lookup and null handling in scope chain
2 parents 90c6ea4 + 3cc899f commit 85f5f8c

File tree

3 files changed

+65
-8
lines changed

3 files changed

+65
-8
lines changed

src/Scope.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ export class ScopeChain {
44
private inner?: ScopeChain = undefined;
55
private data: JSONObject = {};
66

7+
get currentScopeData(): JSONObject {
8+
return this.data;
9+
}
10+
711
public withScope(data: JSONObject): ScopeChain {
812
const outer: ScopeChain = new ScopeChain();
913
outer.inner = this;
@@ -12,15 +16,14 @@ export class ScopeChain {
1216
}
1317

1418
public getValue(identifier: string): JSONValue {
15-
if (identifier in this.data) {
16-
const result = this.data[identifier];
17-
if (result !== null && result !== undefined) {
18-
return result;
19-
}
19+
if (Object.prototype.hasOwnProperty.call(this.data, identifier)) {
20+
return this.data[identifier];
2021
}
22+
2123
if (this.inner) {
2224
return this.inner.getValue(identifier);
2325
}
26+
2427
return null;
2528
}
2629
}

src/TreeInterpreter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ export class TreeInterpreter {
5959
}
6060
case 'Variable': {
6161
const variable = node.name;
62-
const result = this._scope.getValue(variable) ?? null;
63-
if (result === null || result === undefined) {
62+
if (
63+
!this._scope.getValue(variable) &&
64+
!Object.prototype.hasOwnProperty.call(this._scope.currentScopeData, variable)
65+
) {
6466
throw new Error(`Error referencing undefined variable ${variable}`);
6567
}
66-
return result;
68+
return this._scope.getValue(variable);
6769
}
6870
case 'IndexExpression':
6971
return this.visit(node.right, this.visit(node.left, value));

test/scopes.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { describe, it, expect } from 'vitest';
12
import { Scope } from '../src';
23

34
describe('scopes', () => {
@@ -24,4 +25,55 @@ describe('scopes', () => {
2425
expect(outer.getValue('foo')).toEqual('bar');
2526
}
2627
});
28+
it('should not return value for non-existent identifiers', () => {
29+
const scope = Scope();
30+
{
31+
const scoped = scope.withScope({ foo: 'bar' });
32+
expect(scoped.getValue('baz')).toEqual(null);
33+
expect(scoped.getValue('foo')).toEqual('bar');
34+
}
35+
});
36+
it('should return null for identifiers even in nested scopes if absent', () => {
37+
const scope = Scope();
38+
{
39+
const outer = scope.withScope({ foo: 'bar' });
40+
{
41+
const inner = outer.withScope({ bar: 'baz' });
42+
expect(inner.getValue('qux')).toEqual(null);
43+
}
44+
expect(outer.getValue('qux')).toEqual(null);
45+
}
46+
});
47+
it('should handle values in nested scopes differently from outer scopes', () => {
48+
const scope = Scope();
49+
{
50+
const outer = scope.withScope({ foo: 'bar' });
51+
{
52+
const inner = outer.withScope({ bar: 'baz' });
53+
expect(inner.getValue('foo')).toEqual('bar');
54+
expect(inner.getValue('bar')).toEqual('baz');
55+
}
56+
}
57+
});
58+
it('should not fall through to outer scope when key is in current scope with null/undefined', () => {
59+
const scope = Scope();
60+
{
61+
const outer = scope.withScope({ foo: 'bar' });
62+
{
63+
const inner = outer.withScope({ foo: null });
64+
expect(inner.getValue('foo')).toEqual(null);
65+
}
66+
}
67+
});
68+
it('should properly differentiate between keys absent entirely and those in outer scopes', () => {
69+
const scope = Scope();
70+
{
71+
const outer = scope.withScope({ foo: 'bar' });
72+
{
73+
const inner = outer.withScope({});
74+
expect(inner.getValue('foo')).toEqual('bar');
75+
expect(inner.getValue('baz')).toEqual(null);
76+
}
77+
}
78+
});
2779
});

0 commit comments

Comments
 (0)