unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
refactoringtesting-strategy

fs: apply nocase to literal glob exclude patterns

Project: nodejs/node | Language: JavaScript | Concepts: refactoring, testing-strategy | Difficulty: mid
Link: https://github.com/nodejs/node/pull/64817

The problem

On Windows and macOS, file systems are typically case-insensitive. The glob implementation in Node.js matched include patterns case-insensitively, but literal exclude patterns (those without wildcards) were matched case-sensitively. This meant files could be included by an insensitive match but not excluded by a literal pattern with different casing, leading to inconsistent and surprising results.

Why it's non-trivial

A naive fix would simply make all exclude patterns case-insensitive, but this could break expected behavior for patterns containing "magic" (wildcards) or on case-sensitive filesystems (like Linux). The solution needs to apply case-insensitive matching only where appropriate: for literal patterns on case-insensitive platforms, and only in the exclude logic.

The approach

The main change is in how exclude matchers are constructed in the glob implementation:

-          .map((pattern) => createMatcher(pattern));
+          // Exclude matching is a pure string comparison with no filesystem
+          // lookups, so unlike include patterns, literal patterns must also
+          // match case-insensitively on case-insensitive platforms.
+          .map((pattern) => createMatcher(pattern, { nocaseMagicOnly: false }));

What this does:
Previously, exclude patterns were passed to createMatcher with default options, which inherited nocaseMagicOnly: true. This meant only patterns with "magic" (wildcards) would be matched case-insensitively. The change explicitly passes { nocaseMagicOnly: false }, ensuring all exclude patterns (including literals) are matched case-insensitively on case-insensitive platforms.

Why:
This aligns exclude pattern matching with how include patterns are resolved on Windows and macOS, preventing mismatches due to casing.

The PR also adds tests to verify the new behavior:

+// gh-58991: exclude patterns must apply the same case-sensitivity as include
+// patterns. On case-insensitive filesystems include patterns match entries
+// regardless of case, so literal exclude patterns must match that behavior.
+const skipCaseTests = { skip: !common.isWindows && !common.isMacOS };
+describe('glob - exclude case-insensitive consistency', skipCaseTests, function() {
+  test('literal exclude ignores case (sync)', () => {
+    assert.deepStrictEqual(
+      globSync('a/b', { cwd: fixtureDir, exclude: ['A/B'] }), []);
+  });
+  test('literal exclude matches differently-cased results (sync)', () => {
+    assert.deepStrictEqual(
+      globSync('A/b', { cwd: fixtureDir, exclude: ['a/b'] }), []);
+  });
+  // ... async and promise-based variants
+});

What this does:
These tests ensure that, on Windows and macOS, exclude patterns like 'A/B' will correctly exclude results like 'a/b' regardless of case, matching the behavior of the include logic.

From the review thread

No notable review discussion on this PR.

Takeaways

  • When implementing pattern matching that interacts with the filesystem, always align case sensitivity with the platform's filesystem behavior to avoid surprising results.
  • Literal pattern matching should be consistent between include and exclude logic, especially on case-insensitive filesystems.
  • Adding targeted regression tests for platform-specific behavior helps prevent future breakage and clarifies intent.