Files
Boshen 030857b509 refactor(diagnostics): update oxc-miette to v4 (#25395)
Remove the miette `Report` wrapper and update oxc-miette to v4's narrowed diagnostic and source protocol.

AI-assisted by OpenAI Codex.
2026-08-10 07:48:02 +00:00

923 lines
26 KiB
TypeScript
Raw Permalink Blame History

import { Worker } from "node:worker_threads";
import { describe, expect, it, test } from "vitest";
import { parse, parseSync } from "../src-js/index.js";
import { parseSync as parseRawSync } from "./parser.ts";
import type {
ExpressionStatement,
ParserOptions,
Program,
TSTypeAliasDeclaration,
VariableDeclaration,
} from "../src-js/index.js";
describe("parse", () => {
const code = "/* comment */ foo";
it("uses the `lang` option", () => {
const ret = parseSync("test.vue", code, { lang: "ts" });
expect(ret.program.body.length).toBe(1);
expect(ret.errors.length).toBe(0);
});
it("matches output", async () => {
const ret = await parse("test.js", code);
expect(ret.program.body.length).toBe(1);
expect(ret.errors.length).toBe(0);
expect(ret.comments.length).toBe(1);
const comment = ret.comments[0];
expect(comment).toEqual({
type: "Block",
start: 0,
end: 13,
value: " comment ",
});
expect(code.substring(comment.start, comment.end)).toBe("/*" + comment.value + "*/");
});
it("checks semantic", async () => {
const code = "let x; let x;";
let ret = await parse("test.js", code);
expect(ret.errors.length).toBe(0);
ret = await parse("test.js", code, {
showSemanticErrors: true,
});
expect(ret.errors.length).toBe(1);
});
describe("sets lang and sourceType", () => {
const langs: ParserOptions["lang"][] = ["js", "ts", "jsx", "tsx"];
test.each(langs)("%s", (lang) => {
const code = "await(1)";
const ret = parseSync("test.cjs", code, { lang, sourceType: "script" });
expect(ret.errors.length).toBe(0);
expect(ret.program.sourceType).toBe("script");
// Parsed as `await(1)`
expect((ret.program.body[0] as ExpressionStatement).expression.type).toBe("CallExpression");
});
test.each(langs)("%s", (lang) => {
const code = "await(1)";
const ret = parseSync("test.cjs", code, { lang, sourceType: "module" });
expect(ret.errors.length).toBe(0);
expect(ret.program.sourceType).toBe("module");
// Parsed as `await 1`
expect((ret.program.body[0] as ExpressionStatement).expression.type).toBe("AwaitExpression");
});
test.each(langs)("%s", (lang) => {
const code = "return 123;";
const ret = parseSync("test.cjs", code, { lang, sourceType: "commonjs" });
expect(ret.errors.length).toBe(0);
expect(ret.program.sourceType).toBe("commonjs");
expect(ret.program.body[0].type).toBe("ReturnStatement");
});
test("sets lang as dts", () => {
const code = "declare const foo";
const ret = parseSync("test", code, { lang: "dts" });
expect(ret.errors.length).toBe(0);
expect((ret.program.body[0] as VariableDeclaration).declare).toBe(true);
});
});
describe("TS properties", () => {
const code = "let x;";
const withTsFields: Program = {
type: "Program",
start: 0,
end: 6,
body: [
{
type: "VariableDeclaration",
start: 0,
end: 6,
declarations: [
{
type: "VariableDeclarator",
start: 4,
end: 5,
id: {
type: "Identifier",
start: 4,
end: 5,
name: "x",
decorators: [],
typeAnnotation: null,
optional: false,
},
init: null,
definite: false,
},
],
kind: "let",
declare: false,
},
],
sourceType: "script",
hashbang: null,
};
const withoutTsFields: Program = {
type: "Program",
start: 0,
end: 6,
body: [
{
type: "VariableDeclaration",
start: 0,
end: 6,
declarations: [
{
type: "VariableDeclarator",
start: 4,
end: 5,
id: {
type: "Identifier",
start: 4,
end: 5,
name: "x",
},
init: null,
},
],
kind: "let",
},
],
sourceType: "script",
hashbang: null,
};
describe("included when", () => {
it("filename has `.ts` extension", () => {
const { program } = parseSync("test.ts", code);
expect(program).toEqual(withTsFields);
});
it('`lang` option is "ts"', () => {
const { program } = parseSync("test.js", code, { lang: "ts" });
expect(program).toEqual(withTsFields);
});
it('`astType` option is "ts"', () => {
const { program } = parseSync("test.js", code, { lang: "js", astType: "ts" });
expect(program).toEqual(withTsFields);
});
});
describe("not included when", () => {
it("filename has `.js` extension", () => {
const { program } = parseSync("test.js", code);
expect(program).toEqual(withoutTsFields);
});
it('`lang` option is "js"', () => {
const { program } = parseSync("test.ts", code, { lang: "js" });
expect(program).toEqual(withoutTsFields);
});
it('`astType` option is "js"', () => {
const { program } = parseSync("test.ts", code, { lang: "ts", astType: "js" });
expect(program).toEqual(withoutTsFields);
});
});
});
it("`Infinity` is represented as `Infinity` number", () => {
const ret = parseSync("test.js", "1e+350");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 6,
expression: {
type: "Literal",
start: 0,
end: 6,
value: Infinity,
raw: "1e+350",
},
});
});
it("`BigIntLiteral` has `value` as `BigInt`", () => {
const ret = parseSync("test.js", "123_456n");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 8,
expression: {
type: "Literal",
start: 0,
end: 8,
value: 123456n,
raw: "123_456n",
bigint: "123456",
},
});
});
describe("`StringLiteral`", () => {
it("lone surrogates", () => {
const ret = parseSync("test.js", ';"\\uD800\\uDBFF";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(2);
expect(ret.program.body[1]).toEqual({
type: "ExpressionStatement",
start: 1,
end: 16,
expression: {
type: "Literal",
start: 1,
end: 15,
value: "\ud800\udbff",
raw: '"\\uD800\\uDBFF"',
},
});
});
it("lossy replacement character", () => {
const ret = parseSync("test.js", ';"<22>\\u{FFFD}";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(2);
expect(ret.program.body[1]).toEqual({
type: "ExpressionStatement",
start: 1,
end: 13,
expression: {
type: "Literal",
start: 1,
end: 12,
value: "<22><>",
raw: '"<22>\\u{FFFD}"',
},
});
});
it("lone surrogates and lossy replacement characters", () => {
const ret = parseSync("test.js", ';"<22>\\u{FFFD}\\uD800\\uDBFF<46>\\u{FFFD}";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(2);
expect(ret.program.body[1]).toEqual({
type: "ExpressionStatement",
start: 1,
end: 34,
expression: {
type: "Literal",
start: 1,
end: 33,
value: "<22><>\ud800\udbff<66><66>",
raw: '"<22>\\u{FFFD}\\uD800\\uDBFF<46>\\u{FFFD}"',
},
});
});
});
describe("`RegExpLiteral`", () => {
it("has `value` as `RegExp` when valid regexp", () => {
const ret = parseSync("test.js", "/abc/gu");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 7,
expression: {
type: "Literal",
start: 0,
end: 7,
value: /abc/gu,
raw: "/abc/gu",
regex: {
pattern: "abc",
flags: "gu",
},
},
});
});
it("has `value` as `null` when invalid regexp", () => {
const ret = parseSync("test.js", "/+/");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 3,
expression: {
type: "Literal",
start: 0,
end: 3,
value: null,
raw: "/+/",
regex: {
pattern: "+",
flags: "",
},
},
});
});
});
describe("`TemplateLiteral`", () => {
it("lone surrogates", () => {
const ret = parseSync("test.js", "`\\uD800\\uDBFF${x}\\uD800\\uDBFF`;");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 31,
expression: {
type: "TemplateLiteral",
start: 0,
end: 30,
expressions: [
{
type: "Identifier",
start: 15,
end: 16,
name: "x",
},
],
quasis: [
{
type: "TemplateElement",
start: 1,
end: 13,
value: {
raw: "\\uD800\\uDBFF",
cooked: "\ud800\udbff",
},
tail: false,
},
{
type: "TemplateElement",
start: 17,
end: 29,
value: {
raw: "\\uD800\\uDBFF",
cooked: "\ud800\udbff",
},
tail: true,
},
],
},
});
});
it("lossy replacement character", () => {
const ret = parseSync("test.js", "`<60>\\u{FFFD}${x}<7D>\\u{FFFD}`;");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 25,
expression: {
type: "TemplateLiteral",
start: 0,
end: 24,
expressions: [
{
type: "Identifier",
start: 12,
end: 13,
name: "x",
},
],
quasis: [
{
type: "TemplateElement",
start: 1,
end: 10,
value: {
raw: "<22>\\u{FFFD}",
cooked: "<22><>",
},
tail: false,
},
{
type: "TemplateElement",
start: 14,
end: 23,
value: {
raw: "<22>\\u{FFFD}",
cooked: "<22><>",
},
tail: true,
},
],
},
});
});
it("lone surrogates and lossy replacement characters", () => {
const ret = parseSync("test.js", "`<60>\\u{FFFD}\\uD800${x}\\uDBFF<46>\\u{FFFD}`;");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ExpressionStatement",
start: 0,
end: 37,
expression: {
type: "TemplateLiteral",
start: 0,
end: 36,
expressions: [
{
type: "Identifier",
start: 18,
end: 19,
name: "x",
},
],
quasis: [
{
type: "TemplateElement",
start: 1,
end: 16,
value: {
raw: "<22>\\u{FFFD}\\uD800",
cooked: "<22><>\ud800",
},
tail: false,
},
{
type: "TemplateElement",
start: 20,
end: 35,
value: {
raw: "\\uDBFF<46>\\u{FFFD}",
cooked: "\udbff<66><66>",
},
tail: true,
},
],
},
});
});
});
describe("`ImportDeclaration`", () => {
describe("import defer", () => {
it("ESTree", () => {
const ret = parseSync("test.js", 'import defer * as ns from "x";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ImportDeclaration",
start: 0,
end: 30,
specifiers: [
{
type: "ImportNamespaceSpecifier",
start: 13,
end: 20,
local: { type: "Identifier", start: 18, end: 20, name: "ns" },
},
],
source: { type: "Literal", start: 26, end: 29, value: "x", raw: '"x"' },
attributes: [],
phase: "defer",
});
});
it("TS-ESTree", () => {
const ret = parseSync("test.ts", 'import defer * as ns from "x";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ImportDeclaration",
start: 0,
end: 30,
specifiers: [
{
type: "ImportNamespaceSpecifier",
start: 13,
end: 20,
local: {
type: "Identifier",
start: 18,
end: 20,
decorators: [],
name: "ns",
optional: false,
typeAnnotation: null,
},
},
],
source: { type: "Literal", start: 26, end: 29, value: "x", raw: '"x"' },
attributes: [],
phase: "defer",
importKind: "value",
});
});
});
describe("import source", () => {
it("ESTree", () => {
const ret = parseSync("test.js", 'import source src from "x";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ImportDeclaration",
start: 0,
end: 27,
specifiers: [
{
type: "ImportDefaultSpecifier",
start: 14,
end: 17,
local: { type: "Identifier", start: 14, end: 17, name: "src" },
},
],
source: { type: "Literal", start: 23, end: 26, value: "x", raw: '"x"' },
attributes: [],
phase: "source",
});
});
it("TS-ESTree", () => {
const ret = parseSync("test.ts", 'import source src from "x";');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.body[0]).toEqual({
type: "ImportDeclaration",
start: 0,
end: 27,
specifiers: [
{
type: "ImportDefaultSpecifier",
start: 14,
end: 17,
local: {
type: "Identifier",
start: 14,
end: 17,
decorators: [],
name: "src",
optional: false,
typeAnnotation: null,
},
},
],
source: { type: "Literal", start: 23, end: 26, value: "x", raw: '"x"' },
attributes: [],
phase: "source",
importKind: "value",
});
});
});
});
describe("`ImportExpression`", () => {
describe("import.defer()", () => {
it("ESTree", () => {
const ret = parseSync("test.js", 'import.defer("x");');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
// @ts-expect-error - ignore
expect(ret.program.body[0].expression).toEqual({
type: "ImportExpression",
start: 0,
end: 17,
source: { type: "Literal", start: 13, end: 16, value: "x", raw: '"x"' },
options: null,
phase: "defer",
});
});
// This does *not* align with TS-ESLint.
// See https://github.com/oxc-project/oxc/pull/11193.
it("TS-ESTree", () => {
const ret = parseSync("test.ts", 'import.defer("x");');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
// @ts-expect-error - ignore
expect(ret.program.body[0].expression).toEqual({
type: "ImportExpression",
start: 0,
end: 17,
source: { type: "Literal", start: 13, end: 16, value: "x", raw: '"x"' },
options: null,
phase: "defer",
});
});
});
describe("import.source()", () => {
it("ESTree", () => {
const ret = parseSync("test.js", 'import.source("x");');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
// @ts-expect-error - ignore
expect(ret.program.body[0].expression).toEqual({
type: "ImportExpression",
start: 0,
end: 18,
source: { type: "Literal", start: 14, end: 17, value: "x", raw: '"x"' },
options: null,
phase: "source",
});
});
// This does *not* align with TS-ESLint.
// See https://github.com/oxc-project/oxc/pull/11193.
it("TS-ESTree", () => {
const ret = parseSync("test.ts", 'import.source("x");');
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
// @ts-expect-error - ignore
expect(ret.program.body[0].expression).toEqual({
type: "ImportExpression",
start: 0,
end: 18,
source: { type: "Literal", start: 14, end: 17, value: "x", raw: '"x"' },
options: null,
phase: "source",
});
});
});
});
describe("hashbang", () => {
it("is `null` when no hashbang", () => {
const ret = parseSync("test.js", "let x;");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.hashbang).toBeNull();
expect(ret.comments).toHaveLength(0);
});
it("is defined when hashbang", () => {
const ret = parseSync("test.js", "#!/usr/bin/env node\nlet x;");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.program.hashbang).toEqual({
type: "Hashbang",
start: 0,
end: 19,
value: "/usr/bin/env node",
});
});
it("is also recorded as a line comment in JS files", () => {
const ret = parseSync("test.js", "#!/usr/bin/env node\nlet x; // foo");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.comments).toHaveLength(2);
expect(ret.comments[0]).toEqual({
type: "Line",
start: 0,
end: 19,
value: "/usr/bin/env node",
});
});
it("is not recorded as a line comment in TS files", () => {
const ret = parseSync("test.ts", "#!/usr/bin/env node\nlet x; // foo");
expect(ret.errors.length).toBe(0);
expect(ret.program.body.length).toBe(1);
expect(ret.comments).toHaveLength(1);
expect(ret.comments[0].value).toBe(" foo");
});
});
describe("preserveParens", () => {
it("should include parens when true", () => {
let ret = parseSync("test.js", "(x)");
expect((ret.program.body[0] as ExpressionStatement).expression.type).toBe(
"ParenthesizedExpression",
);
ret = parseSync("test.ts", "type Foo = (x)");
expect((ret.program.body[0] as TSTypeAliasDeclaration).typeAnnotation.type).toBe(
"TSParenthesizedType",
);
});
it("should omit parens when false", () => {
const options = { preserveParens: false };
let ret = parseSync("test.js", "(x)", options);
expect((ret.program.body[0] as ExpressionStatement).expression.type).toBe("Identifier");
ret = parseSync("test.ts", "type Foo = (x)", options);
expect((ret.program.body[0] as TSTypeAliasDeclaration).typeAnnotation.type).toBe(
"TSTypeReference",
);
});
});
describe("ranges", () => {
it("should include range when true", () => {
const ret = parseSync("test.js", "(x)", { range: true });
expect(ret.program.body[0].start).toBe(0);
expect(ret.program.body[0].range).toEqual([0, 3]);
});
it("should not include range when false", () => {
const ret = parseSync("test.js", "(x)", { range: false });
expect(ret.program.body[0].range).toBeUndefined();
});
it("should not include range by default", () => {
const ret = parseSync("test.js", "(x)");
expect(ret.program.body[0].range).toBeUndefined();
});
});
});
describe("UTF-16 span", () => {
it("basic", async () => {
const code = "'🤨'";
const utf16 = await parse("test.js", code);
expect(utf16.program.end).toMatchInlineSnapshot(`4`);
});
it("comment", async () => {
const ret = await parse("test.js", `// ∞`);
expect(ret.comments).toMatchInlineSnapshot(`
[
{
"end": 4,
"start": 0,
"type": "Line",
"value": " ∞",
},
]
`);
});
it("module record", async () => {
const ret = await parse(
"test.js",
`"🤨";import x from "x"; export { x };import("y");import.meta.z`,
);
expect(ret.module).toMatchInlineSnapshot(`
{
"dynamicImports": [
{
"end": 48,
"moduleRequest": {
"end": 47,
"start": 44,
},
"start": 37,
},
],
"hasModuleSyntax": true,
"importMetas": [
{
"end": 60,
"start": 49,
},
],
"staticExports": [
{
"end": 23,
"entries": [
{
"end": 34,
"exportName": {
"end": 34,
"kind": "Name",
"name": "x",
"start": 33,
},
"importName": {
"end": 13,
"kind": "Name",
"name": "x",
"start": 12,
},
"isType": false,
"localName": {
"end": null,
"kind": "None",
"name": null,
"start": null,
},
"moduleRequest": {
"end": 22,
"start": 19,
"value": "x",
},
"start": 33,
},
],
"start": 5,
},
],
"staticImports": [
{
"end": 23,
"entries": [
{
"importName": {
"end": 13,
"kind": "Default",
"name": null,
"start": 12,
},
"isType": false,
"localName": {
"end": 13,
"start": 12,
"value": "x",
},
},
],
"moduleRequest": {
"end": 22,
"start": 19,
"value": "x",
},
"start": 5,
},
],
}
`);
});
it("error", async () => {
const ret = await parse("test.js", `"🤨";asdf asdf`);
expect(ret.errors).toMatchInlineSnapshot(`
[
{
"codeframe": "
x Expected a semicolon or an implicit semicolon after a statement, but found
| none
,-[test.js:1:12]
1 | "🤨";asdf asdf
: ^
\`----
help: Try inserting a semicolon here
",
"helpMessage": "Try inserting a semicolon here",
"labels": [
{
"end": 9,
"message": null,
"start": 9,
},
],
"message": "Expected a semicolon or an implicit semicolon after a statement, but found none",
"severity": "Error",
},
]
`);
});
});
describe("error", () => {
const code = "asdf asdf";
it("returns structured error", () => {
const ret = parseSync("test.js", code);
expect(ret.errors.length).toBe(1);
delete ret.errors[0].codeframe;
expect(ret.errors[0]).toStrictEqual({
helpMessage: "Try inserting a semicolon here",
labels: [
{
message: null,
start: 4,
end: 4,
},
],
message: "Expected a semicolon or an implicit semicolon after a statement, but found none",
severity: "Error",
});
});
it("renders raw-transfer diagnostics", () => {
const standard = parseSync("test.js", code);
const raw = parseRawSync("test.js", code, { experimentalRawTransfer: true });
expect(raw.errors[0].codeframe).toBe(standard.errors[0].codeframe);
});
});
describe("worker", () => {
it("should run", async () => {
const code = await new Promise((resolve, reject) => {
const worker = new Worker("./test/worker.js");
worker.on("error", (err) => {
reject(err);
});
worker.on("exit", (code) => {
resolve(code);
});
});
expect(code).toBe(0);
});
});