Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node/path): support matchesGlob #15917

Merged
merged 11 commits into from
Jan 6, 2025
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.*/);
Loading