Repository: nodejs/node
Pull request: fs: detect dot files when using globstar

Using globstar in glob pattern prevents dot (hidden) files from being matched

Fixes: https://github.com/nodejs/node/issues/56321

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/fs/glob.js
patch: @@ -442,7 +442,18 @@ class Glob {
         const fromSymlink = pattern.symlinks.has(index);
 
         if (current === lazyMinimatch().GLOBSTAR) {
-          if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
+          const isDot = entry.name[0] === '.';
+          const nextMatches = pattern.test(nextIndex, entry.name);
+
+          let nextNonGlobIndex = nextIndex;
+          while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) {
+            nextNonGlobIndex++;
+          }
+
+          const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
+
+          if ((isDot && !matchesDot) ||
+              (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
             continue;
           }
           if (!fromSymlink && entry.isDirectory()) {
@@ -455,7 +466,6 @@ class Glob {
 
           // Any pattern after ** is also a potential pattern
           // so we can already test it here
-          const nextMatches = pattern.test(nextIndex, entry.name);
           if (nextMatches && nextIndex === last && !isLast) {
             // If next pattern is the last one, add to results
             this.#results.add(entryPath);
@@ -642,30 +652,36 @@ class Glob {
         const fromSymlink = pattern.symlinks.has(index);
 
         if (current === lazyMinimatch().GLOBSTAR) {
-          if (entry.name[0] === '.' || (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
+          const isDot = entry.name[0] === '.';
+          const nextMatches = pattern.test(nextIndex, entry.name);
+
+          let nextNonGlobIndex = nextIndex;
+          while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) {
+            nextNonGlobIndex++;
+          }
+
+          const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
+
+          if ((isDot && !matchesDot) ||
+              (this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
             continue;
           }
           if (!fromSymlink && entry.isDirectory()) {
             // If directory, add ** to its potential patterns
             subPatterns.add(index);
           } else if (!fromSymlink && index === last) {
             // If ** is last, add to results
-            if (!this.#results.has(entryPath)) {
-              if (this.#results.add(entryPath)) {
-                yield this.#withFileTypes ? entry : entryPath;
-              }
+            if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
+              yield this.#withFileTypes ? entry : entryPath;
             }
           }
 
           // Any pattern after ** is also a potential pattern
           // so we can already test it here
-          const nextMatches = pattern.test(nextIndex, entry.name);
           if (nextMatches && nextIndex === last && !isLast) {
             // If next pattern is the last one, add to results
-            if (!this.#results.has(entryPath)) {
-              if (this.#results.add(entryPath)) {
-                yield this.#withFileTypes ? entry : entryPath;
-              }
+            if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
+              yield this.#withFileTypes ? entry : entryPath;
             }
           } else if (nextMatches && entry.isDirectory()) {
             // Pattern matched, meaning two patterns forward

filename: test/parallel/test-fs-glob.mjs
patch: @@ -30,6 +30,8 @@ async function setup() {
     'a/cb/e/f',
     'a/x/.y/b',
     'a/z/.y/b',
+    'a/.b',
+    'a/b/.b',
   ].map((f) => resolve(fixtureDir, f));
 
   const symlinkTo = resolve(fixtureDir, 'a/symlink/a/b/c');
@@ -188,6 +190,9 @@ const patterns = {
   ],
   '*/*/*/f': ['a/bc/e/f', 'a/cb/e/f'],
   './**/f': ['a/bc/e/f', 'a/cb/e/f'],
+  '**/.b': ['a/.b', 'a/b/.b'],
+  './**/.b': ['a/.b', 'a/b/.b'],
+  'a/**/.b': ['a/.b', 'a/b/.b'],
   'a/symlink/a/b/c/a/b/c/a/b/c//a/b/c////a/b/c/**/b/c/**': common.isWindows ? [] : [
     'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c',
     'a/symlink/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a/b/c/a',