oxc-parser
Version:
Oxc Parser Node API
328 lines (282 loc) • 10 kB
JavaScript
'use strict';
const { TOKEN, constructorError } = require('./lazy-common.js');
// Mapping from a proxy to the `NodeArray` that it wraps.
// Used by `slice`, `values`, `key` and `elements` methods.
//
// TODO: Is there any way to avoid this?
// Seems necessary because `this` in methods is a proxy, so accessing `this.#internal` throws.
const nodeArrays = new WeakMap();
// Function to get element from an array. Initialized in class static block below.
let getElement;
// An array of AST nodes where elements are deserialized lazily upon access.
//
// Extends `Array` to make `Array.isArray` return `true` for a `NodeArray`.
//
// TODO: Other methods could maybe be more optimal, avoiding going via proxy multiple times
// e.g. `some`, `indexOf`.
class NodeArray extends Array {
#internal;
/**
* Create a `NodeArray`.
*
* Constructor does not actually return a `NodeArray`, but one wrapped in a `Proxy`.
* The proxy intercepts accesses to elements and lazily deserializes them,
* and blocks mutation of elements or `length` property.
*
* @constructor
* @param {number} pos - Buffer position of first element
* @param {number} length - Number of elements
* @param {number} stride - Element size in bytes
* @param {Function} construct - Function to deserialize element
* @param {Object} ast - AST object
* @returns {Proxy<NodeArray>} - `NodeArray` wrapped in a `Proxy`
*/
constructor(pos, length, stride, construct, ast) {
if (ast?.token !== TOKEN) constructorError();
super(length);
this.#internal = { pos, ast, stride, construct };
const proxy = new Proxy(this, PROXY_HANDLERS);
nodeArrays.set(proxy, this);
return proxy;
}
// Allow `arr.filter`, `arr.map` etc.
static [Symbol.species] = Array;
// Override `values` method with a more efficient one that avoids going via proxy for every iteration.
// TODO: Benchmark to check that this is actually faster.
values() {
// Get actual `NodeArray`. `this` is a proxy.
const arr = nodeArrays.get(this);
return new NodeArrayValuesIterator(arr.#internal, arr.length);
}
// Override `keys` method with a more efficient one that avoids going via proxy for every iteration.
// TODO: Benchmark to check that this is actually faster.
keys() {
// Get actual `NodeArray`. `this` is a proxy.
// TODO: `this.length` would work here.
// Not sure which is more expensive - property lookup via proxy, or `WeakMap` lookup.
const arr = nodeArrays.get(this);
return new NodeArrayKeysIterator(arr.length);
}
// Override `entries` method with a more efficient one that avoids going via proxy for every iteration.
// TODO: Benchmark to check that this is actually faster.
entries() {
// Get actual `NodeArray`. `this` is a proxy.
const arr = nodeArrays.get(this);
return new NodeArrayEntriesIterator(arr.#internal, arr.length);
}
// This method is overwritten with reference to `values` method below.
// Defining dummy method here to prevent the later assignment altering the shape of class prototype.
[Symbol.iterator]() {}
// Override `slice` method to return a `NodeArray`.
slice(start, end) {
// Get actual `NodeArray`. `this` is a proxy.
const arr = nodeArrays.get(this);
if (arr === void 0) throw new Error('`slice` called on a value which is not a `NodeArray`');
start = toInt(start);
if (start < 0) {
start = arr.length + start;
if (start < 0) start = 0;
}
if (end === void 0) {
end = arr.length;
} else {
end = toInt(end);
if (end < 0) {
end = arr.length + end;
if (end < 0) end = 0;
} else if (end > arr.length) {
end = arr.length;
}
}
let length = end - start;
if (length <= 0 || start >= arr.length) {
start = 0;
length = 0;
}
const internal = arr.#internal,
{ stride } = internal;
return new NodeArray(internal.pos + start * stride, length, stride, internal.construct, internal.ast);
}
// Make `console.log` deserialize all elements.
[Symbol.for('nodejs.util.inspect.custom')]() {
const values = [...this.values()];
Object.setPrototypeOf(values, DebugNodeArray.prototype);
return values;
}
static {
/**
* Get element of `NodeArray` at index `index`.
* `index` must be in bounds (i.e. `< arr.length`).
*
* @param {NodeArray} arr - `NodeArray` object
* @param {number} index - Index of element to get
* @returns {*} - Element at index `index`
*/
getElement = (arr, index) => {
const internal = arr.#internal;
return (0, internal.construct)(internal.pos + index * internal.stride, internal.ast);
};
}
}
NodeArray.prototype[Symbol.iterator] = NodeArray.prototype.values;
module.exports = NodeArray;
// Iterator over values of a `NodeArray`.
// Returned by `values` method, and also used as iterator for `for (const node of nodeArray) {}`.
class NodeArrayValuesIterator {
#internal;
constructor(arrInternal, length) {
const { ast, pos, stride } = arrInternal || {};
if (ast?.token !== TOKEN) constructorError();
this.#internal = {
pos,
endPos: pos + length * stride,
ast,
construct: arrInternal.construct,
stride,
};
}
next() {
const internal = this.#internal,
{ pos } = internal;
if (pos === internal.endPos) return { done: true, value: null };
internal.pos = pos + internal.stride;
return { done: false, value: (0, internal.construct)(pos, internal.ast) };
}
[Symbol.iterator]() {
return this;
}
}
// Iterator over keys of a `NodeArray`. Returned by `keys` method.
class NodeArrayKeysIterator {
#internal;
constructor(length) {
// Don't bother gating constructor with `TOKEN` check.
// This iterator doesn't access the buffer, so is harmless.
this.#internal = { index: 0, length };
}
next() {
const internal = this.#internal,
{ index } = internal;
if (index === internal.length) return { done: true, value: null };
internal.index = index + 1;
return { done: false, value: index };
}
[Symbol.iterator]() {
return this;
}
}
// Iterator over values of a `NodeArray`. Returned by `entries` method.
class NodeArrayEntriesIterator {
#internal;
constructor(arrInternal, length) {
const { ast } = arrInternal || {};
if (ast?.token !== TOKEN) constructorError();
this.#internal = {
index: 0,
length,
pos: arrInternal.pos,
ast,
construct: arrInternal.construct,
stride: arrInternal.stride,
};
}
next() {
const internal = this.#internal,
{ index } = internal;
if (index === internal.length) return { done: true, value: null };
internal.index = index + 1;
return {
done: false,
value: [index, (0, internal.construct)(internal.pos + index * internal.stride, internal.ast)],
};
}
[Symbol.iterator]() {
return this;
}
}
// Class used for `[Symbol.for('nodejs.util.inspect.custom')]` method (for `console.log`).
const DebugNodeArray = class NodeArray extends Array {};
// Proxy handlers.
//
// Every `NodeArray` returned to user is wrapped in a `Proxy`, using these handlers.
// They lazily deserialize array elements upon access, and block mutation of array elements / `length`.
const PROXY_HANDLERS = {
// Return `true` for indexes which are in bounds.
// e.g. `'0' in arr`.
has(arr, key) {
if (isIndex(key)) return key * 1 < arr.length;
return Reflect.has(arr, key);
},
// Get entries which are in bounds.
get(arr, key) {
if (isIndex(key)) {
key *= 1;
if (key >= arr.length) return void 0;
return getElement(arr, key);
}
return Reflect.get(arr, key);
},
// Get descriptors which are in bounds.
getOwnPropertyDescriptor(arr, key) {
if (isIndex(key)) {
key *= 1;
if (key >= arr.length) return void 0;
// Cannot return `configurable: false` unfortunately
return { value: getElement(arr, key), writable: false, enumerable: true, configurable: true };
}
// Cannot return `writable: false` for `length` property unfortunately
return Reflect.getOwnPropertyDescriptor(arr, key);
},
// Prevent setting `length` or entries.
// Catches:
// * `Object.defineProperty(arr, 0, {value: null})`.
// * `arr[1] = null`.
// * `arr.length = 0`.
// * `Object.defineProperty(arr, 'length', {value: 0})`.
// * Other operations which mutate entries e.g. `arr.push(123)`.
defineProperty(arr, key, descriptor) {
if (key === 'length' || isIndex(key)) return false;
return Reflect.defineProperty(arr, key, descriptor);
},
// Prevent deleting entries.
deleteProperty(arr, key) {
// Note: `Reflect.deleteProperty(arr, 'length')` already returns `false`
if (isIndex(key)) return false;
return Reflect.deleteProperty(arr, key);
},
// Get keys, including element indexes.
ownKeys(arr) {
const keys = [];
for (let i = 0; i < arr.length; i++) {
keys.push(i + '');
}
keys.push(...Reflect.ownKeys(arr));
return keys;
},
};
/**
* Check if a key is a valid array index.
* Only strings comprising a plain integer are valid indexes.
* e.g. `"-1"`, `"01"`, `"0xFF"`, `"1e1"`, `"1 "` are not valid indexes.
*
* @param {*} - Key used for property lookup.
* @returns {boolean} - `true` if `key` is a valid array index.
*/
function isIndex(key) {
// TODO: Any way to do this without a regex?
return typeof key === 'string' && (key === '0' || INDEX_REGEX.test(key));
}
const INDEX_REGEX = /^[1-9]\d*$/;
/**
* Convert value to integer.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#integer_conversion
*
* @param {*} value - Value to convert to integer.
* @returns {number} - Integer
*/
function toInt(value) {
value = Math.trunc(+value);
// `value === 0` check is to convert -0 to 0
if (value === 0 || Number.isNaN(value)) return 0;
return value;
}