mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
* 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.
188 lines
6.5 KiB
TypeScript
188 lines
6.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import { analyzeParsedJavaScriptReferences } from "./javascript/javascriptSemanticAnalysis.js";
|
|
import {
|
|
semanticBindingId,
|
|
semanticScopeId,
|
|
} from "./javascript/javascriptSemanticState.js";
|
|
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
|
import type { JavaScriptSemanticReference } from "./javascript/javascriptSemanticIr.js";
|
|
import {
|
|
isExplicitModuleSourcePath,
|
|
type ParsedJavaScriptSource,
|
|
} from "./javascript/javascriptSourceParser.js";
|
|
|
|
/**
|
|
* Keep calls that resolve either to Node's CommonJS wrapper loader or to a
|
|
* direct ESM `createRequire(import.meta.url)` binding. The shared lexical
|
|
* analyzer supplies binding ownership; this boundary accounts for the two
|
|
* Node loader bindings without evaluating arbitrary values.
|
|
*/
|
|
export const unshadowedReferenceSourceRequireCalls = (
|
|
file: Pick<ParsedJavaScriptSource, "program">,
|
|
path: string,
|
|
): ReadonlySet<t.CallExpression> => {
|
|
const references = new Map(
|
|
analyzeParsedJavaScriptReferences(file).map((reference) => [
|
|
`${reference.location.start.line}:${reference.location.start.column}`,
|
|
reference,
|
|
]),
|
|
);
|
|
const programRequireBinding = semanticBindingId(
|
|
semanticScopeId("program", file.program),
|
|
"require",
|
|
);
|
|
// Syntax alone cannot establish Node's runtime mode: `.js` depends on its
|
|
// package scope, and `.ts` may be compiled with a CommonJS module target.
|
|
// Keep unmarked paths runtime-unknown and preserve candidate require edges.
|
|
const allowsUnboundRequire = !isExplicitModuleSourcePath(path);
|
|
const hasTopLevelVarRequire =
|
|
allowsUnboundRequire && hasUninitializedTopLevelVarRequire(file.program);
|
|
const createRequireBinding = directCreateRequireBinding(
|
|
file.program,
|
|
references,
|
|
);
|
|
const requireLoaderWasWritten = [...references.values()].some(
|
|
(reference) =>
|
|
reference.name === "require" &&
|
|
reference.role === "write" &&
|
|
(reference.resolution === "unbound" ||
|
|
reference.bindingId === programRequireBinding),
|
|
);
|
|
const calls = new Set<t.CallExpression>();
|
|
|
|
traverseJavaScriptAst(file.program, {
|
|
enter: (node, _parent, readAncestors) => {
|
|
if (!t.isCallExpression(node)) return;
|
|
const identifier = requireCalleeIdentifier(node.callee);
|
|
const start = identifier?.loc?.start;
|
|
if (start === undefined) return;
|
|
if (
|
|
readAncestors().some(
|
|
(ancestor) =>
|
|
t.isWithStatement(ancestor) && contains(ancestor.body, node),
|
|
)
|
|
)
|
|
return;
|
|
const reference = references.get(`${start.line}:${start.column}`);
|
|
if (
|
|
(allowsUnboundRequire &&
|
|
!requireLoaderWasWritten &&
|
|
reference?.resolution === "unbound") ||
|
|
(((hasTopLevelVarRequire && !requireLoaderWasWritten) ||
|
|
createRequireBinding) &&
|
|
reference?.resolution === "resolved" &&
|
|
reference.bindingId === programRequireBinding)
|
|
)
|
|
calls.add(node);
|
|
},
|
|
});
|
|
return calls;
|
|
};
|
|
|
|
const contains = (container: t.Node, node: t.Node): boolean =>
|
|
typeof container.start === "number" &&
|
|
typeof container.end === "number" &&
|
|
typeof node.start === "number" &&
|
|
typeof node.end === "number" &&
|
|
container.start <= node.start &&
|
|
node.end <= container.end;
|
|
|
|
const hasUninitializedTopLevelVarRequire = (program: t.Program): boolean => {
|
|
let found = false;
|
|
let initialized = false;
|
|
traverseJavaScriptAst(program, {
|
|
enter: (node, _parent, readAncestors) => {
|
|
if (!t.isVariableDeclaration(node, { kind: "var" })) return;
|
|
if (
|
|
readAncestors().some(
|
|
(ancestor) => t.isFunction(ancestor) || t.isStaticBlock(ancestor),
|
|
)
|
|
)
|
|
return;
|
|
for (const declaration of node.declarations) {
|
|
if (!Object.hasOwn(t.getBindingIdentifiers(declaration.id), "require"))
|
|
continue;
|
|
found = true;
|
|
initialized ||= declaration.init !== null;
|
|
}
|
|
},
|
|
});
|
|
return found && !initialized;
|
|
};
|
|
|
|
const directCreateRequireBinding = (
|
|
program: t.Program,
|
|
references: ReadonlyMap<string, JavaScriptSemanticReference>,
|
|
): boolean => {
|
|
const programScope = semanticScopeId("program", program);
|
|
const importedNames = new Set<string>();
|
|
for (const declaration of program.body) {
|
|
if (
|
|
!t.isImportDeclaration(declaration) ||
|
|
(declaration.source.value !== "node:module" &&
|
|
declaration.source.value !== "module")
|
|
)
|
|
continue;
|
|
for (const specifier of declaration.specifiers) {
|
|
if (
|
|
t.isImportSpecifier(specifier) &&
|
|
t.isIdentifier(specifier.imported, { name: "createRequire" })
|
|
)
|
|
importedNames.add(specifier.local.name);
|
|
}
|
|
}
|
|
|
|
return program.body.some(
|
|
(statement) =>
|
|
t.isVariableDeclaration(statement, { kind: "const" }) &&
|
|
statement.declarations.some((declaration) => {
|
|
if (
|
|
!t.isIdentifier(declaration.id, { name: "require" }) ||
|
|
!t.isCallExpression(declaration.init) ||
|
|
!t.isIdentifier(declaration.init.callee) ||
|
|
!importedNames.has(declaration.init.callee.name) ||
|
|
!isImportMetaUrl(declaration.init.arguments[0])
|
|
)
|
|
return false;
|
|
const start = declaration.init.callee.loc?.start;
|
|
if (start === undefined) return false;
|
|
const reference = references.get(`${start.line}:${start.column}`);
|
|
return (
|
|
reference?.resolution === "resolved" &&
|
|
reference.bindingId ===
|
|
semanticBindingId(programScope, declaration.init.callee.name)
|
|
);
|
|
}),
|
|
);
|
|
};
|
|
|
|
const isImportMetaUrl = (node: t.Node | undefined): boolean =>
|
|
t.isMemberExpression(node) &&
|
|
!node.computed &&
|
|
t.isIdentifier(node.property, { name: "url" }) &&
|
|
t.isMetaProperty(node.object) &&
|
|
t.isIdentifier(node.object.meta, { name: "import" }) &&
|
|
t.isIdentifier(node.object.property, { name: "meta" });
|
|
|
|
const requireCalleeIdentifier = (
|
|
callee: t.Node | null | undefined,
|
|
): t.Identifier | undefined => {
|
|
if (t.isIdentifier(callee, { name: "require" })) return callee;
|
|
if (
|
|
!t.isMemberExpression(callee) ||
|
|
!t.isIdentifier(callee.object, { name: "require" }) ||
|
|
((!t.isIdentifier(callee.property) || callee.computed) &&
|
|
!t.isStringLiteral(callee.property))
|
|
)
|
|
return undefined;
|
|
const propertyName = t.isIdentifier(callee.property)
|
|
? callee.property.name
|
|
: t.isStringLiteral(callee.property)
|
|
? callee.property.value
|
|
: undefined;
|
|
return propertyName === "resolve" || propertyName === "main"
|
|
? callee.object
|
|
: undefined;
|
|
};
|