From 44b197f924ef23814e480e91d97ed65fa28cbff9 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Wed, 14 Oct 2026 18:12:30 -0700 Subject: [PATCH] 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(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 --- docs/javascript-artifact-reconstruction.md | 3 ++ .../javascript/javascriptSourceParser.test.ts | 42 +++++++++++++++++++ .../javascript/javascriptSourceParser.ts | 12 ++++-- 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 src/domain/javascript/javascriptSourceParser.test.ts diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index c37a99002..18175ef8c 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -206,6 +206,9 @@ code unit, matching the original bytes identified by the artifact digest. Plain `.ts` artifact sources use TypeScript syntax without JSX, including angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing. +JavaScript sources use JavaScript grammar with JSX, never TypeScript syntax, +so valid JavaScript that TypeScript reads differently keeps its JavaScript +meaning. Relative TypeScript imports can name their emitted `.js`, `.mjs`, or `.cjs` counterparts while resolving to inventoried source files. This substitution diff --git a/src/domain/javascript/javascriptSourceParser.test.ts b/src/domain/javascript/javascriptSourceParser.test.ts new file mode 100644 index 000000000..e5f829a35 --- /dev/null +++ b/src/domain/javascript/javascriptSourceParser.test.ts @@ -0,0 +1,42 @@ +import { expect, it } from "vitest"; + +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; + +const analyze = (source: string, path: string) => { + const parsed = parseJavaScriptSource(source, path); + return parsed === null + ? null + : { + errors: parsed.errors.length, + facts: analyzeParsedJavaScriptStaticSource(source, parsed), + }; +}; + +it("parses conditional arrow bodies in JavaScript sources with JavaScript grammar", () => { + // Bundlers emit this shape; TypeScript reads `):H=>` as a return type. + const source = `const {ipcMain}=require("electron"); +const pick=n===0?H=>({start:H.startY}):H=>({start:H.startX}); +ipcMain.handle("layout:ready",()=>pick);`; + for (const path of ["main.js", "main.cjs", "main.mjs", "main.jsx"]) { + const result = analyze(source, path); + expect(result?.errors).toBe(0); + expect(result?.facts.electron.ipc).toContainEqual( + expect.objectContaining({ channel: "layout:ready", operation: "handle" }), + ); + } +}); + +it("keeps JavaScript comparison meaning for angle brackets in JavaScript sources", () => { + const source = `const a=fetch("/api/compare");`; + expect(analyze(source, "main.js")?.facts.endpoints).toEqual([]); + expect(analyze(source, "main.ts")?.facts.endpoints).toContainEqual( + expect.objectContaining({ value: "/api/compare" }), + ); +}); + +it("keeps TypeScript syntax available for TypeScript and unnamed sources", () => { + const source = "let count: number = 1; export type Id = string;"; + expect(parseJavaScriptSource(source, "value.ts")?.errors).toEqual([]); + expect(parseJavaScriptSource(source)?.errors).toEqual([]); +}); diff --git a/src/domain/javascript/javascriptSourceParser.ts b/src/domain/javascript/javascriptSourceParser.ts index 630d5e9bf..d1141a243 100644 --- a/src/domain/javascript/javascriptSourceParser.ts +++ b/src/domain/javascript/javascriptSourceParser.ts @@ -9,17 +9,21 @@ export type ParsedJavaScriptSource = ReturnType; * * - `dts:true` only for `.d.ts/.d.mts/.d.cts` ambient declarations; * ordinary `.ts` keeps the missing-initializer diagnostic. - * - JSX enabled for `.tsx`, `.jsx`, `.js`, and unknown paths; disabled for - * plain `.ts/.cts/.mts` (angle-bracket assertions, not JSX) and for all + * - `.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(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-capable contract (best-effort - * parse; failures surface via `errorRecovery`, never silent misparse). + * - 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);