Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 88 additions & 38 deletions napi/parser/test/parse-raw-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@ import {
TS_ESTREE_DIR_PATH,
} from './parse-raw-common.ts';
import { makeUnitsFromTest } from './typescript-make-units-from-test.ts';
import type { Node, ParserOptions } from '../src-js/index.js';

const { hasOwn } = Object,
{ isArray } = Array;

interface ExtendedParserOptions extends ParserOptions {
experimentalRawTransfer?: boolean;
experimentalParent?: boolean;
experimentalLazy?: boolean;
}

type TestCaseProps = string | { filename: string; sourceText: string };

// Run test case and return whether it passes.
// This is the entry point when run as a worker.
export default async function (data) {
export default async function (data: { type: number; props: TestCaseProps }): Promise<boolean> {
try {
await runCase(data, simpleExpect);
return true;
Expand All @@ -40,35 +49,43 @@ export default async function (data) {
// Run test case with specified `expect` implementation.
// If test fails, will throw an error.
// Can be called from main thread.
export async function runCase({ type, props }, expect) {
export async function runCase(
{ type, props }: { type: number; props: TestCaseProps },
expect: ExpectFunction,
): Promise<void> {
const rangeParent = (type & TEST_TYPE_RANGE_PARENT) !== 0,
lazy = (type & TEST_TYPE_LAZY) !== 0,
pretty = (type & TEST_TYPE_PRETTY) !== 0;
type &= TEST_TYPE_MAIN_MASK;

switch (type) {
case TEST_TYPE_TEST262:
await runTest262Case(props, rangeParent, lazy, expect);
await runTest262Case(props as string, rangeParent, lazy, expect);
break;
case TEST_TYPE_JSX:
await runJsxCase(props, rangeParent, lazy, expect);
await runJsxCase(props as string, rangeParent, lazy, expect);
break;
case TEST_TYPE_TS:
await runTsCase(props, rangeParent, lazy, expect);
await runTsCase(props as string, rangeParent, lazy, expect);
break;
case TEST_TYPE_FIXTURE:
await runFixture(props, rangeParent, lazy, pretty, expect);
await runFixture(props as string, rangeParent, lazy, pretty, expect);
break;
case TEST_TYPE_INLINE_FIXTURE:
await runInlineFixture(props, rangeParent, lazy, pretty, expect);
await runInlineFixture(props as { filename: string; sourceText: string }, rangeParent, lazy, pretty, expect);
break;
default:
throw new Error('Unexpected test type');
}
}

// Run Test262 test case
async function runTest262Case(path, rangeParent, lazy, expect) {
async function runTest262Case(
path: string,
rangeParent: boolean,
lazy: boolean,
expect: ExpectFunction,
): Promise<void> {
const filename = basename(path);
const [sourceText, acornJson] = await Promise.all([
readFile(pathJoin(TEST262_DIR_PATH, path), 'utf8'),
Expand All @@ -93,7 +110,12 @@ async function runTest262Case(path, rangeParent, lazy, expect) {
}

// Run JSX test case
async function runJsxCase(filename, rangeParent, lazy, expect) {
async function runJsxCase(
filename: string,
rangeParent: boolean,
lazy: boolean,
expect: ExpectFunction,
): Promise<void> {
const sourcePath = pathJoin(JSX_DIR_PATH, filename),
jsonPath = sourcePath.slice(0, -1) + 'on'; // `.jsx` -> `.json`
const [sourceText, acornJson] = await Promise.all([readFile(sourcePath, 'utf8'), readFile(jsonPath, 'utf8')]);
Expand All @@ -120,7 +142,7 @@ const TS_CASE_HEADER = '__ESTREE_TEST__:PASS:\n```json\n';
const TS_CASE_FOOTER = '\n```\n';
const TS_CASE_FOOTER_LEN = TS_CASE_FOOTER.length;

async function runTsCase(path, rangeParent, lazy, expect) {
async function runTsCase(path: string, rangeParent: boolean, lazy: boolean, expect: ExpectFunction): Promise<void> {
const tsPath = path.slice(0, -3); // Trim off `.md`
let [sourceText, casesJson] = await Promise.all([
readFile(pathJoin(TS_DIR_PATH, tsPath), 'utf8'),
Expand All @@ -140,7 +162,7 @@ async function runTsCase(path, rangeParent, lazy, expect) {
for (let i = 0; i < tests.length; i++) {
const { name: filename, content: code, sourceType } = tests[i];

const options = {
const options: ExtendedParserOptions = {
sourceType: sourceType.module ? 'module' : 'unambiguous',
astType: 'ts',
preserveParens: false,
Expand All @@ -156,8 +178,7 @@ async function runTsCase(path, rangeParent, lazy, expect) {
continue;
}

// @ts-ignore
const { program, errors } = parseSync(filename, code, options);
const { program, errors } = parseSync(filename, code, options as ParserOptions);
const oxcJson = stringifyAcornTest262Style(program);

const estreeJson = estreeJsons[i];
Expand All @@ -168,8 +189,7 @@ async function runTsCase(path, rangeParent, lazy, expect) {
// Fall back to comparing to AST parsed via JSON transfer.
// We can fail to match the TS-ESLint snapshots where there are syntax errors,
// because our parser is not recoverable.
// @ts-ignore
const standard = parseSync(filename, code, { ...options, experimentalRawTransfer: false });
const standard = parseSync(filename, code, { ...options, experimentalRawTransfer: false } as ParserOptions);
const standardJson = stringifyAcornTest262Style(standard.program);
const errorsStandard = standard.errors;

Expand All @@ -183,7 +203,13 @@ async function runTsCase(path, rangeParent, lazy, expect) {
}

// Test raw transfer output matches standard (via JSON) output for a fixture file
async function runFixture(path, rangeParent, lazy, pretty, expect) {
async function runFixture(
path: string,
rangeParent: boolean,
lazy: boolean,
pretty: boolean,
expect: ExpectFunction,
): Promise<void> {
const filename = basename(path);
const sourceText = await readFile(pathJoin(ROOT_DIR_PATH, path), 'utf8');

Expand All @@ -197,7 +223,13 @@ async function runFixture(path, rangeParent, lazy, pretty, expect) {
}

// Test raw transfer output matches standard (via JSON) output for a fixture, with provided source text
async function runInlineFixture({ filename, sourceText }, rangeParent, lazy, pretty, expect) {
async function runInlineFixture(
{ filename, sourceText }: { filename: string; sourceText: string },
rangeParent: boolean,
lazy: boolean,
pretty: boolean,
expect: ExpectFunction,
): Promise<void> {
if (rangeParent) {
testRangeParent(filename, sourceText, null, expect);
} else if (lazy) {
Expand All @@ -208,17 +240,21 @@ async function runInlineFixture({ filename, sourceText }, rangeParent, lazy, pre
}

// Test `range` and `parent` fields are correct on all AST nodes.
function testRangeParent(filename, sourceText, options, expect) {
// @ts-ignore
function testRangeParent(
filename: string,
sourceText: string,
options: ExtendedParserOptions | null,
expect: ExpectFunction,
): void {
const ret = parseSync(filename, sourceText, {
...options,
range: true,
experimentalRawTransfer: true,
experimentalParent: true,
});
} as ParserOptions);

let parent = null;
function walk(node) {
let parent: any = null;
function walk(node: null | Node[] | Node): void {
if (node === null || typeof node !== 'object') return;

if (isArray(node)) {
Expand Down Expand Up @@ -260,21 +296,25 @@ function testRangeParent(filename, sourceText, options, expect) {

// Test lazy deserialization does not throw an error.
// We don't test the correctness of the output.
function testLazy(filename, sourceText, options) {
// @ts-ignore
function testLazy(filename: string, sourceText: string, options: ExtendedParserOptions | null): void {
const ret = parseSync(filename, sourceText, {
...options,
experimentalRawTransfer: false,
experimentalLazy: true,
});
} as ParserOptions);
JSON.stringify(ret.program);
JSON.stringify(ret.comments);
JSON.stringify(ret.errors);
JSON.stringify(ret.module);
}

// Assert raw transfer output matches standard (via JSON) output
function assertRawAndStandardMatch(filename, sourceText, pretty, expect) {
function assertRawAndStandardMatch(
filename: string,
sourceText: string,
pretty: boolean,
expect: ExpectFunction,
): void {
const retStandard = parseSync(filename, sourceText);
const {
program: programStandard,
Expand All @@ -290,8 +330,7 @@ function assertRawAndStandardMatch(filename, sourceText, pretty, expect) {
moveStartAndEndToLast(moduleStandard.staticExports, true);
moveStartAndEndToLast(moduleStandard.dynamicImports, false);

// @ts-ignore
const retRaw = parseSync(filename, sourceText, { experimentalRawTransfer: true });
const retRaw = parseSync(filename, sourceText, { experimentalRawTransfer: true } as ParserOptions);
const { program: programRaw, comments: commentsRaw } = retRaw;
// Remove `null` values, to match what NAPI-RS does
const moduleRaw = removeNullProperties(retRaw.module);
Expand All @@ -309,14 +348,17 @@ function assertRawAndStandardMatch(filename, sourceText, pretty, expect) {
expect(jsonRaw).toEqual(jsonStandard);
}

function moveStartAndEndToLast(arr, reorderEntries) {
function moveStartAndEndToLast<T extends { entries?: any[]; start: number; end: number }>(
arr: T[],
reorderEntries: boolean,
): void {
for (const obj of arr) {
const { start, end } = obj;
delete obj.start;
delete obj.end;
obj.start = start;
obj.end = end;
if (reorderEntries) moveStartAndEndToLast(obj.entries, false);
if (reorderEntries && obj.entries) moveStartAndEndToLast(obj.entries, false);
}
}

Expand All @@ -329,14 +371,14 @@ function moveStartAndEndToLast(arr, reorderEntries) {
// }
// ```
// For speed, extract `sourceType` with a slice, rather than parsing the JSON.
function getSourceTypeFromJSON(json) {
function getSourceTypeFromJSON(json: string): 'script' | 'module' {
const index = json.lastIndexOf('"sourceType": "');
return json.slice(index + 15, index + 21);
return json.slice(index + 15, index + 21) as 'script' | 'module';
}

// Stringify to JSON, replacing values which are invalid in JSON.
// If `pretty === true`, JSON is pretty-printed.
function stringify(obj, pretty) {
function stringify(obj: any, pretty: boolean): string {
return JSON.stringify(
obj,
(_key, value) => {
Expand All @@ -354,7 +396,7 @@ function stringify(obj, pretty) {
const INFINITY_PLACEHOLDER = '__INFINITY__INFINITY__INFINITY__';
const INFINITY_REGEXP = new RegExp(`"${INFINITY_PLACEHOLDER}"`, 'g');

function stringifyAcornTest262Style(obj) {
function stringifyAcornTest262Style(obj: any): string {
let containsInfinity = false;
const json = JSON.stringify(
obj,
Expand All @@ -373,16 +415,24 @@ function stringifyAcornTest262Style(obj) {
}

// Remove `null` values, to match what NAPI-RS does
function removeNullProperties(obj) {
function removeNullProperties(obj: any): any {
return JSON.parse(JSON.stringify(obj, (_key, value) => (value === null ? undefined : value)));
}

// Type for expect function
interface ExpectFunction {
(value: any): {
toEqual: (expected: any) => void;
toBe: (expected: any) => void;
};
}

// Very simple `expect` implementation.
// Only supports `expect(x).toEqual(y)` and `expect(x).toBe(y)`, and both use only a simple `===` comparison.
// Therefore, only works for primitive values e.g. strings.
function simpleExpect(value) {
const toBe = (expected) => {
const simpleExpect: ExpectFunction = (value: any) => {
const toBe = (expected: any): void => {
if (value !== expected) throw new Error('Mismatch');
};
return { toEqual: toBe, toBe };
}
};
Loading