Skip to content

Commit 0427eb5

Browse files
authored
feat(NODE-4711)!: remove evalFunctions option (#539)
1 parent 633bd21 commit 0427eb5

File tree

9 files changed

+181
-131
lines changed

9 files changed

+181
-131
lines changed

docs/upgrade-to-v5.md

+32
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,41 @@ EJSON.parse("...", { strict: false }); /* migrate to */ EJSON.parse("...", { r
179179
// stringify
180180
EJSON.stringify({}, { strict: true }); /* migrate to */ EJSON.stringify({}, { relaxed: false });
181181
EJSON.stringify({}, { strict: false }); /* migrate to */ EJSON.stringify({}, { relaxed: true });
182+
```
182183

183184
### The BSON default export has been removed.
184185

185186
* If you import BSON commonjs style `const BSON = require('bson')` then the `BSON.default` property is no longer present.
186187
* If you import BSON esmodule style `import BSON from 'bson'` then this code will crash upon loading. **TODO: This is not the case right now but it will be after NODE-4713.**
187188
* This error will throw: `SyntaxError: The requested module 'bson' does not provide an export named 'default'`.
189+
190+
### `class Code` always converts `.code` to string
191+
192+
The `Code` class still supports the same constructor arguments as before.
193+
It will now convert the first argument to a string before saving it to the code property, see the following:
194+
195+
```typescript
196+
const myCode = new Code(function iLoveJavascript() { console.log('I love javascript') });
197+
// myCode.code === "function iLoveJavascript() { console.log('I love javascript') }"
198+
// typeof myCode.code === 'string'
199+
```
200+
201+
### `BSON.deserialize()` only returns `Code` instances
202+
203+
The deserialize options: `evalFunctions`, `cacheFunctions`, and `cacheFunctionsCrc32` have been removed.
204+
The `evalFunctions` option, when enabled, would return BSON Code typed values as eval-ed javascript functions, now it will always return Code instances.
205+
206+
See the following snippet for how to migrate:
207+
```typescript
208+
const bsonBytes = BSON.serialize(
209+
{ iLoveJavascript: function () { console.log('I love javascript') } },
210+
{ serializeFunctions: true } // serializeFunctions still works!
211+
);
212+
const result = BSON.deserialize(bsonBytes)
213+
// result.iLoveJavascript instanceof Code
214+
// result.iLoveJavascript.code === "function () { console.log('I love javascript') }"
215+
const iLoveJavascript = new Function(`return ${result.iLoveJavascript.code}`)();
216+
iLoveJavascript();
217+
// prints "I love javascript"
218+
// iLoveJavascript.name === "iLoveJavascript"
219+
```

src/code.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Document } from './bson';
22

33
/** @public */
44
export interface CodeExtended {
5-
$code: string | Function;
5+
$code: string;
66
$scope?: Document;
77
}
88

@@ -16,19 +16,27 @@ export class Code {
1616
return 'Code';
1717
}
1818

19-
code!: string | Function;
20-
scope?: Document;
19+
code: string;
20+
21+
// a code instance having a null scope is what determines whether
22+
// it is BSONType 0x0D (just code) / 0x0F (code with scope)
23+
scope: Document | null;
24+
2125
/**
2226
* @param code - a string or function.
2327
* @param scope - an optional scope for the function.
2428
*/
25-
constructor(code: string | Function, scope?: Document) {
26-
this.code = code;
27-
this.scope = scope;
29+
constructor(code: string | Function, scope?: Document | null) {
30+
this.code = code.toString();
31+
this.scope = scope ?? null;
2832
}
2933

30-
toJSON(): { code: string | Function; scope?: Document } {
31-
return { code: this.code, scope: this.scope };
34+
toJSON(): { code: string; scope?: Document } {
35+
if (this.scope != null) {
36+
return { code: this.code, scope: this.scope };
37+
}
38+
39+
return { code: this.code };
3240
}
3341

3442
/** @internal */
@@ -53,7 +61,7 @@ export class Code {
5361
inspect(): string {
5462
const codeJson = this.toJSON();
5563
return `new Code("${String(codeJson.code)}"${
56-
codeJson.scope ? `, ${JSON.stringify(codeJson.scope)}` : ''
64+
codeJson.scope != null ? `, ${JSON.stringify(codeJson.scope)}` : ''
5765
})`;
5866
}
5967
}

src/parser/calculate_size.ts

+4-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Binary } from '../binary';
22
import type { Document } from '../bson';
33
import * as constants from '../constants';
44
import { ByteUtils } from '../utils/byte_utils';
5-
import { isAnyArrayBuffer, isDate, isRegExp, normalizedFunctionString } from './utils';
5+
import { isAnyArrayBuffer, isDate, isRegExp } from './utils';
66

77
export function calculateObjectSize(
88
object: Document,
@@ -189,38 +189,14 @@ function calculateElement(
189189
);
190190
}
191191
case 'function':
192-
// WTF for 0.4.X where typeof /someregexp/ === 'function'
193-
if (value instanceof RegExp || isRegExp(value) || String.call(value) === '[object RegExp]') {
192+
if (serializeFunctions) {
194193
return (
195194
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
196195
1 +
197-
ByteUtils.utf8ByteLength(value.source) +
198-
1 +
199-
(value.global ? 1 : 0) +
200-
(value.ignoreCase ? 1 : 0) +
201-
(value.multiline ? 1 : 0) +
196+
4 +
197+
ByteUtils.utf8ByteLength(value.toString()) +
202198
1
203199
);
204-
} else {
205-
if (serializeFunctions && value.scope != null && Object.keys(value.scope).length > 0) {
206-
return (
207-
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
208-
1 +
209-
4 +
210-
4 +
211-
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
212-
1 +
213-
calculateObjectSize(value.scope, serializeFunctions, ignoreUndefined)
214-
);
215-
} else if (serializeFunctions) {
216-
return (
217-
(name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) +
218-
1 +
219-
4 +
220-
ByteUtils.utf8ByteLength(normalizedFunctionString(value)) +
221-
1
222-
);
223-
}
224200
}
225201
}
226202

src/parser/deserializer.ts

+2-63
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ import { validateUtf8 } from '../validate_utf8';
1919

2020
/** @public */
2121
export interface DeserializeOptions {
22-
/** evaluate functions in the BSON document scoped to the object deserialized. */
23-
evalFunctions?: boolean;
24-
/** cache evaluated functions for reuse. */
25-
cacheFunctions?: boolean;
26-
/**
27-
* use a crc32 code for caching, otherwise use the string of the function.
28-
* @deprecated this option to use the crc32 function never worked as intended
29-
* due to the fact that the crc32 function itself was never implemented.
30-
* */
31-
cacheFunctionsCrc32?: boolean;
3222
/** when deserializing a Long will fit it into a Number if it's smaller than 53 bits */
3323
promoteLongs?: boolean;
3424
/** when deserializing a Binary will return it as a node.js Buffer instance. */
@@ -67,8 +57,6 @@ export interface DeserializeOptions {
6757
const JS_INT_MAX_LONG = Long.fromNumber(constants.JS_INT_MAX);
6858
const JS_INT_MIN_LONG = Long.fromNumber(constants.JS_INT_MIN);
6959

70-
const functionCache: { [hash: string]: Function } = {};
71-
7260
export function deserialize(
7361
buffer: Uint8Array,
7462
options: DeserializeOptions,
@@ -120,9 +108,6 @@ function deserializeObject(
120108
options: DeserializeOptions,
121109
isArray = false
122110
) {
123-
const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions'];
124-
const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions'];
125-
126111
const fieldsAsRaw = options['fieldsAsRaw'] == null ? null : options['fieldsAsRaw'];
127112

128113
// Return raw bson buffer instead of parsing it
@@ -569,18 +554,7 @@ function deserializeObject(
569554
shouldValidateKey
570555
);
571556

572-
// If we are evaluating the functions
573-
if (evalFunctions) {
574-
// If we have cache enabled let's look for the md5 of the function in the cache
575-
if (cacheFunctions) {
576-
// Got to do this to avoid V8 deoptimizing the call due to finding eval
577-
value = isolateEval(functionString, functionCache, object);
578-
} else {
579-
value = isolateEval(functionString);
580-
}
581-
} else {
582-
value = new Code(functionString);
583-
}
557+
value = new Code(functionString);
584558

585559
// Update parse index position
586560
index = index + stringSize;
@@ -643,20 +617,7 @@ function deserializeObject(
643617
throw new BSONError('code_w_scope total size is too long, clips outer document');
644618
}
645619

646-
// If we are evaluating the functions
647-
if (evalFunctions) {
648-
// If we have cache enabled let's look for the md5 of the function in the cache
649-
if (cacheFunctions) {
650-
// Got to do this to avoid V8 deoptimizing the call due to finding eval
651-
value = isolateEval(functionString, functionCache, object);
652-
} else {
653-
value = isolateEval(functionString);
654-
}
655-
656-
value.scope = scopeObject;
657-
} else {
658-
value = new Code(functionString, scopeObject);
659-
}
620+
value = new Code(functionString, scopeObject);
660621
} else if (elementType === constants.BSON_DATA_DBPOINTER) {
661622
// Get the code string size
662623
const stringSize =
@@ -728,28 +689,6 @@ function deserializeObject(
728689
return object;
729690
}
730691

731-
/**
732-
* Ensure eval is isolated, store the result in functionCache.
733-
*
734-
* @internal
735-
*/
736-
function isolateEval(
737-
functionString: string,
738-
functionCache?: { [hash: string]: Function },
739-
object?: Document
740-
) {
741-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
742-
if (!functionCache) return new Function(functionString);
743-
// Check for cache hit, eval if missing and return cached function
744-
if (functionCache[functionString] == null) {
745-
// eslint-disable-next-line @typescript-eslint/no-implied-eval
746-
functionCache[functionString] = new Function(functionString);
747-
}
748-
749-
// Set the object
750-
return functionCache[functionString].bind(object);
751-
}
752-
753692
function getValidatedString(
754693
buffer: Uint8Array,
755694
start: number,

src/parser/serializer.ts

+7-22
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,7 @@ import type { MinKey } from '../min_key';
1313
import type { ObjectId } from '../objectid';
1414
import type { BSONRegExp } from '../regexp';
1515
import { ByteUtils } from '../utils/byte_utils';
16-
import {
17-
isBigInt64Array,
18-
isBigUInt64Array,
19-
isDate,
20-
isMap,
21-
isRegExp,
22-
isUint8Array,
23-
normalizedFunctionString
24-
} from './utils';
16+
import { isBigInt64Array, isBigUInt64Array, isDate, isMap, isRegExp, isUint8Array } from './utils';
2517

2618
/** @public */
2719
export interface SerializeOptions {
@@ -386,22 +378,15 @@ function serializeDouble(buffer: Uint8Array, key: string, value: Double, index:
386378
return index;
387379
}
388380

389-
function serializeFunction(
390-
buffer: Uint8Array,
391-
key: string,
392-
value: Function,
393-
index: number,
394-
_checkKeys = false,
395-
_depth = 0
396-
) {
381+
function serializeFunction(buffer: Uint8Array, key: string, value: Function, index: number) {
397382
buffer[index++] = constants.BSON_DATA_CODE;
398383
// Number of written bytes
399384
const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
400385
// Encode the name
401386
index = index + numberOfWrittenBytes;
402387
buffer[index++] = 0;
403388
// Function string
404-
const functionString = normalizedFunctionString(value);
389+
const functionString = value.toString();
405390

406391
// Write the string
407392
const size = ByteUtils.encodeUTF8Into(buffer, functionString, index + 4) + 1;
@@ -441,7 +426,7 @@ function serializeCode(
441426

442427
// Serialize the function
443428
// Get the function string
444-
const functionString = typeof value.code === 'string' ? value.code : value.code.toString();
429+
const functionString = value.code;
445430
// Index adjustment
446431
index = index + 4;
447432
// Write string into buffer
@@ -679,7 +664,7 @@ export function serializeInto(
679664
} else if (value['_bsontype'] === 'Double') {
680665
index = serializeDouble(buffer, key, value, index);
681666
} else if (typeof value === 'function' && serializeFunctions) {
682-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
667+
index = serializeFunction(buffer, key, value, index);
683668
} else if (value['_bsontype'] === 'Code') {
684669
index = serializeCode(
685670
buffer,
@@ -790,7 +775,7 @@ export function serializeInto(
790775
ignoreUndefined
791776
);
792777
} else if (typeof value === 'function' && serializeFunctions) {
793-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
778+
index = serializeFunction(buffer, key, value, index);
794779
} else if (value['_bsontype'] === 'Binary') {
795780
index = serializeBinary(buffer, key, value, index);
796781
} else if (value['_bsontype'] === 'Symbol') {
@@ -894,7 +879,7 @@ export function serializeInto(
894879
ignoreUndefined
895880
);
896881
} else if (typeof value === 'function' && serializeFunctions) {
897-
index = serializeFunction(buffer, key, value, index, checkKeys, depth);
882+
index = serializeFunction(buffer, key, value, index);
898883
} else if (value['_bsontype'] === 'Binary') {
899884
index = serializeBinary(buffer, key, value, index);
900885
} else if (value['_bsontype'] === 'Symbol') {

src/parser/utils.ts

-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
/**
2-
* Normalizes our expected stringified form of a function across versions of node
3-
* @param fn - The function to stringify
4-
*/
5-
export function normalizedFunctionString(fn: Function): string {
6-
return fn.toString().replace('function(', 'function (');
7-
}
8-
91
export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer {
102
return ['[object ArrayBuffer]', '[object SharedArrayBuffer]'].includes(
113
Object.prototype.toString.call(value)

0 commit comments

Comments
 (0)