Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/polyfill-connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@babel/parser": "7.29.2",
"@babel/parser": "8.0.4",
"@biomejs/biome": "2.5.8",
"@types/better-sqlite3": "^9.6.0",
"@types/ws": "^8.18.1",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion reference-implementation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"web-push": "^3.6.7"
},
"devDependencies": {
"@babel/parser": "7.29.2",
"@babel/parser": "8.0.4",
"@biomejs/biome": "2.5.8",
"@types/pg": "8.21.0",
"jsdom": "^30.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,18 @@ export function calleeName(callee: Node): string | null {
* `jsx` Babel parser plugins are mutually exclusive for `.ts` (non-`.tsx`)
* sources (enabling both misparses a type-cast or generic like `<T>` as a
* JSX element), so `.tsx`/`.jsx` files select
* `["typescript", "jsx", "decorators", "importAttributes"]` and everything
* else selects `["typescript", "decorators", "importAttributes"]`. The
* `decorators` plugin (standard/stage-3 syntax, not `decorators-legacy`) is
* always enabled: this repo's `tsconfig.json` sets `erasableSyntaxOnly:
* true`, which rejects the legacy experimental-decorators form outright, so
* standard decorators are the only decorator syntax that can validly appear
* in a real `.ts` source file here — without this plugin, any file using
* that (valid, erasable) syntax would hit the parse-failure path below for
* a reason that has nothing to do with the file being malformed.
* `["typescript", "jsx", "decorators"]` and everything else selects
* `["typescript", "decorators"]`. The `decorators` plugin (standard/stage-3
* syntax, not `decorators-legacy`) is always enabled: this repo's
* `tsconfig.json` sets `erasableSyntaxOnly: true`, which rejects the legacy
* experimental-decorators form outright, so standard decorators are the
* only decorator syntax that can validly appear in a real `.ts` source file
* here — without this plugin, any file using that (valid, erasable) syntax
* would hit the parse-failure path below for a reason that has nothing to
* do with the file being malformed. `importAttributes` (Babel 7's opt-in
* flag for `import x from "y" with { type: "json" }`) was removed in Babel 8
* — that syntax now parses unconditionally, so the plugin name is no longer
* needed (or valid) here.
*
* Throws on a genuine parse failure (mirroring `@babel/parser`'s own
* `parse()`); callers keep their own try/catch around this call. A parse
Expand All @@ -156,9 +159,7 @@ export function parseSource(raw: string, absPath: string): Node {
const isJsxExtension = absPath.endsWith(".tsx") || absPath.endsWith(".jsx");
const ast = parse(raw, {
errorRecovery: true,
plugins: isJsxExtension
? ["typescript", "jsx", "decorators", "importAttributes"]
: ["typescript", "decorators", "importAttributes"],
plugins: isJsxExtension ? ["typescript", "jsx", "decorators"] : ["typescript", "decorators"],
sourceType: "module",
}) as unknown as { program: Node };
return ast.program;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -959,17 +959,38 @@ export function scanFileDataLoads(
return true;
}

/** require(...) / dynamic import(...) reaching a sibling JSON/YAML resource. Returns true if this call site was handled. */
function checkRequireOrDynamicImport(node: Node, callee: Node, enclosingFunctionName: string | null): boolean {
const isRequire = node.type === "CallExpression" && isIdentifier(callee, "require");
const isDynamicImport = node.type === "CallExpression" && callee.type === "Import";
if (!(isRequire || isDynamicImport)) {
/** require(...) reaching a sibling JSON/YAML resource. Returns true if this call site was handled. */
function checkRequireCall(node: Node, callee: Node, enclosingFunctionName: string | null): boolean {
if (!(node.type === "CallExpression" && isIdentifier(callee, "require"))) {
return false;
}
const [first] = nodeArrayField(node, "arguments");
if (!first) {
return true;
}
return checkResolvedImportLikeSource(node, first, enclosingFunctionName);
}

/** Dynamic `import(...)` (a Babel `ImportExpression` node, not a `CallExpression` —
* unlike `require(...)`, `@babel/parser` has never modeled dynamic import as a call
* with an `Import` pseudo-callee; that legacy shape belongs to older non-Babel
* parsers) reaching a sibling JSON/YAML resource. Returns true if this call site
* was handled. */
function checkDynamicImportExpression(node: Node, enclosingFunctionName: string | null): boolean {
if (node.type !== "ImportExpression") {
return false;
}
const source = nodeField(node, "source");
if (!source) {
return true;
}
return checkResolvedImportLikeSource(node, source, enclosingFunctionName);
}

/** Shared resolution/classification tail for `require(...)`'s and dynamic
* `import(...)`'s first argument/`source`. Always returns true (the call site was
* handled) — callers only reach this once they've confirmed the node shape matches. */
function checkResolvedImportLikeSource(node: Node, first: Node, enclosingFunctionName: string | null): boolean {
const siteKey = `${relPath}:${lineOf(node)}`;
if (SANCTIONED_GENERIC_DATA_READ_CALL_SITES.has(siteKey)) {
return true;
Expand Down Expand Up @@ -1087,16 +1108,21 @@ export function scanFileDataLoads(
}

walk(program, (node, parent, ancestors) => {
const enclosingFunctionName = enclosingFunctionNameOf(ancestors);

if (node.type === "ImportExpression") {
checkDynamicImportExpression(node, enclosingFunctionName);
return;
}
if (node.type !== "CallExpression" && node.type !== "NewExpression") {
return;
}
const callee = node.callee as Node;
const enclosingFunctionName = enclosingFunctionNameOf(ancestors);

if (checkProhibitedEvasionMechanism(node, callee)) {
return;
}
if (checkRequireOrDynamicImport(node, callee, enclosingFunctionName)) {
if (checkRequireCall(node, callee, enclosingFunctionName)) {
return;
}
if (checkReadFileCall(node, callee, parent, enclosingFunctionName)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,20 +557,34 @@ function scanDynamicImportSpecifiers(
report: ReportFn
): void {
walk(program, (node, _parent, ancestors) => {
const enclosingFunctionName = enclosingFunctionNameOf(ancestors);

// Dynamic `import(...)` parses as its own `ImportExpression` node (its
// specifier is `.source`, not a `CallExpression`'s first argument) —
// @babel/parser has never modeled it as a call with an `Import`
// pseudo-callee.
if (node.type === "ImportExpression") {
const source = nodeField(node, "source");
if (source) {
const resolvedPath = resolveImportSpecifierPath(source, analysis, enclosingFunctionName, fileDir);
if (resolvedPath && isConnectorModulePath(resolvedPath)) {
report(node, "connector-module-import");
}
}
return;
}

if (node.type !== "CallExpression") {
return;
}
const callee = node.callee as Node;
const isDynamicImport = callee.type === "Import";
const isRequire = isIdentifier(callee, "require");
if (!(isDynamicImport || isRequire)) {
if (!isIdentifier(callee, "require")) {
return;
}
const [first] = nodeArrayField(node, "arguments");
if (!first) {
return;
}
const enclosingFunctionName = enclosingFunctionNameOf(ancestors);
const resolvedPath = resolveImportSpecifierPath(first, analysis, enclosingFunctionName, fileDir);
if (resolvedPath && isConnectorModulePath(resolvedPath)) {
report(node, "connector-module-import");
Expand Down Expand Up @@ -606,13 +620,18 @@ function collectManifestImportBindings(program: Node, fileDir: string): Set<stri
return;
}
const init = node.init as Node;
const isRequireOrImportCall =
init.type === "CallExpression" &&
(isIdentifier(init.callee as Node, "require") || (init.callee as Node).type === "Import");
if (!isRequireOrImportCall) {
return;
// Dynamic `import(...)` parses as its own `ImportExpression` node (its
// specifier is `.source`, not a `CallExpression`'s first argument) —
// @babel/parser has never modeled it as a call with an `Import`
// pseudo-callee.
const isRequireCall = init.type === "CallExpression" && isIdentifier(init.callee as Node, "require");
let first: Node | undefined;
if (init.type === "ImportExpression") {
first = nodeField(init, "source");
} else if (isRequireCall) {
const [requireArg] = nodeArrayField(init, "arguments");
first = requireArg;
}
const [first] = nodeArrayField(init, "arguments");
if (first?.type !== "StringLiteral" || !isRelativeSpecifier(first.value as string)) {
return;
}
Expand Down
43 changes: 35 additions & 8 deletions scripts/test-migration/importer-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,30 +158,56 @@ function staticImportExportSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccur
return globalThis.undefined;
}

function dynamicCallSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccurrence | undefined {
/** Dynamic `import(...)` parses as its own `ImportExpression` node (its
* argument is `.source`, not a `CallExpression`'s first argument) —
* @babel/parser has never modeled dynamic import as a call with an
* `Import` pseudo-callee; that legacy shape belongs to older non-Babel
* parsers. */
function dynamicImportSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccurrence | undefined {
if (typed.type !== "ImportExpression") {
return;
}
const { source } = typed as { source?: { type?: string; value?: unknown } };
const resolved = stringOrTemplateStaticValue(source);
if (resolved) {
return {
form: "dynamic-import",
value: resolved.value,
line: typed.loc.start.line,
unresolvable: resolved.unresolvable,
};
}
if (source) {
// A computed/non-literal argument (e.g. a variable) — cannot be
// statically resolved. Reported as unresolvable rather than dropped: a
// caller with a renamed file in scope must see this as UNKNOWN, not as
// silence.
return { form: "dynamic-import", value: "<computed>", line: typed.loc.start.line, unresolvable: true };
}
return globalThis.undefined;
}

function requireCallSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccurrence | undefined {
if (typed.type !== "CallExpression") {
return;
}
const { callee, arguments: args = [] } = typed as {
arguments?: unknown[];
callee?: { name?: string; type?: string };
};
const isDynamicImport = callee?.type === "Import";
const isRequireCall = callee?.type === "Identifier" && callee.name === "require";
if (!(isDynamicImport || isRequireCall)) {
if (!(callee?.type === "Identifier" && callee.name === "require")) {
return;
}
const form: SpecifierForm = isDynamicImport ? "dynamic-import" : "require";
const resolved = stringOrTemplateStaticValue(args[0] as { type?: string; value?: unknown } | undefined);
if (resolved) {
return { form, value: resolved.value, line: typed.loc.start.line, unresolvable: resolved.unresolvable };
return { form: "require", value: resolved.value, line: typed.loc.start.line, unresolvable: resolved.unresolvable };
}
if (args.length > 0) {
// A computed/non-literal argument (e.g. a variable) — cannot be
// statically resolved. Reported as unresolvable rather than dropped: a
// caller with a renamed file in scope must see this as UNKNOWN, not as
// silence.
return { form, value: "<computed>", line: typed.loc.start.line, unresolvable: true };
return { form: "require", value: "<computed>", line: typed.loc.start.line, unresolvable: true };
}
return globalThis.undefined;
}
Expand All @@ -192,7 +218,8 @@ function collectSpecifierOccurrences(sourceText: string, fileName: string): RawS
const found: RawSpecifierOccurrence[] = [];
walkBabelAst(ast.program, (node) => {
const typed = node as BabelNodeWithLoc;
const occurrence = staticImportExportSpecifier(typed) ?? dynamicCallSpecifier(typed);
const occurrence =
staticImportExportSpecifier(typed) ?? dynamicImportSpecifier(typed) ?? requireCallSpecifier(typed);
if (occurrence) {
found.push(occurrence);
}
Expand Down