safe-mdx 1.6.0 → 1.7.0

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.
@@ -3818,7 +3818,7 @@ test('scope with function in spread attribute', () => {
3818
3818
  expect(html).toMatchInlineSnapshot(`"<h1>Spread test</h1>"`)
3819
3819
  })
3820
3820
 
3821
- test('scope with .map and arrow function callback fails without generate', () => {
3821
+ test('scope with .map and arrow function callback works without generate (safe interpreter)', () => {
3822
3822
  const scope = {
3823
3823
  items: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }],
3824
3824
  }
@@ -3828,16 +3828,8 @@ test('scope with .map and arrow function callback fails without generate', () =>
3828
3828
  `
3829
3829
 
3830
3830
  const { html, errors } = render(code, undefined, undefined, undefined, scope)
3831
- expect(errors).toMatchInlineSnapshot(`
3832
- [
3833
- {
3834
- "line": 1,
3835
- "message": "Failed to evaluate expression: items.map(item => item.name).join(", "). Expected options.generate to be the "generate" function from "escodegen"",
3836
- "type": "expression",
3837
- },
3838
- ]
3839
- `)
3840
- expect(html).toMatchInlineSnapshot(`""`)
3831
+ expect(errors).toMatchInlineSnapshot(`[]`)
3832
+ expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
3841
3833
  })
3842
3834
 
3843
3835
  test('scope with .map and arrow function callback works with generate', () => {
@@ -3853,3 +3845,355 @@ test('scope with .map and arrow function callback works with generate', () => {
3853
3845
  expect(errors).toMatchInlineSnapshot(`[]`)
3854
3846
  expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
3855
3847
  })
3848
+
3849
+ test('safe interpreter: arrow with block body and return', () => {
3850
+ const scope = {
3851
+ items: [1, 2, 3],
3852
+ }
3853
+
3854
+ const code = dedent`
3855
+ {items.map(x => { return x * 2 }).join(", ")}
3856
+ `
3857
+
3858
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3859
+ expect(errors).toMatchInlineSnapshot(`[]`)
3860
+ expect(html).toMatchInlineSnapshot(`"2, 4, 6"`)
3861
+ })
3862
+
3863
+ test('safe interpreter: arrow with multiple params', () => {
3864
+ const scope = {
3865
+ items: ['a', 'b', 'c'],
3866
+ }
3867
+
3868
+ const code = dedent`
3869
+ {items.map((item, i) => i + ":" + item).join(", ")}
3870
+ `
3871
+
3872
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3873
+ expect(errors).toMatchInlineSnapshot(`[]`)
3874
+ expect(html).toMatchInlineSnapshot(`"0:a, 1:b, 2:c"`)
3875
+ })
3876
+
3877
+ test('safe interpreter: arrow with object destructuring', () => {
3878
+ const scope = {
3879
+ items: [
3880
+ { name: 'Alice', age: 30 },
3881
+ { name: 'Bob', age: 25 },
3882
+ ],
3883
+ }
3884
+
3885
+ const code = dedent`
3886
+ {items.map(({ name, age }) => name + "(" + age + ")").join(", ")}
3887
+ `
3888
+
3889
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3890
+ expect(errors).toMatchInlineSnapshot(`[]`)
3891
+ expect(html).toMatchInlineSnapshot(`"Alice(30), Bob(25)"`)
3892
+ })
3893
+
3894
+ test('safe interpreter: arrow with ternary expression', () => {
3895
+ const scope = {
3896
+ items: [
3897
+ { name: 'Alice', active: true },
3898
+ { name: 'Bob', active: false },
3899
+ ],
3900
+ }
3901
+
3902
+ const code = dedent`
3903
+ {items.map(item => item.active ? item.name : "inactive").join(", ")}
3904
+ `
3905
+
3906
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3907
+ expect(errors).toMatchInlineSnapshot(`[]`)
3908
+ expect(html).toMatchInlineSnapshot(`"Alice, inactive"`)
3909
+ })
3910
+
3911
+ test('safe interpreter: .filter with arrow function', () => {
3912
+ const scope = {
3913
+ items: [1, 2, 3, 4, 5, 6],
3914
+ }
3915
+
3916
+ const code = dedent`
3917
+ {items.filter(x => x > 3).join(", ")}
3918
+ `
3919
+
3920
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3921
+ expect(errors).toMatchInlineSnapshot(`[]`)
3922
+ expect(html).toMatchInlineSnapshot(`"4, 5, 6"`)
3923
+ })
3924
+
3925
+ test('safe interpreter: .reduce with arrow function', () => {
3926
+ const scope = {
3927
+ items: [1, 2, 3, 4],
3928
+ }
3929
+
3930
+ const code = dedent`
3931
+ {items.reduce((acc, x) => acc + x, 0)}
3932
+ `
3933
+
3934
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3935
+ expect(errors).toMatchInlineSnapshot(`[]`)
3936
+ expect(html).toMatchInlineSnapshot(`"10"`)
3937
+ })
3938
+
3939
+ test('safe interpreter: chained .filter.map', () => {
3940
+ const scope = {
3941
+ users: [
3942
+ { name: 'Alice', role: 'admin' },
3943
+ { name: 'Bob', role: 'user' },
3944
+ { name: 'Charlie', role: 'admin' },
3945
+ ],
3946
+ }
3947
+
3948
+ const code = dedent`
3949
+ {users.filter(u => u.role === "admin").map(u => u.name).join(", ")}
3950
+ `
3951
+
3952
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3953
+ expect(errors).toMatchInlineSnapshot(`[]`)
3954
+ expect(html).toMatchInlineSnapshot(`"Alice, Charlie"`)
3955
+ })
3956
+
3957
+ test('safe interpreter: .find with arrow function', () => {
3958
+ const scope = {
3959
+ items: [
3960
+ { id: 1, name: 'Alice' },
3961
+ { id: 2, name: 'Bob' },
3962
+ ],
3963
+ }
3964
+
3965
+ const code = dedent`
3966
+ {items.find(item => item.id === 2).name}
3967
+ `
3968
+
3969
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3970
+ expect(errors).toMatchInlineSnapshot(`[]`)
3971
+ expect(html).toMatchInlineSnapshot(`"Bob"`)
3972
+ })
3973
+
3974
+ test('safe interpreter: .some and .every with arrow functions', () => {
3975
+ const scope = {
3976
+ nums: [2, 4, 6],
3977
+ }
3978
+
3979
+ const code = dedent`
3980
+ {nums.every(n => n > 0) ? "all positive" : "nope"}
3981
+ `
3982
+
3983
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3984
+ expect(errors).toMatchInlineSnapshot(`[]`)
3985
+ expect(html).toMatchInlineSnapshot(`"all positive"`)
3986
+ })
3987
+
3988
+ test('safe interpreter: nested arrow functions', () => {
3989
+ const scope = {
3990
+ matrix: [[1, 2], [3, 4], [5, 6]],
3991
+ }
3992
+
3993
+ const code = dedent`
3994
+ {matrix.map(row => row.map(x => x * 10).join("-")).join(", ")}
3995
+ `
3996
+
3997
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3998
+ expect(errors).toMatchInlineSnapshot(`[]`)
3999
+ expect(html).toMatchInlineSnapshot(`"10-20, 30-40, 50-60"`)
4000
+ })
4001
+
4002
+ test('safe interpreter: arrow accessing outer scope variables', () => {
4003
+ const scope = {
4004
+ items: [1, 2, 3],
4005
+ multiplier: 5,
4006
+ }
4007
+
4008
+ const code = dedent`
4009
+ {items.map(x => x * multiplier).join(", ")}
4010
+ `
4011
+
4012
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4013
+ expect(errors).toMatchInlineSnapshot(`[]`)
4014
+ expect(html).toMatchInlineSnapshot(`"5, 10, 15"`)
4015
+ })
4016
+
4017
+ test('safe interpreter: arrow with block body and variable declaration', () => {
4018
+ const scope = {
4019
+ items: [{ first: 'John', last: 'Doe' }, { first: 'Jane', last: 'Smith' }],
4020
+ }
4021
+
4022
+ const code = dedent`
4023
+ {items.map(item => { const full = item.first + " " + item.last; return full }).join(", ")}
4024
+ `
4025
+
4026
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4027
+ expect(errors).toMatchInlineSnapshot(`[]`)
4028
+ expect(html).toMatchInlineSnapshot(`"John Doe, Jane Smith"`)
4029
+ })
4030
+
4031
+ test('safe interpreter: arrow with if/else in block body', () => {
4032
+ const scope = {
4033
+ items: [1, 2, 3, 4, 5],
4034
+ }
4035
+
4036
+ const code = dedent`
4037
+ {items.map(x => { if (x > 3) { return "big" } else { return "small" } }).join(", ")}
4038
+ `
4039
+
4040
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4041
+ expect(errors).toMatchInlineSnapshot(`[]`)
4042
+ expect(html).toMatchInlineSnapshot(`"small, small, small, big, big"`)
4043
+ })
4044
+
4045
+ test('safe interpreter: .sort with comparator arrow', () => {
4046
+ const scope = {
4047
+ items: [3, 1, 4, 1, 5],
4048
+ }
4049
+
4050
+ const code = dedent`
4051
+ {items.sort((a, b) => a - b).join(", ")}
4052
+ `
4053
+
4054
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4055
+ expect(errors).toMatchInlineSnapshot(`[]`)
4056
+ expect(html).toMatchInlineSnapshot(`"1, 1, 3, 4, 5"`)
4057
+ })
4058
+
4059
+ test('safe interpreter: arrow in JSX attribute', () => {
4060
+ const scope = {
4061
+ items: [{ name: 'Alice' }, { name: 'Bob' }],
4062
+ }
4063
+
4064
+ const code = dedent`
4065
+ <Heading level={items.map(i => i.name).join(", ")}>Title</Heading>
4066
+ `
4067
+
4068
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4069
+ expect(errors).toMatchInlineSnapshot(`[]`)
4070
+ expect(html).toMatchInlineSnapshot(`"<h1>Title</h1>"`)
4071
+ })
4072
+
4073
+ test('safe interpreter: arrow with array destructuring', () => {
4074
+ const scope = {
4075
+ pairs: [['a', 1], ['b', 2], ['c', 3]],
4076
+ }
4077
+
4078
+ const code = dedent`
4079
+ {pairs.map(([letter, num]) => letter + num).join(", ")}
4080
+ `
4081
+
4082
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4083
+ expect(errors).toMatchInlineSnapshot(`[]`)
4084
+ expect(html).toMatchInlineSnapshot(`"a1, b2, c3"`)
4085
+ })
4086
+
4087
+ test('safe interpreter: calling scope functions inside arrow callback', () => {
4088
+ const scope = {
4089
+ items: [{ name: 'alice' }, { name: 'bob' }],
4090
+ formatName: (s: string) => s.toUpperCase(),
4091
+ }
4092
+
4093
+ const code = dedent`
4094
+ {items.map(item => formatName(item.name)).join(", ")}
4095
+ `
4096
+
4097
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4098
+ expect(errors).toMatchInlineSnapshot(`[]`)
4099
+ expect(html).toMatchInlineSnapshot(`"ALICE, BOB"`)
4100
+ })
4101
+
4102
+ test('safe interpreter: calling scope function with multiple args inside arrow', () => {
4103
+ const scope = {
4104
+ items: [1, 2, 3],
4105
+ add: (a: number, b: number) => a + b,
4106
+ base: 10,
4107
+ }
4108
+
4109
+ const code = dedent`
4110
+ {items.map(x => add(x, base)).join(", ")}
4111
+ `
4112
+
4113
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4114
+ expect(errors).toMatchInlineSnapshot(`[]`)
4115
+ expect(html).toMatchInlineSnapshot(`"11, 12, 13"`)
4116
+ })
4117
+
4118
+ test('safe interpreter: scope function returning object used in arrow', () => {
4119
+ const scope = {
4120
+ ids: [1, 2, 3],
4121
+ getUser: (id: number) => ({ id, name: 'User' + id }),
4122
+ }
4123
+
4124
+ const code = dedent`
4125
+ {ids.map(id => getUser(id).name).join(", ")}
4126
+ `
4127
+
4128
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4129
+ expect(errors).toMatchInlineSnapshot(`[]`)
4130
+ expect(html).toMatchInlineSnapshot(`"User1, User2, User3"`)
4131
+ })
4132
+
4133
+ test('safe interpreter: arrow with default parameter', () => {
4134
+ const scope = {
4135
+ items: [undefined, 'hello', undefined],
4136
+ fallback: 'default',
4137
+ }
4138
+
4139
+ const code = dedent`
4140
+ {items.map((x = fallback) => x).join(", ")}
4141
+ `
4142
+
4143
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4144
+ expect(errors).toMatchInlineSnapshot(`[]`)
4145
+ expect(html).toMatchInlineSnapshot(`"default, hello, default"`)
4146
+ })
4147
+
4148
+ test('scope with template literal in expression', () => {
4149
+ const scope = {
4150
+ name: 'World',
4151
+ count: 3,
4152
+ }
4153
+
4154
+ const code = dedent`
4155
+ {${'`'}Hello ${'${'}name${'}'}, you have ${'${'}count${'}'} items${'`'}}
4156
+ `
4157
+
4158
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4159
+ expect(errors).toMatchInlineSnapshot(`[]`)
4160
+ expect(html).toMatchInlineSnapshot(`"Hello World, you have 3 items"`)
4161
+ })
4162
+
4163
+ test('scope with tagged template literal function', () => {
4164
+ const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
4165
+ return strings.reduce((result, str, i) => {
4166
+ return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
4167
+ }, '')
4168
+ }
4169
+
4170
+ const scope = {
4171
+ myTag,
4172
+ name: 'world',
4173
+ }
4174
+
4175
+ const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
4176
+
4177
+ const { html, errors } = render(code, undefined, undefined, undefined, scope, { generate })
4178
+ expect(errors).toMatchInlineSnapshot(`[]`)
4179
+ expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
4180
+ })
4181
+
4182
+ test('scope with tagged template literal without generate', () => {
4183
+ const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
4184
+ return strings.reduce((result, str, i) => {
4185
+ return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
4186
+ }, '')
4187
+ }
4188
+
4189
+ const scope = {
4190
+ myTag,
4191
+ name: 'world',
4192
+ }
4193
+
4194
+ const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
4195
+
4196
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4197
+ expect(errors).toMatchInlineSnapshot(`[]`)
4198
+ expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
4199
+ })
package/src/safe-mdx.tsx CHANGED
@@ -534,6 +534,19 @@ export class MdastToJsx {
534
534
  const options = hasScope || this.evaluateOptions
535
535
  ? { ...(hasScope ? { functions: true } : {}), ...this.evaluateOptions }
536
536
  : undefined
537
+
538
+ // When functions are enabled and the user hasn't provided their own
539
+ // `generate` (escodegen), inject our safe AST-interpreting visitors
540
+ // that handle ArrowFunctionExpression and FunctionExpression without
541
+ // using `new Function()` or `eval()`. This makes arrow function
542
+ // callbacks like `.map(x => x.name)` work in Cloudflare Workers.
543
+ if (options && options.functions && !options.generate) {
544
+ ;(options as any).visitors = {
545
+ ...(options as any).visitors,
546
+ ...createSafeFunctionVisitors(),
547
+ }
548
+ }
549
+
537
550
  return Evaluate.evaluate.sync(expression, context, options)
538
551
  }
539
552
 
@@ -1145,3 +1158,236 @@ export function mdastBfs(
1145
1158
  type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
1146
1159
  [key: string]: any
1147
1160
  }
1161
+
1162
+ /**
1163
+ * Bind function parameters to argument values, handling Identifier,
1164
+ * ObjectPattern, ArrayPattern, RestElement, and AssignmentPattern nodes.
1165
+ * Writes bindings into `ctx` in place.
1166
+ */
1167
+ function bindParams(
1168
+ params: any[],
1169
+ args: any[],
1170
+ ctx: Record<string, any>,
1171
+ visit: (node: any, context: any, parent?: any) => any,
1172
+ ) {
1173
+ for (let i = 0; i < params.length; i++) {
1174
+ const param = params[i]
1175
+ switch (param.type) {
1176
+ case 'Identifier':
1177
+ ctx[param.name] = args[i]
1178
+ break
1179
+ case 'RestElement':
1180
+ if (param.argument.type === 'Identifier') {
1181
+ ctx[param.argument.name] = args.slice(i)
1182
+ }
1183
+ break
1184
+ case 'AssignmentPattern': {
1185
+ const val =
1186
+ args[i] !== undefined
1187
+ ? args[i]
1188
+ : visit(param.right, ctx, param)
1189
+ if (param.left.type === 'Identifier') {
1190
+ ctx[param.left.name] = val
1191
+ }
1192
+ break
1193
+ }
1194
+ case 'ObjectPattern': {
1195
+ const obj = args[i] || {}
1196
+ for (const prop of param.properties) {
1197
+ if (prop.type === 'RestElement') {
1198
+ const used = new Set(
1199
+ param.properties
1200
+ .filter((p: any) => p !== prop)
1201
+ .map(
1202
+ (p: any) =>
1203
+ p.key?.name ?? p.key?.value,
1204
+ ),
1205
+ )
1206
+ const rest: Record<string, any> = {}
1207
+ for (const key of Object.keys(obj)) {
1208
+ if (!used.has(key)) rest[key] = obj[key]
1209
+ }
1210
+ if (prop.argument.type === 'Identifier') {
1211
+ ctx[prop.argument.name] = rest
1212
+ }
1213
+ } else {
1214
+ const key =
1215
+ prop.key.type === 'Identifier'
1216
+ ? prop.key.name
1217
+ : prop.key.value
1218
+ if (prop.value.type === 'Identifier') {
1219
+ ctx[prop.value.name] = obj[key]
1220
+ } else if (
1221
+ prop.value.type === 'AssignmentPattern'
1222
+ ) {
1223
+ const val =
1224
+ obj[key] !== undefined
1225
+ ? obj[key]
1226
+ : visit(
1227
+ prop.value.right,
1228
+ ctx,
1229
+ prop.value,
1230
+ )
1231
+ if (
1232
+ prop.value.left.type === 'Identifier'
1233
+ ) {
1234
+ ctx[prop.value.left.name] = val
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ break
1240
+ }
1241
+ case 'ArrayPattern': {
1242
+ const arr = args[i] || []
1243
+ for (let j = 0; j < param.elements.length; j++) {
1244
+ const elem = param.elements[j]
1245
+ if (!elem) continue
1246
+ if (elem.type === 'Identifier') {
1247
+ ctx[elem.name] = arr[j]
1248
+ } else if (
1249
+ elem.type === 'RestElement' &&
1250
+ elem.argument.type === 'Identifier'
1251
+ ) {
1252
+ ctx[elem.argument.name] = arr.slice(j)
1253
+ }
1254
+ }
1255
+ break
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+
1261
+ // Sentinel value to signal a return from inside a block body
1262
+ const RETURN_SENTINEL = Symbol('return')
1263
+
1264
+ /**
1265
+ * Execute a block statement body (array of statements) using the
1266
+ * eval-estree-expression visitor's `this.visit`. Returns the value
1267
+ * from the first ReturnStatement encountered, or undefined.
1268
+ */
1269
+ function executeBlockBody(
1270
+ body: any[],
1271
+ ctx: Record<string, any>,
1272
+ visit: (node: any, context: any, parent?: any) => any,
1273
+ parentNode: any,
1274
+ ): any {
1275
+ for (const stmt of body) {
1276
+ switch (stmt.type) {
1277
+ case 'ReturnStatement':
1278
+ return stmt.argument
1279
+ ? visit(stmt.argument, ctx, stmt)
1280
+ : undefined
1281
+ case 'ExpressionStatement':
1282
+ visit(stmt.expression, ctx, stmt)
1283
+ break
1284
+ case 'VariableDeclaration':
1285
+ for (const decl of stmt.declarations) {
1286
+ const value = decl.init
1287
+ ? visit(decl.init, ctx, decl)
1288
+ : undefined
1289
+ if (decl.id.type === 'Identifier') {
1290
+ ctx[decl.id.name] = value
1291
+ }
1292
+ }
1293
+ break
1294
+ case 'IfStatement': {
1295
+ const test = visit(stmt.test, ctx, stmt)
1296
+ if (test) {
1297
+ if (stmt.consequent.type === 'BlockStatement') {
1298
+ const result = executeBlockBody(
1299
+ stmt.consequent.body,
1300
+ ctx,
1301
+ visit,
1302
+ stmt,
1303
+ )
1304
+ if (result !== undefined) return result
1305
+ } else if (
1306
+ stmt.consequent.type === 'ReturnStatement'
1307
+ ) {
1308
+ return stmt.consequent.argument
1309
+ ? visit(
1310
+ stmt.consequent.argument,
1311
+ ctx,
1312
+ stmt.consequent,
1313
+ )
1314
+ : undefined
1315
+ } else {
1316
+ visit(stmt.consequent, ctx, stmt)
1317
+ }
1318
+ } else if (stmt.alternate) {
1319
+ if (stmt.alternate.type === 'BlockStatement') {
1320
+ const result = executeBlockBody(
1321
+ stmt.alternate.body,
1322
+ ctx,
1323
+ visit,
1324
+ stmt,
1325
+ )
1326
+ if (result !== undefined) return result
1327
+ } else if (
1328
+ stmt.alternate.type === 'ReturnStatement'
1329
+ ) {
1330
+ return stmt.alternate.argument
1331
+ ? visit(
1332
+ stmt.alternate.argument,
1333
+ ctx,
1334
+ stmt.alternate,
1335
+ )
1336
+ : undefined
1337
+ } else {
1338
+ visit(stmt.alternate, ctx, stmt)
1339
+ }
1340
+ }
1341
+ break
1342
+ }
1343
+ }
1344
+ }
1345
+ return undefined
1346
+ }
1347
+
1348
+ /**
1349
+ * Custom visitors for eval-estree-expression that interpret arrow functions
1350
+ * and function expressions by walking the AST recursively, without using
1351
+ * `new Function()` or `eval()`. This makes them safe for Cloudflare Workers
1352
+ * and other edge runtimes that block dynamic code evaluation.
1353
+ *
1354
+ * The visitors are called with `this` bound to the Expression evaluator
1355
+ * instance, giving access to `this.visit()` for recursive evaluation.
1356
+ */
1357
+ export function createSafeFunctionVisitors() {
1358
+ // Using a regular function (not arrow) so `this` is the Expression instance
1359
+ function functionExpressionVisitor(
1360
+ this: any,
1361
+ node: any,
1362
+ context: any,
1363
+ ) {
1364
+ const self = this
1365
+ return function (this: any, ...args: any[]) {
1366
+ const newContext = { ...context }
1367
+ bindParams(node.params, args, newContext, (n, ctx, p) =>
1368
+ self.visit(n, ctx, p),
1369
+ )
1370
+
1371
+ if (
1372
+ node.expression ||
1373
+ node.body.type !== 'BlockStatement'
1374
+ ) {
1375
+ // Expression body: x => x.name
1376
+ return self.visit(node.body, newContext, node)
1377
+ }
1378
+
1379
+ // Block body: x => { ... return ... }
1380
+ return executeBlockBody(
1381
+ node.body.body,
1382
+ newContext,
1383
+ (n, ctx, p) => self.visit(n, ctx, p),
1384
+ node,
1385
+ )
1386
+ }
1387
+ }
1388
+
1389
+ return {
1390
+ ArrowFunctionExpression: functionExpressionVisitor,
1391
+ FunctionExpression: functionExpressionVisitor,
1392
+ }
1393
+ }