diff --git a/packages/polyfill-connectors/package.json b/packages/polyfill-connectors/package.json index cb15d5add..d374d0453 100644 --- a/packages/polyfill-connectors/package.json +++ b/packages/polyfill-connectors/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9a640e04..c6602fc74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -498,8 +498,8 @@ importers: version: 4.4.3 devDependencies: '@babel/parser': - specifier: 7.29.2 - version: 7.29.2 + specifier: 8.0.4 + version: 8.0.4 '@biomejs/biome': specifier: 2.5.8 version: 2.5.8 @@ -586,8 +586,8 @@ importers: version: 3.6.7 devDependencies: '@babel/parser': - specifier: 7.29.2 - version: 7.29.2 + specifier: 8.0.4 + version: 8.0.4 '@biomejs/biome': specifier: 2.5.8 version: 2.5.8 @@ -8789,7 +8789,7 @@ snapshots: '@vue/compiler-sfc@2.7.16': dependencies: - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 postcss: 8.5.26 source-map: 0.6.1 optionalDependencies: diff --git a/reference-implementation/package.json b/reference-implementation/package.json index 78f2d4005..c6f03282e 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -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", diff --git a/reference-implementation/test/helpers/ri-zero-connector-knowledge-ast-shared.ts b/reference-implementation/test/helpers/ri-zero-connector-knowledge-ast-shared.ts index 1192af433..c4fcfc313 100644 --- a/reference-implementation/test/helpers/ri-zero-connector-knowledge-ast-shared.ts +++ b/reference-implementation/test/helpers/ri-zero-connector-knowledge-ast-shared.ts @@ -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 `` 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 @@ -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; diff --git a/reference-implementation/test/helpers/ri-zero-connector-knowledge-data-load-scan.ts b/reference-implementation/test/helpers/ri-zero-connector-knowledge-data-load-scan.ts index 6628ed20d..cbb26d78a 100644 --- a/reference-implementation/test/helpers/ri-zero-connector-knowledge-data-load-scan.ts +++ b/reference-implementation/test/helpers/ri-zero-connector-knowledge-data-load-scan.ts @@ -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; @@ -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)) { diff --git a/reference-implementation/test/helpers/ri-zero-connector-knowledge-identity-scan.ts b/reference-implementation/test/helpers/ri-zero-connector-knowledge-identity-scan.ts index 0fcce983b..448c47ae1 100644 --- a/reference-implementation/test/helpers/ri-zero-connector-knowledge-identity-scan.ts +++ b/reference-implementation/test/helpers/ri-zero-connector-knowledge-identity-scan.ts @@ -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"); @@ -606,13 +620,18 @@ function collectManifestImportBindings(program: Node, fileDir: string): Set", line: typed.loc.start.line, unresolvable: true }; + } + return globalThis.undefined; +} + +function requireCallSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccurrence | undefined { if (typed.type !== "CallExpression") { return; } @@ -166,22 +195,19 @@ function dynamicCallSpecifier(typed: BabelNodeWithLoc): RawSpecifierOccurrence | 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: "", line: typed.loc.start.line, unresolvable: true }; + return { form: "require", value: "", line: typed.loc.start.line, unresolvable: true }; } return globalThis.undefined; } @@ -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); }