@openmrs/esm-expression-evaluator 11.0.1-pre.5771 → 11.0.1-pre.5776

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 5 files with swc (208.86ms)
1
+ [0] Successfully compiled: 5 files with swc (225.5ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
package/dist/extractor.js CHANGED
@@ -81,58 +81,39 @@ function visitExpression(expression, context) {
81
81
  }
82
82
  }
83
83
  function visitUnaryExpression(expression, context) {
84
- return visitExpression(expression.argument, context);
84
+ visitExpression(expression.argument, context);
85
85
  }
86
86
  function visitBinaryExpression(expression, context) {
87
- const left = visitExpression(expression.left, context);
88
- const right = visitExpression(expression.right, context);
89
- return [
90
- left,
91
- right
92
- ].filter(Boolean);
87
+ visitExpression(expression.left, context);
88
+ visitExpression(expression.right, context);
93
89
  }
94
90
  function visitConditionalExpression(expression, context) {
95
- const consequent = visitExpression(expression.consequent, context);
96
- const test = visitExpression(expression.test, context);
97
- const alternate = visitExpression(expression.alternate, context);
98
- return [
99
- consequent,
100
- test,
101
- alternate
102
- ].filter(Boolean);
91
+ visitExpression(expression.consequent, context);
92
+ visitExpression(expression.test, context);
93
+ visitExpression(expression.alternate, context);
103
94
  }
104
95
  function visitCallExpression(expression, context) {
105
- const fn = visitExpression(expression.callee, context);
96
+ visitExpression(expression.callee, context);
106
97
  expression.arguments?.map(handleNullableExpression(context));
107
- return fn;
108
98
  }
