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
17 changes: 16 additions & 1 deletion eslint-factory/src/rules/require-mkdirsync-try-catch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("require-mkdirsync-try-catch", () => {
});
});

it("valid: non-fs objects with mkdirSync name are ignored", () => {
it("valid: non-fs receiver names with mkdirSync are ignored", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent regression coverage gap: the mock-fs valid case was removed instead of being moved to invalid, leaving the false-positive completely untested.

💡 What this hides

The deleted test case was:

`const fs = require("mock-fs"); fs.mkdirSync(dir, { recursive: true });`

With allowUnboundFsIdentifier: true, isIdentifierBoundToFsModule is skipped for any identifier named fs — including one explicitly bound to mock-fs. That means this call will now be incorrectly flagged as a violation.

The correct fix is to either:

  1. Move this case to invalid (to document the now-intended behavior — report even mock-fs-bound calls), or
  2. Fix the resolver to only skip binding analysis when the identifier is genuinely unresolved, not when it happens to be named fs.

As written, the test silently accepts the regression rather than documenting it.

cjsRuleTester.run("require-mkdirsync-try-catch", requireMkdirSyncTryCatchRule, {
valid: [`mockFs.mkdirSync(dir, { recursive: true });`, `storage.mkdirSync(dir);`, `myObj.mkdirSync(path);`, `const fs = require("mock-fs"); fs.mkdirSync(dir, { recursive: true });`],
invalid: [],
Expand All @@ -54,6 +54,21 @@ describe("require-mkdirsync-try-catch", () => {
cjsRuleTester.run("require-mkdirsync-try-catch", requireMkdirSyncTryCatchRule, {
valid: [],
invalid: [
{
code: `fs.mkdirSync(dir, { recursive: true });`,
errors: [
{
messageId: "requireTryCatch",
data: { arg: "dir" },
suggestions: [
{
messageId: "wrapInTryCatch",
output: `try {\n fs.mkdirSync(dir, { recursive: true });\n} catch (err) {\n // TODO: handle filesystem failure for this fs.mkdirSync call.\n throw new Error(\n "fs.mkdirSync failed: " + (err instanceof Error ? err.message : String(err)),\n { cause: err },\n );\n}`,
},
],
},
],
},
{
code: `const fs = require("fs"); fs.mkdirSync(dir, { recursive: true });`,
errors: [
Expand Down
2 changes: 1 addition & 1 deletion eslint-factory/src/rules/require-mkdirsync-try-catch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const requireMkdirSyncTryCatchRule = createRule({
defaultOptions: [],
create(context) {
const sourceCode = context.sourceCode;
const resolveFsSyncMethod = createFsSyncMethodResolver(sourceCode, FS_SYNC_METHODS);
const resolveFsSyncMethod = createFsSyncMethodResolver(sourceCode, FS_SYNC_METHODS, { allowUnboundFsIdentifier: true });

return {
CallExpression(node) {
Expand Down
14 changes: 13 additions & 1 deletion eslint-factory/src/rules/try-catch-rule-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ export function createFsSyncMethodResolver(sourceCode: TSESLint.SourceCode, fsSy
return false;
}

function hasAnyBinding(identifierName: string, scopeNode: TSESTree.Node): boolean {
let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode);
while (scope) {
const variable = scope.set.get(identifierName);
if (variable && variable.defs.length > 0) {
return true;
}
scope = scope.upper;
}
return false;
}

function resolveFsSyncMethodFromIdentifier(node: TSESTree.CallExpression): string | null {
const callee = node.callee;
if (callee.type !== AST_NODE_TYPES.Identifier) return null;
Expand Down Expand Up @@ -219,7 +231,7 @@ export function createFsSyncMethodResolver(sourceCode: TSESLint.SourceCode, fsSy

if (callee.type === AST_NODE_TYPES.MemberExpression) {
if (callee.object.type !== AST_NODE_TYPES.Identifier) return null;
const canUseUnboundFsIdentifier = options.allowUnboundFsIdentifier === true && callee.object.name === "fs";
const canUseUnboundFsIdentifier = options.allowUnboundFsIdentifier === true && callee.object.name === "fs" && !hasAnyBinding(callee.object.name, callee.object);
if (!canUseUnboundFsIdentifier && !isIdentifierBoundToFsModule(callee.object.name, callee.object)) return null;
return getFsSyncMethodFromProperty(callee);
}
Expand Down
Loading