From 703c11c90fb5a24dd9f511b1ad5eede336df6885 Mon Sep 17 00:00:00 2001 From: Tawhidul Islam Date: Wed, 4 Nov 2026 17:23:52 +0600 Subject: [PATCH] fix(javascript): recover configuration file reads from fs/promises (#8446) * fix(javascript): recover configuration file reads from fs/promises * fix(javascript): disallow nonexistent readFileSync on fs/promises --------- Co-authored-by: Tawhidul Islam <276017696+FhDBro@users.noreply.github.com> --- ...tSemanticAnalysis.builtin_defaults.test.ts | 55 +++++++++++++++++++ .../javascriptSemanticDataEffects.ts | 15 +++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts index 5adc82e75a..38741665e0 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts @@ -677,3 +677,58 @@ it("retains an earlier captured function after its object method is replaced", ( { kind: "acquire", method: "open" }, ]); }); + +it.each([ + "import { readFile } from 'node:fs/promises'; readFile('config.json');", + "import fs from 'node:fs/promises'; fs.readFile('config.json');", + "import fs from 'node:fs'; fs.promises.readFile('config.json');", + "const { readFile } = require('fs/promises'); readFile('config.json');", + "const fsp = require('node:fs/promises'); fsp.readFile('config.json');", + "const fs = require('node:fs'); fs.promises.readFile('config.json');", +])( + "recovers configuration file reads from promise-based fs APIs: %s", + (source) => { + const ir = analyzeCommonJsSemanticFixture(source); + expect(ir.configurationOperations).toMatchObject([ + { + kind: "file", + key: "config.json", + resolution: "complete", + }, + ]); + }, +); + +it("recovers partial configuration file reads from promise-based fs APIs with dynamic path", () => { + const ir = analyzeCommonJsSemanticFixture( + "import { readFile } from 'node:fs/promises'; readFile(getConfigPath());", + ); + expect(ir.configurationOperations).toMatchObject([ + { + kind: "file", + key: null, + resolution: "partial", + }, + ]); +}); + +it("invalidates promise-based configuration file reads after mutation", () => { + const ir = analyzeCommonJsSemanticFixture( + "const fs = require('fs'); fs.promises.readFile = custom; const fsp = require('fs/promises'); fsp.readFile('wrong');", + ); + expect(ir.configurationOperations).toEqual([]); +}); + +it.each([ + "const { readFileSync } = require('fs/promises'); readFileSync('config.json');", + "const { readFileSync } = require('node:fs/promises'); readFileSync('config.json');", + "import { readFileSync } from 'node:fs/promises'; readFileSync('config.json');", + "import fs from 'node:fs/promises'; fs.readFileSync('config.json');", + "const fs = require('node:fs'); fs.promises.readFileSync('config.json');", +])( + "does not treat nonexistent readFileSync on promise-based fs as configuration read: %s", + (source) => { + const ir = analyzeCommonJsSemanticFixture(source); + expect(ir.configurationOperations).toEqual([]); + }, +); diff --git a/src/domain/javascript/javascriptSemanticDataEffects.ts b/src/domain/javascript/javascriptSemanticDataEffects.ts index f2bfa6ee88..7e8e8163b3 100644 --- a/src/domain/javascript/javascriptSemanticDataEffects.ts +++ b/src/domain/javascript/javascriptSemanticDataEffects.ts @@ -123,11 +123,16 @@ const configurationDetails = ( } if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) return null; - const fileMethod = builtinMethod(node.callee, context.state, [ - "fs", - "node:fs", - ]); - return fileMethod === "readFile" || fileMethod === "readFileSync" + const fsMethod = builtinMethod(node.callee, context.state, ["fs", "node:fs"]); + const isFileRead = + fsMethod === "readFile" || + fsMethod === "readFileSync" || + (fsMethod === null && + builtinMethod(node.callee, context.state, [ + "fs/promises", + "node:fs/promises", + ]) === "readFile"); + return isFileRead ? { kind: "file", key: literalString(node.arguments[0]),