Files
reverse-engineering-anything/src/domain/javascript/javascriptSourceParser.ts
T
Kaoru0822andClaude Opus 5.5 44b197f924 fix(javascript): parse JavaScript sources with JavaScript grammar (#3782)
parserPluginsForPath enabled Babel's typescript plugin for .js, .jsx,
.mjs and .cjs sources. TypeScript reads some valid JavaScript
differently: a conditional whose consequent is an arrow with a
parenthesized body (c?x=>(a):y=>(b)) throws as a return-type
annotation, and a<b>(c) becomes a generic call. VS Code's 20 MB
workbench bundles therefore returned no AST at all.

Select JavaScript grammar (JSX plus legacy decorators) for explicit
JavaScript extensions. TypeScript dialects and unnamed sources keep the
existing TypeScript-capable contract.

Fixes #3780
Refs #3670

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:12:30 -07:00

148 lines
5.5 KiB
TypeScript

import { parse, type ParserOptions, type ParserPlugin } from "@babel/parser";
/** Babel AST produced by REA's inert JavaScript parser boundary. */
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
/**
* Babel plugins for one source path. Single owner for TypeScript/JSX
* mode selection (tsc + Babel `typescript` plugin semantics).
*
* - `dts:true` only for `.d.ts/.d.mts/.d.cts` ambient declarations;
* ordinary `.ts` keeps the missing-initializer diagnostic.
* - `.js/.jsx/.mjs/.cjs` use JavaScript grammar with JSX. TypeScript reads
* some valid JavaScript differently (`c?x=>(a):y=>(b)` as a return type,
* `a<b>(c)` as a generic call), so it must not select their meaning.
* - JSX enabled for `.tsx` and unknown paths; disabled for plain
* `.ts/.cts/.mts` (angle-bracket assertions, not JSX) and for all
* declaration files. `.mts` (including `.d.mts`) sets
* `disallowAmbiguousJSXLike`, matching tsc's `.mts` mode.
* - `decorators-legacy` always present: without it decorated sources fail
* hard instead of reporting recovered syntax, losing every derived fact.
* - Unknown paths retain the existing JSX- and TypeScript-capable contract
* (best-effort parse; failures surface via `errorRecovery`).
*/
export const parserPluginsForPath = (sourcePath?: string): ParserPlugin[] => {
const path = sourcePath ?? "";
if (/\.(?:js|jsx|mjs|cjs)$/iu.test(path)) return ["decorators-legacy", "jsx"];
const dts = /\.d\.(?:ts|mts|cts)$/iu.test(path);
const isTsLike = /\.(?:ts|cts|mts)$/iu.test(path);
const isTsx = /\.tsx$/iu.test(path);
const isMts = /\.mts$/iu.test(path);
const typescript: ParserPlugin = [
"typescript",
{
dts,
...(isMts ? { disallowAmbiguousJSXLike: true } : {}),
},
];
const jsxCapable = !dts && (isTsx || !isTsLike);
return jsxCapable
? ["decorators-legacy", "jsx", typescript]
: ["decorators-legacy", typescript];
};
/** `.mjs` and `.mts` explicitly select module mode. */
export const isExplicitModuleSourcePath = (path?: string): boolean =>
/\.m[jt]s$/iu.test(path ?? "");
const runtimeSourceTypeForPath = (
path?: string,
): "commonjs" | "module" | "unambiguous" =>
/\.cjs$/iu.test(path ?? "")
? "commonjs"
: isExplicitModuleSourcePath(path)
? "module"
: "unambiguous";
export interface RecoveredJavaScriptParse {
readonly file: ParsedJavaScriptSource;
/** Errors that apply only if a source of unknown mode is an ES module. */
readonly moduleOnlyErrors: readonly string[];
}
/**
* Parse a module or script with recovery. Babel tries module mode first and
* keeps its strict-mode errors after falling back to a script, so a script
* with errors is parsed again as a script. Strict errors that only the module
* parse reports stay visible because the source may still be a module.
*/
export const parseRecoveredJavaScript = (
source: string,
sourcePath: string | undefined,
options: Omit<ParserOptions, "sourceType" | "errorRecovery">,
): RecoveredJavaScriptParse => {
const sourceType = runtimeSourceTypeForPath(sourcePath);
if (sourceType !== "unambiguous") {
const normalizedOptions = { ...options };
if (sourceType === "commonjs") {
// Babel owns these wrapper semantics in CommonJS mode and rejects
// explicit script overrides, including the reference-import options.
delete normalizedOptions.allowAwaitOutsideFunction;
delete normalizedOptions.allowReturnOutsideFunction;
delete normalizedOptions.allowNewTargetOutsideFunction;
}
return {
file: parse(source, {
...normalizedOptions,
sourceType,
errorRecovery: true,
}),
moduleOnlyErrors: [],
};
}
const file = parse(source, {
...options,
sourceType: "unambiguous",
errorRecovery: true,
});
if (file.program.sourceType !== "script" || file.errors.length === 0)
return { file, moduleOnlyErrors: [] };
const script = parse(source, {
...options,
sourceType: "script",
errorRecovery: true,
});
const scriptErrors = new Set(script.errors.map((error) => error.message));
return {
file: script,
moduleOnlyErrors: file.errors
.map((error) => error.message)
.filter((message) => !scriptErrors.has(message)),
};
};
/** Parse inert JavaScript or TypeScript facts without attaching comments to AST nodes. */
export const parseJavaScriptSource = (
source: string,
sourcePath?: string,
): ParsedJavaScriptSource | null => {
try {
const runtimeSourceType = runtimeSourceTypeForPath(sourcePath);
const commonjs = runtimeSourceType === "commonjs";
const options = {
errorRecovery: true,
attachComment: false,
plugins: parserPluginsForPath(sourcePath),
};
const file = parse(source, {
...options,
// Inert artifact analysis retains module declarations even in .cjs assets.
// Infer their syntax here; reference imports keep filename-mode diagnostics.
sourceType: commonjs ? "unambiguous" : runtimeSourceType,
allowReturnOutsideFunction: commonjs,
allowNewTargetOutsideFunction: commonjs,
});
// Unambiguous mode retains module-only diagnostics after choosing a script.
// The .cjs extension establishes wrapper mode, so obtain its own diagnostics.
return commonjs &&
file.program.sourceType === "script" &&
file.errors.length > 0
? parse(source, { ...options, sourceType: "commonjs" })
: file;
} catch (cause: unknown) {
// Unparseable source is represented by the null return.
void cause;
return null;
}
};