256 lines
8.1 KiB
JavaScript
256 lines
8.1 KiB
JavaScript
|
|
import { createRequire } from "node:module";
|
||
|
|
import { TOKENS_OFFSET_POS_32, TOKENS_LEN_POS_32 } from "../generated/constants.js";
|
||
|
|
import { isJsAst, parseAsyncRawImpl, parseSyncRawImpl, returnBufferToCache } from "./common.js";
|
||
|
|
|
||
|
|
const require = createRequire(import.meta.url);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse JS/TS source synchronously on current thread, using raw transfer to speed up deserialization.
|
||
|
|
*
|
||
|
|
* @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`
|
||
|
|
*/
|
||
|
|
export function parseSyncRaw(filename, sourceText, options) {
|
||
|
|
return parseSyncRawImpl(filename, sourceText, options, deserialize);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parse JS/TS source asynchronously, using raw transfer to speed up deserialization.
|
||
|
|
*
|
||
|
|
* Note that not all of the workload can happen on a separate thread.
|
||
|
|
* Parsing on Rust side does happen in a separate thread, but deserialization of the AST to JS objects
|
||
|
|
* has to happen on current thread. This synchronous deserialization work typically outweighs
|
||
|
|
* the asynchronous parsing by a factor of around 3.
|
||
|
|
*
|
||
|
|
* i.e. the majority of the workload cannot be parallelized by using this method.
|
||
|
|
*
|
||
|
|
* Generally `parseSyncRaw` is preferable to use as it does not have the overhead of spawning a thread.
|
||
|
|
* If you need to parallelize parsing multiple files, it is recommended to use worker threads.
|
||
|
|
*
|
||
|
|
* @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`
|
||
|
|
*/
|
||
|
|
export function parse(filename, sourceText, options) {
|
||
|
|
return parseAsyncRawImpl(filename, sourceText, options, deserialize);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deserializers are large files, so lazy-loaded.
|
||
|
|
// `deserialize` functions are stored in this array once loaded.
|
||
|
|
// Index into these arrays is `isJs * 1 + range * 2 + experimentalParent * 4`.
|
||
|
|
const deserializers = [null, null, null, null, null, null, null, null];
|
||
|
|
const deserializerNames = [
|
||
|
|
"ts",
|
||
|
|
"js",
|
||
|
|
"ts_range",
|
||
|
|
"js_range",
|
||
|
|
"ts_parent",
|
||
|
|
"js_parent",
|
||
|
|
"ts_range_parent",
|
||
|
|
"js_range_parent",
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deserialize whole AST from buffer.
|
||
|
|
*
|
||
|
|
* @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`
|
||
|
|
*/
|
||
|
|
function deserialize(buffer, sourceText, sourceStartPos, sourceByteLen, options) {
|
||
|
|
const isJs = isJsAst(buffer),
|
||
|
|
range = !!options.range,
|
||
|
|
parent = !!options.experimentalParent;
|
||
|
|
|
||
|
|
// Lazy load deserializer, and deserialize buffer to JS objects
|
||
|
|
const deserializerIndex = +isJs | (+range << 1) | (+parent << 2);
|
||
|
|
let deserializeThis = deserializers[deserializerIndex];
|
||
|
|
if (deserializeThis === null) {
|
||
|
|
deserializeThis = deserializers[deserializerIndex] = require(
|
||
|
|
`../generated/deserialize/${deserializerNames[deserializerIndex]}.js`,
|
||
|
|
).deserialize;
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = deserializeThis(buffer, sourceText, sourceStartPos, sourceByteLen);
|
||
|
|
|
||
|
|
// Add a line comment for hashbang if JS.
|
||
|
|
// Do not add comment if TS, to match `@typescript-eslint/parser`.
|
||
|
|
// See https://github.com/oxc-project/oxc/blob/ea784f5f082e4c53c98afde9bf983afd0b95e44e/napi/parser/src/lib.rs#L106-L130
|
||
|
|
if (isJs) {
|
||
|
|
const { hashbang } = data.program;
|
||
|
|
if (hashbang !== null) {
|
||
|
|
data.comments.unshift(
|
||
|
|
range
|
||
|
|
? {
|
||
|
|
type: "Line",
|
||
|
|
value: hashbang.value,
|
||
|
|
start: hashbang.start,
|
||
|
|
end: hashbang.end,
|
||
|
|
range: hashbang.range,
|
||
|
|
}
|
||
|
|
: { type: "Line", value: hashbang.value, start: hashbang.start, end: hashbang.end },
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deserialize tokens
|
||
|
|
const tokens = options.experimentalTokens ? deserializeTokens(buffer, sourceText, isJs) : null;
|
||
|
|
|
||
|
|
// Return buffer to cache, to be reused
|
||
|
|
returnBufferToCache(buffer);
|
||
|
|
|
||
|
|
// We cannot lazily deserialize in the getters, because the buffer might be re-used to parse
|
||
|
|
// another file before the getter is called
|
||
|
|
if (tokens !== null) {
|
||
|
|
return {
|
||
|
|
get program() {
|
||
|
|
return data.program;
|
||
|
|
},
|
||
|
|
get module() {
|
||
|
|
return data.module;
|
||
|
|
},
|
||
|
|
get comments() {
|
||
|
|
return data.comments;
|
||
|
|
},
|
||
|
|
get tokens() {
|
||
|
|
return tokens;
|
||
|
|
},
|
||
|
|
get errors() {
|
||
|
|
return data.errors;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
get program() {
|
||
|
|
return data.program;
|
||
|
|
},
|
||
|
|
get module() {
|
||
|
|
return data.module;
|
||
|
|
},
|
||
|
|
get comments() {
|
||
|
|
return data.comments;
|
||
|
|
},
|
||
|
|
get errors() {
|
||
|
|
return data.errors;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// `ESTreeKind` discriminants (set by Rust side)
|
||
|
|
const PRIVATE_IDENTIFIER_KIND = 2;
|
||
|
|
const REGEXP_KIND = 8;
|
||
|
|
|
||
|
|
// Indexed by `ESTreeKind` discriminant (matches `ESTreeKind` enum in `estree_kind.rs`)
|
||
|
|
const TOKEN_TYPES = [
|
||
|
|
"Identifier",
|
||
|
|
"Keyword",
|
||
|
|
"PrivateIdentifier",
|
||
|
|
"Punctuator",
|
||
|
|
"Numeric",
|
||
|
|
"String",
|
||
|
|
"Boolean",
|
||
|
|
"Null",
|
||
|
|
"RegularExpression",
|
||
|
|
"Template",
|
||
|
|
"JSXText",
|
||
|
|
"JSXIdentifier",
|
||
|
|
];
|
||
|
|
|
||
|
|
// Mask for active bits in `ESTreeKind` discriminants
|
||
|
|
const TOKEN_KIND_MASK = 15;
|
||
|
|
|
||
|
|
// Details of Rust `Token` type
|
||
|
|
const TOKEN_SIZE = 16;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deserialize tokens from buffer.
|
||
|
|
* @param {Uint8Array} buffer - Buffer containing AST in raw form
|
||
|
|
* @param {string} sourceText - Source for the file
|
||
|
|
* @param {boolean} isJs - `true` if parsing in JS mode
|
||
|
|
* @returns {Object[]} - Array of token objects
|
||
|
|
*/
|
||
|
|
function deserializeTokens(buffer, sourceText, isJs) {
|
||
|
|
const { int32 } = buffer;
|
||
|
|
|
||
|
|
let pos = int32[TOKENS_OFFSET_POS_32];
|
||
|
|
const len = int32[TOKENS_LEN_POS_32];
|
||
|
|
const endPos = pos + len * TOKEN_SIZE;
|
||
|
|
|
||
|
|
const tokens = [];
|
||
|
|
while (pos < endPos) {
|
||
|
|
tokens.push(deserializeToken(pos, int32, sourceText, isJs));
|
||
|
|
pos += TOKEN_SIZE;
|
||
|
|
}
|
||
|
|
return tokens;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deserialize a token from buffer at position `pos`.
|
||
|
|
* @param {number} pos - Position in buffer containing Rust `Token` type
|
||
|
|
* @param {Int32Array} int32 - Buffer containing AST in raw form as an `Int32Array`
|
||
|
|
* @param {string} sourceText - Source for the file
|
||
|
|
* @param {boolean} isJs - `true` if parsing in JS mode
|
||
|
|
* @returns {Object} - Token object
|
||
|
|
*/
|
||
|
|
function deserializeToken(pos, int32, sourceText, isJs) {
|
||
|
|
const pos32 = pos >> 2,
|
||
|
|
start = int32[pos32],
|
||
|
|
end = int32[pos32 + 1],
|
||
|
|
kindAndFlags = int32[pos32 + 2];
|
||
|
|
|
||
|
|
let value = sourceText.slice(start, end);
|
||
|
|
|
||
|
|
// `Kind` is byte at index 8 in `Token`.
|
||
|
|
// `Kind` has 12 variants numbered from 0 to 11.
|
||
|
|
// We have to mask the bottom byte (`& 0xFF`), so may as well mask off bits which can't be set in `Kind` at same time.
|
||
|
|
// This may allow V8 to generate more efficient code for `TOKEN_TYPES[kind]`.
|
||
|
|
const kind = kindAndFlags & TOKEN_KIND_MASK;
|
||
|
|
|
||
|
|
if (kind === REGEXP_KIND) {
|
||
|
|
const patternEnd = value.lastIndexOf("/");
|
||
|
|
return {
|
||
|
|
type: "RegularExpression",
|
||
|
|
value,
|
||
|
|
regex: {
|
||
|
|
pattern: value.slice(1, patternEnd),
|
||
|
|
flags: value.slice(patternEnd + 1),
|
||
|
|
},
|
||
|
|
start,
|
||
|
|
end,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Strip leading `#` from private identifiers
|
||
|
|
if (kind === PRIVATE_IDENTIFIER_KIND) value = value.slice(1);
|
||
|
|
|
||
|
|
// Unescape identifiers, keywords, and private identifiers in JS mode.
|
||
|
|
// `is_escaped` flag is in byte 10 of `Token`, and is a `bool`.
|
||
|
|
if (isJs && kind <= PRIVATE_IDENTIFIER_KIND && (kindAndFlags & 0x10000) !== 0) {
|
||
|
|
value = unescapeIdentifier(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { type: TOKEN_TYPES[kind], value, start, end };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Unescape an identifier.
|
||
|
|
*
|
||
|
|
* We do this on JS side, because escaped identifiers are so extremely rare that this function
|
||
|
|
* is never called in practice anyway.
|
||
|
|
*
|
||
|
|
* @param {string} name - Identifier name to unescape
|
||
|
|
* @returns {string} - Unescaped identifier name
|
||
|
|
*/
|
||
|
|
function unescapeIdentifier(name) {
|
||
|
|
return name.replace(/\\u(?:\{([0-9a-fA-F]+)\}|([0-9a-fA-F]{4}))/g, (_, hex1, hex2) =>
|
||
|
|
String.fromCodePoint(parseInt(hex1 ?? hex2, 16)),
|
||
|
|
);
|
||
|
|
}
|