Skip to content

Commit

Permalink
feat(node/path): support matchesGlob (#15917)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac authored Jan 6, 2025
1 parent 8d82302 commit 189684f
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
46 changes: 46 additions & 0 deletions src/js/node/path.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Hardcoded module "node:path"
const { validateString } = require("internal/validators");

const [bindingPosix, bindingWin32] = $cpp("Path.cpp", "createNodePathBinding");
const toNamespacedPathPosix = bindingPosix.toNamespacedPath.bind(bindingPosix);
const toNamespacedPathWin32 = bindingWin32.toNamespacedPath.bind(bindingWin32);
Expand Down Expand Up @@ -40,4 +42,48 @@ const win32 = {
};
posix.win32 = win32.win32 = win32;
posix.posix = posix;

type Glob = import("bun").Glob;

let LazyGlob: Glob | undefined;
function loadGlob(): LazyGlob {
LazyGlob = require("bun").Glob;
}

// the most-recently used glob is memoized in case `matchesGlob` is called in a
// loop with the same pattern
let prevGlob: Glob | undefined;
let prevPattern: string | undefined;
function matchesGlob(isWindows, path, pattern) {
let glob: Glob;

validateString(path, "path");
if (isWindows) path = path.replaceAll("\\", "/");

if (prevGlob) {
$assert(prevPattern !== undefined);
if (prevPattern === pattern) {
glob = prevGlob;
} else {
if (LazyGlob === undefined) loadGlob();
validateString(pattern, "pattern");
if (isWindows) pattern = pattern.replaceAll("\\", "/");
glob = prevGlob = new LazyGlob(pattern);
prevPattern = pattern;
}
} else {
loadGlob(); // no prevGlob implies LazyGlob isn't loaded
validateString(pattern, "pattern");
if (isWindows) pattern = pattern.replaceAll("\\", "/");
glob = prevGlob = new LazyGlob(pattern);
prevPattern = pattern;
}

return glob.match(path);
}

// posix.matchesGlob = win32.matchesGlob = matchesGlob;
posix.matchesGlob = matchesGlob.bind(null, false);
win32.matchesGlob = matchesGlob.bind(null, true);

export default process.platform === "win32" ? win32 : posix;
7 changes: 7 additions & 0 deletions test/js/bun/glob/match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,13 @@ describe("Glob.match", () => {
expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue();
});

test("square braces", () => {
expect(new Glob("src/*.[tj]s").match("src/foo.js")).toBeTrue();
expect(new Glob("src/*.[tj]s").match("src/foo.ts")).toBeTrue();
expect(new Glob("foo/ba[rz].md").match("foo/bar.md")).toBeTrue();
expect(new Glob("foo/ba[rz].md").match("foo/baz.md")).toBeTrue();
});

test("bash wildmatch", () => {
expect(new Glob("a[]-]b").match("aab")).toBeFalse();
expect(new Glob("[ten]").match("ten")).toBeFalse();
Expand Down
78 changes: 78 additions & 0 deletions test/js/node/path/matches-glob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import path from "path";

describe("path.matchesGlob(path, glob)", () => {
const stringLikeObject = {
toString() {
return "hi";
},
};

it.each([
// line break
null,
undefined,
123,
stringLikeObject,
Symbol("hi"),
])("throws if `path` is not a string", (notAString: any) => {
expect(() => path.matchesGlob(notAString, "*")).toThrow(TypeError);
});

it.each([
// line break
null,
undefined,
123,
stringLikeObject,
Symbol("hi"),
])("throws if `glob` is not a string", (notAString: any) => {
expect(() => path.matchesGlob("hi", notAString)).toThrow(TypeError);
});
});

describe("path.posix.matchesGlob(path, glob)", () => {
it.each([
// line break
["foo.js", "*.js"],
["foo.js", "*.[tj]s"],
["foo.ts", "*.[tj]s"],
["foo.js", "**/*.js"],
["src/bar/foo.js", "**/*.js"],
["foo/bar/baz", "foo/[bcr]ar/baz"],
])("path '%s' matches pattern '%s'", (pathname, glob) => {
expect(path.posix.matchesGlob(pathname, glob)).toBeTrue();
});
it.each([
// line break
["foo.js", "*.ts"],
["src/foo.js", "*.js"],
["foo.js", "src/*.js"],
["foo/bar", "*"],
])("path '%s' does not match pattern '%s'", (pathname, glob) => {
expect(path.posix.matchesGlob(pathname, glob)).toBeFalse();
});
});

describe("path.win32.matchesGlob(path, glob)", () => {
it.each([
// line break
["foo.js", "*.js"],
["foo.js", "*.[tj]s"],
["foo.ts", "*.[tj]s"],
["foo.js", "**\\*.js"],
["src\\bar\\foo.js", "**\\*.js"],
["src\\bar\\foo.js", "**/*.js"],
["foo\\bar\\baz", "foo\\[bcr]ar\\baz"],
["foo\\bar\\baz", "foo/[bcr]ar/baz"],
])("path '%s' matches gattern '%s'", (pathname, glob) => {
expect(path.win32.matchesGlob(pathname, glob)).toBeTrue();
});
it.each([
// line break
["foo.js", "*.ts"],
["foo.js", "src\\*.js"],
["foo/bar", "*"],
])("path '%s' does not match pattern '%s'", (pathname, glob) => {
expect(path.win32.matchesGlob(pathname, glob)).toBeFalse();
});
});
44 changes: 44 additions & 0 deletions test/js/node/test/parallel/test-path-glob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

require('../common');
const assert = require('assert');
const path = require('path');

const globs = {
win32: [
['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar'
['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between
['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1'
['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5'
['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx'
['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar'
['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo'
['foo\\bar\\baz', '*', false], // No match
],
posix: [
['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar'
['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between
['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1'
['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5'
['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx'
['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar'
['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo'
['foo/bar/baz', '*', false], // No match
],
};


for (const [platform, platformGlobs] of Object.entries(globs)) {
for (const [pathStr, glob, expected] of platformGlobs) {
const actual = path[platform].matchesGlob(pathStr, glob);
assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`);
}
}

// Test for non-string input
assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/);
assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/);

0 comments on commit 189684f

Please sign in to comment.