Files
overlookmotel c2c8f806f9 refactor(napi/parser): raw transfer store source text at end of buffer (#22392)
Oxlint's raw transfer implementation stores the source text at end of the buffer. Prior to this PR `napi/parser` stored source text at the start of the buffer.

Bring `napi/parser` into line with Oxlint, by also storing source text at end of the buffer.

Making all versions of raw transfer that we use align with each other removes complication (all versions now have the same implementation of `deserializeStr`) and also removes some fragile code where it'd be easy to accidentally trigger UB (see the updated comments in `arena/fixed_size/windows.rs` and `arena/alloc_impl.rs`).

Unfortunately, storing source at end of buffer is slightly less efficient than storing it at the start. We'll switch all implementations to storing source at start of buffer when we move to storing all strings (source text and all other strings) together in a single contiguous block. But in meantime, this slight inefficiency is outweighed by the gain in safety.
2026-05-14 22:26:41 +00:00

163 lines
6.3 KiB
JavaScript

import { DATA_POINTER_POS_32, PROGRAM_OFFSET } from "../generated/constants.js";
import { RawTransferData } from "../generated/lazy/constructors.js";
import { walkProgram } from "../generated/lazy/walk.js";
import { parseAsyncRawImpl, parseSyncRawImpl, returnBufferToCache } from "./common.js";
import { TOKEN } from "./lazy-common.js";
import { getVisitorsArr } from "./visitor.js";
export { Visitor } from "./visitor.js";
/**
* Parse JS/TS source synchronously on current thread.
*
* The data in buffer is not deserialized. Is deserialized to JS objects lazily, when accessing the
* properties of objects.
*
* e.g. `program` in returned object is an instance of `Program` class, with getters for `start`, `end`,
* `body` etc.
*
* Returned object contains a `visit` function which can be used to visit the AST with a `Visitor`
* (`Visitor` class can be obtained by calling `experimentalGetLazyVisitor()`).
*
* Returned object contains a `dispose` method. When finished with this AST, it's advisable to call
* `dispose`, to return the buffer to the cache, so it can be reused.
* Garbage collector should do this anyway at some point, but on an unpredictable schedule,
* so it's preferable to call `dispose` manually, to ensure the buffer can be reused immediately.
*
* @param {string} filename - Filename
* @param {string} sourceText - Source text of file
* @param {Object} options - Parsing options
* @returns {Object} - Object with property getters for `program`, `module`, `comments`, and `errors`,
* and `dispose` and `visit` methods
*/
export function parseSyncLazy(filename, sourceText, options) {
return parseSyncRawImpl(filename, sourceText, options, construct);
}
/**
* Parse JS/TS source asynchronously on a separate thread.
*
* The data in buffer is not deserialized. Is deserialized to JS objects lazily, when accessing the
* properties of objects.
*
* e.g. `program` in returned object is an instance of `Program` class, with getters for `start`, `end`,
* `body` etc.
*
* Because this function does not deserialize the AST, unlike `parse`, very little work happens
* on current thread in this function. Deserialization work only occurs when properties of the objects
* are accessed.
*
* Returned object contains a `visit` function which can be used to visit the AST with a `Visitor`
* (`Visitor` class can be obtained by calling `experimentalGetLazyVisitor()`).
*
* Returned object contains a `dispose` method. When finished with this AST, it's advisable to call
* `dispose`, to return the buffer to the cache, so it can be reused.
* Garbage collector should do this anyway at some point, but on an unpredictable schedule,
* so it's preferable to call `dispose` manually, to ensure the buffer can be reused immediately.
*
* @param {string} filename - Filename
* @param {string} sourceText - Source text of file
* @param {Object} options - Parsing options
* @returns {Object} - Object with property getters for `program`, `module`, `comments`, and `errors`,
* and `dispose` and `visit` methods
*/
export function parse(filename, sourceText, options) {
return parseAsyncRawImpl(filename, sourceText, options, construct);
}
// Registry for buffers which are held by lazily-deserialized ASTs.
// Returns buffer to cache when the `ast` wrapper is garbage collected.
//
// Check for existence of `FinalizationRegistry`, to avoid errors on old versions of NodeJS
// which don't support it. e.g. Prettier supports NodeJS v14.
// Raw transfer is disabled on NodeJS before v22, so it doesn't matter if this is `null` on old NodeJS
// - it'll never be accessed in that case.
const bufferRecycleRegistry =
typeof FinalizationRegistry === "undefined"
? null
: new FinalizationRegistry(returnBufferToCache);
/**
* Get an object with getters which lazy deserialize AST and other data from buffer.
*
* Object also includes `dispose` and `visit` functions.
*
* @param {Uint8Array} buffer - Buffer containing AST in raw form
* @param {string} sourceText - Source for the file
* @param {number} sourceStartPos - Position of first byte of source text in buffer
* @param {number} sourceByteLen - Length of source text in UTF-8 bytes
* @param {Object} _options - Parsing options
* @returns {Object} - Object with property getters for `program`, `module`, `comments`, and `errors`,
* and `dispose` and `visit` methods
*/
function construct(buffer, sourceText, sourceStartPos, sourceByteLen, _options) {
// Create AST object
const sourceIsAscii = sourceText.length === sourceByteLen;
const ast = {
buffer,
sourceText,
sourceStartPos,
sourceByteLen,
sourceIsAscii,
nodes: new Map(),
token: TOKEN,
};
// Register `ast` with the recycle registry so buffer is returned to cache
// when `ast` is garbage collected
bufferRecycleRegistry.register(ast, buffer, ast);
// Get root data class instance
const rawDataPos = buffer.int32[DATA_POINTER_POS_32];
const data = new RawTransferData(rawDataPos, ast);
return {
get program() {
return data.program;
},
get module() {
return data.module;
},
get comments() {
return data.comments;
},
get errors() {
return data.errors;
},
dispose: dispose.bind(null, ast),
visit(visitor) {
walkProgram(rawDataPos + PROGRAM_OFFSET, ast, getVisitorsArr(visitor));
},
};
}
/**
* Dispose of this AST.
*
* After calling this method, trying to read any nodes from this AST may cause an error.
*
* Buffer is returned to the cache to be reused.
*
* The buffer would be returned to the cache anyway, once all nodes of the AST are garbage collected,
* but calling `dispose` is preferable, as it will happen immediately.
* Otherwise, garbage collector may take time to collect the `ast` object, and new buffers may be created
* in the meantime, when we could have reused this one.
*
* @param {Object} ast - AST object containing buffer etc
* @returns {undefined}
*/
function dispose(ast) {
// Return buffer to cache, to be reused
returnBufferToCache(ast.buffer);
// Remove connection between `ast` and the buffer
ast.buffer = null;
// Clear other contents of `ast`, so they can be garbage collected
ast.sourceText = null;
ast.nodes = null;
// Remove `ast` from recycling register.
// When `ast` is garbage collected, there's no longer any action to be taken.
bufferRecycleRegistry.unregister(ast);
}