From ad1f6b508fbdd44d8bf1884ed96ae69082808e7d Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 20:38:11 -0700 Subject: [PATCH] fix(javascript): keep exact empty require paths and request fields Migrate the remaining `""`-sentinel consumers from #783 to the nullable `semanticStaticPropertyKey`: CommonJS module origins (`require("m")['']`), require destructuring (`const {'': a} = require("m")`), and request body fields (`{ '': value }`). Dynamic keys still produce no link or field. API selectors that compare against known names are unchanged. Fixes #783 Co-Authored-By: Claude Opus 5.5 --- ...ascriptSemanticAnalysis.empty_keys.test.ts | 48 +++++++++++++++++++ src/domain/javascriptSemanticDataEffects.ts | 4 +- src/domain/javascriptSemanticProjection.ts | 13 ++--- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts b/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts index 014934066..168b3a36c 100644 --- a/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts +++ b/src/domain/javascriptSemanticAnalysis.empty_keys.test.ts @@ -55,3 +55,51 @@ it("retains exact empty method names and environment keys", () => { { kind: "environment", key: null, resolution: "partial" }, ]); }); + +it("retains exact empty require paths without dynamic-key links", () => { + const ir = analyzeJavaScriptSemantics(` + const { '': fromPattern } = require("./table.js"); + const fromMember = require("./table.js")['']; + const { [key]: dynamicPattern } = require("./table.js"); + const dynamicMember = require("./table.js")[key]; + module.exports.root = require("./table.js")['']; + `); + expect( + ir.moduleLinks.map(({ kind, importedName, localName, exportedName }) => ({ + kind, + importedName, + localName, + exportedName, + })), + ).toEqual([ + { + kind: "require", + importedName: "", + localName: "fromPattern", + exportedName: null, + }, + { + kind: "require", + importedName: "", + localName: "fromMember", + exportedName: null, + }, + { + kind: "commonjs-export", + importedName: "", + localName: null, + exportedName: "root", + }, + ]); +}); + +it("retains exact empty request field names without dynamic fields", () => { + const ir = analyzeJavaScriptSemantics(` + fetch("https://example.test", { '': value, [key]: 1, body: "x" }); + `); + expect( + ir.requestOperations.flatMap(({ fields }) => + fields.map(({ name }) => name), + ), + ).toEqual(["", "body"]); +}); diff --git a/src/domain/javascriptSemanticDataEffects.ts b/src/domain/javascriptSemanticDataEffects.ts index 830d2e94b..e3874fd52 100644 --- a/src/domain/javascriptSemanticDataEffects.ts +++ b/src/domain/javascriptSemanticDataEffects.ts @@ -405,8 +405,8 @@ const requestFields = ( if (!t.isObjectProperty(property)) return []; // `{[key]: 1}` sends no knowable field name; reporting `key` would invent // a request field that may not exist. - const name = semanticStaticPropertyName(property.key, property.computed); - if (name.length === 0) return []; + const name = semanticStaticPropertyKey(property.key, property.computed); + if (name === null) return []; return [ { name, diff --git a/src/domain/javascriptSemanticProjection.ts b/src/domain/javascriptSemanticProjection.ts index c6dc3cd02..4db6d9bf8 100644 --- a/src/domain/javascriptSemanticProjection.ts +++ b/src/domain/javascriptSemanticProjection.ts @@ -25,10 +25,7 @@ import { range, stringValue, } from "./javascriptStaticAnalysisHelpers.js"; -import { - semanticStaticPropertyKey, - semanticStaticPropertyName, -} from "./javascriptAstValues.js"; +import { semanticStaticPropertyKey } from "./javascriptAstValues.js"; interface CollectCallableInput { readonly node: t.Node; @@ -181,8 +178,8 @@ const semanticModuleOrigin = ( const nested = t.isNode(node.object) ? semanticModuleOrigin(node.object, projection, state) : undefined; - const member = semanticStaticPropertyName(node.property, node.computed); - return nested === undefined || member === "" + const member = semanticStaticPropertyKey(node.property, node.computed); + return nested === undefined || member === null ? undefined : { ...nested, importedPath: [...nested.importedPath, member] }; } @@ -341,8 +338,8 @@ const requirePatternBindings = ( if (t.isObjectPattern(pattern)) return pattern.properties.flatMap((property) => { if (t.isRestElement(property)) return []; - const name = semanticStaticPropertyName(property.key, property.computed); - return name === "" + const name = semanticStaticPropertyKey(property.key, property.computed); + return name === null ? [] : requirePatternBindings(property.value, [...path, name]); });