Merge pull request #695 from kaoru0822-kitauji/fix/reference-parser-recovery

fix(reference): retain recovered parser diagnostics
This commit is contained in:
Kaoru0822 authored and GitHub committed 2026-10-06 01:09:40 -07:00
commit 11d0ef2e09
3 files changed
+312 -16

No files matched your search

+19 -16
View File
@@ -291,8 +291,7 @@ const parseWithBabel = (
language: string | null,
): {
ast: File | undefined;
parseState: ReferenceSourceImportParseState;
reason?: string;
reasons: readonly string[];
} => {
const plugins: ParserPlugin[] = ["jsx"];
if (language === "TypeScript" || language === "TSX") {
@@ -316,12 +315,11 @@ const parseWithBabel = (
errorRecovery: true,
plugins,
});
return { ast, parseState: "parsed" };
return { ast, reasons: ast.errors.map((error) => error.message) };
} catch (cause: unknown) {
return {
ast: undefined,
parseState: "unknown",
reason: cause instanceof Error ? cause.message : "Parse failed",
reasons: [cause instanceof Error ? cause.message : "Parse failed"],
};
}
};
@@ -336,18 +334,17 @@ export const parseReferenceSourceImports = (
return { relationships: [], parse_failures: [] };
const source = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
const { ast, parseState, reason } = parseWithBabel(path, source, language);
const { ast, reasons } = parseWithBabel(path, source, language);
const parseFailures = reasons.map((reason) => ({
path,
parser: "babel",
reason,
}));
if (ast === undefined || parseState === "unknown") {
if (ast === undefined) {
return {
relationships: [],
parse_failures: [
{
path,
parser: "babel",
reason: reason ?? "Unknown parse failure",
},
],
parse_failures: parseFailures,
};
}
@@ -356,7 +353,13 @@ export const parseReferenceSourceImports = (
extractRequireAndDynamicImports(ast.program.body, path, relationships);
return {
relationships,
parse_failures: [],
relationships: relationships.map((relationship) => ({
...relationship,
parse_state:
parseFailures.length > 0 && relationship.parse_state === "parsed"
? "partial"
: relationship.parse_state,
})),
parse_failures: parseFailures,
};
};
@@ -0,0 +1,150 @@
import { describe, expect, it } from "vitest";
import { parseReferenceSourceImports } from "./referenceSourceImportParsing.js";
const recoverySource = [
"const missing;",
"let duplicate;",
"let duplicate;",
"let duplicate;",
].join("\n");
const recoveryReasons = [
"Missing initializer in const declaration. (1:13)",
"Identifier 'duplicate' has already been declared. (3:4)",
"Identifier 'duplicate' has already been declared. (4:4)",
];
const moduleSource = [
'import "./static.js";',
'export { value } from "./named.js";',
'export * from "./all.js";',
'require("./common.cjs");',
'require.resolve("node:fs");',
'import("./lazy.js");',
"import(moduleName);",
].join("\n");
const moduleRelationships = [
["./static.js", "imports", "internal", "parsed"],
["./named.js", "imports", "internal", "parsed"],
["./all.js", "imports", "internal", "parsed"],
["./common.cjs", "requires", "internal", "parsed"],
["node:fs", "requires", "external", "parsed"],
["./lazy.js", "imports", "internal", "parsed"],
["<dynamic-import>", "imports", "unknown", "partial"],
].map(([to, kind, resolution, parse_state]) => ({
to,
kind,
resolution,
parse_state,
}));
const parseSource = (path: string, source: string, language: string | null) =>
parseReferenceSourceImports(path, new TextEncoder().encode(source), language);
describe.each([
["main.js", "JavaScript"],
["main.jsx", "JSX"],
["main.ts", "TypeScript"],
["main.tsx", "TSX"],
])("recovered reference imports in %s", (path, language) => {
it("retains every diagnostic and marks recovered relationships partial", () => {
const result = parseSource(
path,
`${recoverySource}\n${moduleSource}`,
language,
);
expect(result.parse_failures).toEqual(
recoveryReasons.map((reason) => ({ path, parser: "babel", reason })),
);
expect(result.relationships).toEqual(
moduleRelationships.map((relationship) => ({
...relationship,
from_path: path,
parse_state: "partial",
})),
);
});
it("preserves valid and computed relationships without diagnostics", () => {
expect(parseSource(path, moduleSource, language)).toEqual({
relationships: moduleRelationships.map((relationship) => ({
...relationship,
from_path: path,
})),
parse_failures: [],
});
});
it("reports recovery diagnostics when there are no relationships", () => {
expect(parseSource(path, recoverySource, language)).toEqual({
relationships: [],
parse_failures: recoveryReasons.map((reason) => ({
path,
parser: "babel",
reason,
})),
});
});
it("returns no relationships when parsing throws", () => {
expect(
parseSource(path, 'import "./before.js";\nconst = ;', language),
).toEqual({
relationships: [],
parse_failures: [
{ path, parser: "babel", reason: "Unexpected token (2:6)" },
],
});
});
});
it.each(["main.ts", "types.d.ts", "types.d.mts", "types.d.cts"])(
"marks recovered TypeScript declarations partial in %s",
(path) => {
const result = parseSource(
path,
[
"let duplicate: number;",
"let duplicate: number;",
'import dep = require("node:path");',
'declare module "ambient-package" {}',
].join("\n"),
"TypeScript",
);
expect(result).toEqual({
relationships: [
{
from_path: path,
to: "node:path",
kind: "requires",
resolution: "external",
parse_state: "partial",
},
{
from_path: path,
to: "ambient-package",
kind: "declares-module",
resolution: "unknown",
parse_state: "partial",
},
],
parse_failures: [
{
path,
parser: "babel",
reason: "Identifier 'duplicate' has already been declared. (2:4)",
},
],
});
},
);
it("keeps unsupported source languages outside Babel parsing", () => {
expect(parseSource("main.py", recoverySource, "Python")).toEqual({
relationships: [],
parse_failures: [],
});
});