109
99
  function visitArrowFunctionExpression(expression, context) {
110
- const newContext = {
111
- ...context
112
- };
113
- newContext.isLocalExpression = true;
114
- const params = expression.params?.map(handleNullableExpression(newContext)) ?? [];
115
- const bodyVariables = visitExpression(expression.body, newContext) ?? [];
116
- if (bodyVariables && Array.isArray(bodyVariables)) {
117
- for (const v of bodyVariables){
118
- if (!params.includes(v)) {
119
- context.variables.add(v);
120
- }
100
+ const boundNames = new Set(context.boundNames);
101
+ for (const param of expression.params ?? []){
102
+ if (param?.type === 'Identifier') {
103
+ boundNames.add(param.name);
121
104
  }
122
105
  }
106
+ visitExpression(expression.body, {
107
+ ...context,
108
+ boundNames
109
+ });
123
110
  }
124
111
  function visitMemberExpression(expression, context) {
125
112
  visitExpression(expression.object, context);
126
113
  // the `b` of `a[b]` is a variable the caller has to supply, whereas the `b` of `a.b` is a property name
127
114
  if (expression.computed) {
128
115
  visitExpression(expression.property, context);
129
- return;
130
116
  }
131
- const newContext = {
132
- ...context
133
- };
134
- newContext.isLocalExpression = true;
135
- visitExpression(expression.property, newContext);
136
117
  }
137
118
  function visitArrayExpression(expression, context) {
138
119
  expression.elements?.map(handleNullableExpression(context));
@@ -149,12 +130,8 @@ function visitTemplateLiteral(expression, context) {
149
130
  }
150
131
  function visitTemplateElement(expression, context) {}
151
132
  function visitIdentifier(expression, context) {
152
- if (!(expression.name in context.globals)) {
153
- if (!context.isLocalExpression) {
154
- context.variables.add(expression.name);
155
- } else {
156
- return expression.name;
157
- }
133
+ if (!(expression.name in context.globals) && !context.boundNames.has(expression.name)) {
134
+ context.variables.add(expression.name);
158
135
  }
159
136
  }
160
137
  function visitLiteral(expression, context) {}
@@ -166,7 +143,7 @@ function createContextInternal(globals_) {
166
143
  globals: {
167
144
  ...globals_
168
145
  },
169
- isLocalExpression: false,
146
+ boundNames: new Set(),
170
147
  variables: new Set()
171
148
  };
172
149
  return context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-expression-evaluator",
3
- "version": "11.0.1-pre.5771",
3
+ "version": "11.0.1-pre.5776",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Utilities for evaluating user-defined expressions",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "url": "git+https://github.com/openmrs/openmrs-esm-core.git"
45
45
  },
46
46
  "bugs": {
47
- "url": "https://github.com/openmrs/openmrs-esm-core/issues"
47
+ "url": "https://openmrs.atlassian.net/jira/software/c/projects/O3/boards/37"
48
48
  },
49
49
  "homepage": "https://github.com/openmrs/openmrs-esm-core#readme",
50
50
  "publishConfig": {
@@ -51,6 +51,15 @@ describe('OpenMRS Expression Evaluator', () => {
51
51
  expect(evaluate('a.includes("v")', { a: 'value' })).toBe(true);
52
52
  expect(evaluate('"value".includes("v")')).toBe(true);
53
53
  expect(evaluate('(3.14159).toPrecision(3)')).toBe('3.14');
54
+ expect(evaluate('s.toUpperCase()', { s: 'active' })).toBe('ACTIVE');
55
+ expect(evaluate('n.toFixed(1)', { n: 3.14159 })).toBe('3.1');
56
+ });
57
+
58
+ it('should read properties of strings from the string itself', () => {
59
+ expect(evaluate('s.length', { s: 'active' })).toBe(6);
60
+ expect(evaluate('"active".length')).toBe(6);
61
+ expect(evaluate('s.length', { s: 'active' })).toBe(evaluate('s["length"]', { s: 'active' }));
62
+ expect(evaluate('s[0]', { s: 'active' })).toBe('a');
54
63
  });
55
64
 
56
65
  it('should give a useful error message for properties on missing objects', () => {
@@ -47,6 +47,26 @@ describe('OpenMRS Expression Extractor', () => {
47
47
  expect(extractVariableNames('[1, 2, 3].find(v => v === a)')).toEqual(['a']);
48
48
  });
49
49
 
50
+ it('extracts unbound variables from any kind of arrow function body', () => {
51
+ expect(extractVariableNames('a.map(x => f(b))')).toEqual(['a', 'f', 'b']);
52
+ expect(extractVariableNames('a.map(x => !b)')).toEqual(['a', 'b']);
53
+ expect(extractVariableNames('a.map(x => b.c)')).toEqual(['a', 'b']);
54
+ expect(extractVariableNames('a.map(x => x + b)')).toEqual(['a', 'b']);
55
+ expect(extractVariableNames('a.map(x => x ? b : c)')).toEqual(['a', 'b', 'c']);
56
+ expect(extractVariableNames('a.then(v => !isEmpty(b) ? v + 3 : v)')).toEqual(['a', 'isEmpty', 'b']);
57
+ });
58
+
59
+ it('excludes arrow function parameters, including from nested arrow functions', () => {
60
+ expect(extractVariableNames('a.map(x => x.y)')).toEqual(['a']);
61
+ expect(extractVariableNames('a.reduce((acc, x) => acc + x, 0)')).toEqual(['a']);
62
+ expect(extractVariableNames('a.map(x => x.items.filter(y => y === x || y === b))')).toEqual(['a', 'b']);
63
+ expect(extractVariableNames('a.map(x => f(y => y + x + z))')).toEqual(['a', 'f', 'z']);
64
+ });
65
+
66
+ it('only binds arrow function parameters within the arrow function body', () => {
67
+ expect(extractVariableNames('a.map(x => x) + x')).toEqual(['a', 'x']);
68
+ });
69
+
50
70
  it('supports real-world use-cases', () => {
51
71
  expect(extractVariableNames('!isEmpty(array)')).toEqual(['isEmpty', 'array']);
52
72
 
package/src/extractor.ts CHANGED
@@ -89,42 +89,34 @@ function visitExpression(expression: jsep.Expression, context: EvaluationContext
89
89
  }
90
90
 
91
91
  function visitUnaryExpression(expression: jsep.UnaryExpression, context: EvaluationContext) {
92
- return visitExpression(expression.argument, context);
92
+ visitExpression(expression.argument, context);
93
93
  }
94
94
 
95
95
  function visitBinaryExpression(expression: jsep.BinaryExpression, context: EvaluationContext) {
96
- const left = visitExpression(expression.left, context);
97
- const right = visitExpression(expression.right, context);
98
- return [left, right].filter(Boolean);
96
+ visitExpression(expression.left, context);
97
+ visitExpression(expression.right, context);
99
98
  }
100
99
 
101
100
  function visitConditionalExpression(expression: jsep.ConditionalExpression, context: EvaluationContext) {
102
- const consequent = visitExpression(expression.consequent, context);
103
- const test = visitExpression(expression.test, context);
104
- const alternate = visitExpression(expression.alternate, context);
105
- return [consequent, test, alternate].filter(Boolean);
101
+ visitExpression(expression.consequent, context);
102
+ visitExpression(expression.test, context);
103
+ visitExpression(expression.alternate, context);
106
104
  }
107
105
 
108
106
  function visitCallExpression(expression: jsep.CallExpression, context: EvaluationContext) {
109
- const fn = visitExpression(expression.callee, context);
107
+ visitExpression(expression.callee, context);
110
108
  expression.arguments?.map(handleNullableExpression(context));
111
- return fn;
112
109
  }
113
110
 
114
111
  function visitArrowFunctionExpression(expression: ArrowExpression, context: EvaluationContext) {
115
- const newContext = { ...context };
116
- newContext.isLocalExpression = true;
117
-
118
- const params = expression.params?.map(handleNullableExpression(newContext)) ?? [];
119
- const bodyVariables = visitExpression(expression.body, newContext) ?? [];
120
-
121
- if (bodyVariables && Array.isArray(bodyVariables)) {
122
- for (const v of bodyVariables) {
123
- if (!params.includes(v)) {
124
- context.variables.add(v);
125
- }
112
+ const boundNames = new Set(context.boundNames);
113
+ for (const param of expression.params ?? []) {
114
+ if (param?.type === 'Identifier') {
115
+ boundNames.add((param as jsep.Identifier).name);
126
116
  }
127
117
  }
118
+
119
+ visitExpression(expression.body, { ...context, boundNames });
128
120
  }
129
121
 
130
122
  function visitMemberExpression(expression: jsep.MemberExpression, context: EvaluationContext) {
@@ -133,12 +125,7 @@ function visitMemberExpression(expression: jsep.MemberExpression, context: Evalu
133
125
  // the `b` of `a[b]` is a variable the caller has to supply, whereas the `b` of `a.b` is a property name
134
126
  if (expression.computed) {
135
127
  visitExpression(expression.property, context);
136
- return;
137
128
  }
138
-
139
- const newContext = { ...context };
140
- newContext.isLocalExpression = true;
141
- visitExpression(expression.property, newContext);
142
129
  }
143
130
 
144
131
  function visitArrayExpression(expression: jsep.ArrayExpression, context: EvaluationContext) {
@@ -161,12 +148,8 @@ function visitTemplateLiteral(expression: TemplateLiteral, context: EvaluationCo
161
148
  function visitTemplateElement(expression: TemplateElement, context: EvaluationContext) {}
162
149
 
163
150
  function visitIdentifier(expression: jsep.Identifier, context: EvaluationContext) {
164
- if (!(expression.name in context.globals)) {
165
- if (!context.isLocalExpression) {
166
- context.variables.add(expression.name);
167
- } else {
168
- return expression.name;
169
- }
151
+ if (!(expression.name in context.globals) && !context.boundNames.has(expression.name)) {
152
+ context.variables.add(expression.name);
170
153
  }
171
154
  }
172
155
 
@@ -175,7 +158,8 @@ function visitLiteral(expression: jsep.Literal, context: EvaluationContext) {}
175
158
  // Internals
176
159
  interface EvaluationContext {
177
160
  globals: typeof globalsAsync;
178
- isLocalExpression: boolean;
161
+ /** Parameters of the enclosing arrow functions, which the caller does not supply */
162
+ boundNames: Set<string>;
179
163
  variables: Set<string>;
180
164
  }
181
165
 
@@ -186,7 +170,7 @@ function createAsynchronousContext(): EvaluationContext {
186
170
  function createContextInternal(globals_: typeof globalsAsync) {
187
171
  const context = {
188
172
  globals: { ...globals_ },
189
- isLocalExpression: false,
173
+ boundNames: new Set<string>(),
190
174
  variables: new Set<string>(),
191
175
  };
192
176