fix(reference): report sloppy scripts, PHP, C# and enclosing Git facts (#1770)

* fix(reference): report sloppy scripts, PHP, C# and enclosing Git facts

Importing the source/ subtree of a legacy PHP and C# checkout produced
four wrong or missing facts:

- Babel's unambiguous mode parses a module first and keeps its
  strict-mode recovery errors after falling back to a script, so a valid
  sloppy-mode `delete messages;` was reported as a parse failure and the
  semantic IR marked the file as parser-recovered. Parsing now goes
  through one owner that re-parses such a script as a script.
- .php and .cs files had no language; they are now detected as PHP and
  C# (detection only, no parsing).
- A root without its own .git was reported as having no version
  control even inside a checkout. The adapter now resolves the nearest
  enclosing repository, as Git does, and reports `none` only when no
  ancestor has one.
- The CLI recorded importer_version as null although the package
  version is known.

* fix(reference): keep module strictness and classify C# and PHP as source

Review follow-up. `.mjs` and `.mts` files are modules by extension, but
the script fallback re-parsed them in sloppy mode and erased real
strict-mode errors. The parser boundary now owns the extension rule,
parses explicit modules as modules and applies the fallback only when
the mode is ambiguous; the require-binding analysis uses the same rule.

`.cs` and `.php` received a language but stayed classified as unknown,
so source-to-bundle comparison dropped them. Both are now source
extensions, like the other detected languages.

* fix(reference): honor package module scope and keep unknown-mode strict errors

Review follow-up. A `.js` file under a package.json with `"type":
"module"` is an ES module, but it went through the script fallback that
erases strict-mode errors. The import now reads each in-inventory
package.json and parses `.js` files in a module scope as modules. Other
`.js` and `.ts` sources keep an unknown mode, because bundlers treat them
as modules without that field: they are no longer forced to script, and
errors that only the module parse reports are kept as an entry limitation.

* fix(reference): bound package scopes and keep application parsing unchanged

Review follow-up.
- `.cjs` files parse as scripts, so module-only strict errors no longer
  become limitations on CommonJS trees. `.cts` keeps an unknown mode
  because TypeScript compiles ES import syntax there.
- An unreadable or linked package.json now bounds the module scope with
  an unknown type instead of letting an outer `"type": "module"` apply.
- The application pipeline parses as on main again. Its module-only
  strict errors keep a source partial; package-mode handling stays in the
  reference importer.

* refactor(reference): keep unmarked module modes unknown instead of resolving package scope

Review follow-up. Resolving Node package scope for `.js` kept pulling in
more of Node's lookup semantics (require bindings, excluded or
directory-named manifests). Only extensions now select a mode: `.mjs`
and `.mts` parse as modules and `.cjs` as scripts. Every other source
keeps an unknown mode, so module-only strict errors stay visible as an
entry limitation instead of being dropped or asserted. This matches the
require-binding analysis, which also treats unmarked paths as unknown.
This commit is contained in:
Martin Klein
2026-10-12 22:52:27 +02:00
committed by GitHub
parent 3ee6ff43c1
commit 7b15762d09
9 changed files with 231 additions and 44 deletions
@@ -1,4 +1,4 @@
import { parse, type ParserPlugin } from "@babel/parser";
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>;
@@ -37,6 +37,67 @@ export const parserPluginsForPath = (sourcePath?: string): ParserPlugin[] => {
: ["decorators-legacy", typescript];
};
/** `.mjs` and `.mts` are modules by extension; other paths leave the mode to syntax. */
export const isExplicitModuleSourcePath = (path?: string): boolean =>
/\.m[jt]s$/iu.test(path ?? "");
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 => {
// `.cjs` is CommonJS by extension, so module-only errors cannot apply.
if (/\.cjs$/iu.test(sourcePath ?? ""))
return {
file: parse(source, {
...options,
sourceType: "script",
errorRecovery: true,
}),
moduleOnlyErrors: [],
};
if (isExplicitModuleSourcePath(sourcePath))
return {
file: parse(source, {
...options,
sourceType: "module",
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 JavaScript or TypeScript once without attaching comments to AST nodes. */
export const parseJavaScriptSource = (
source: string,