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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
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
79 changes: 79 additions & 0 deletions test/js/node/path/matches-glob.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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"],
["foo.js", "**\\*.js"],
["src/bar/foo.js", "**/*.js"],
["foo/bar/baz", "foo/[bcr]ar/baz"],
])("%s matches %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", "*"],
])("%s does not match %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"],
])("%s matches %s", (pathname, glob) => {
expect(path.win32.matchesGlob(pathname, glob)).toBeTrue();
});
it.each([
// line break
["foo.js", "*.ts"],
["foo.js", "src\\*.js"],
["foo/bar", "*"],
])("%s does not match %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