├── .gitattributes ├── default.project.json ├── .robloxrc ├── Packages └── .robloxrc ├── src ├── init.lua ├── __tests__ │ ├── support │ │ ├── init.lua │ │ └── match.lua │ ├── options.noglobstar.spec.lua │ ├── parens.spec.lua │ ├── options.noextglob.spec.lua │ ├── brackets.spec.lua │ ├── options.expandRange.spec.lua │ ├── malicious.spec.lua │ ├── non-globs.spec.lua │ ├── issue-related.spec.lua │ ├── wildmat.spec.lua │ ├── minimatch.spec.lua │ ├── options.format.spec.lua │ ├── options.ignore.spec.lua │ ├── options.onMatch.spec.lua │ ├── qmarks.spec.lua │ ├── braces.spec.lua │ ├── negation.spec.lua │ ├── options.spec.lua │ ├── stars.spec.lua │ ├── regex-features.spec.lua │ ├── api.picomatch.spec.lua │ ├── bash.spec.lua │ ├── posix-classes.spec.lua │ └── dotfiles.spec.lua ├── stringUtils.lua ├── utils.lua ├── constants.lua ├── scan.lua └── picomatch.lua ├── tests.project.json ├── foreman.toml ├── CHANGELOG.md ├── README.md ├── rotriever.toml ├── bin ├── spec.lua └── ci.sh ├── selene.toml ├── .vscode └── launch.sample.json ├── .gitignore ├── LICENSE.txt └── testez.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lua linguist-language=Luau 2 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Picomatch", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.robloxrc: -------------------------------------------------------------------------------- 1 | { 2 | "language": { 3 | "mode": "strict" 4 | }, 5 | "lint": { 6 | "*": "enabled" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Packages/.robloxrc: -------------------------------------------------------------------------------- 1 | { 2 | "language": { 3 | "mode": "nocheck" 4 | }, 5 | "lint": { 6 | "*": "disabled" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/blob/2.3.1/index.js 2 | 3 | return require(script.picomatch) 4 | -------------------------------------------------------------------------------- /tests.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PicomatchTestModel", 3 | "tree": { 4 | "$className": "Folder", 5 | "Packages": { 6 | "$path": "Packages", 7 | "Dev": { 8 | "$path": "Packages/Dev" 9 | }, 10 | "Picomatch": { 11 | "$path": "src" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rotrieve = { source = "roblox/rotriever", version = "0.5.5" } 3 | rojo = { source = "Roblox/rojo-rbx-rojo", version = "7.2.1" } 4 | selene = { source = "Roblox/Kampfkarren-selene", version = "0.20.0" } 5 | stylua = { source = "Roblox/JohnnyMorganz-StyLua", version = "=0.14.3" } 6 | rbx-aged-cli = { source = "Roblox/rbx-aged-tool", version = "=5.7.9" } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Picomatch Changelog 2 | 3 | ## 0.4.0 4 | 5 | * upgrade LuauPolyfill to v1 6 | 7 | ## 0.3.1 8 | 9 | * fix translation error in parse logic ([#4](https://github.com/Roblox/picomatch-lua/pull/4)) 10 | 11 | ## 0.3.0 12 | 13 | * :hammer_and_wrench: update LuauPolyfill version to [v0.4.0](https://github.com/Roblox/luau-polyfill/blob/v0.4.0/CHANGELOG.md#040) ([#3](https://github.com/Roblox/picomatch-lua/pull/3)) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # picomatch 2 | 3 | Status: :heavy_check_mark: Ported 4 | 5 | Source: https://github.com/micromatch/picomatch/tree/2.3.1 6 | 7 | Version: v2.3.1 8 | 9 | --- 10 | 11 | ### :pencil2: Notes 12 | 13 | ### :x: Excluded 14 | 15 | 16 | ### :package: [Dependencies](https://github.com/micromatch/picomatch/blob/2.3.1/package.json) 17 | 18 | | Package | Version | Status | Notes | 19 | | ------- | ------- | ------ | ----- | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rotriever.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Picomatch" 3 | version = "0.4.0" 4 | authors = [ 5 | "Chris Trześniewski ", 6 | ] 7 | 8 | content_root = "src" 9 | 10 | files = ["*", "!**/__tests__/**"] 11 | 12 | [dependencies] 13 | LuauPolyfill = "github.com/roblox/luau-polyfill@1" 14 | Promise = "github.com/roblox/roblox-lua-promise@3.3.0" 15 | RegExp = "github.com/roblox/luau-regexp@0.2.0" 16 | 17 | [dev_dependencies] 18 | JestGlobals = "github.com/roblox/jest-roblox@2" 19 | -------------------------------------------------------------------------------- /bin/spec.lua: -------------------------------------------------------------------------------- 1 | local ProcessService = game:GetService("ProcessService") 2 | local Packages = script.Parent.PicomatchTestModel.Packages 3 | 4 | local JestGlobals = require(Packages.Dev.JestGlobals) 5 | 6 | local TestEZ = JestGlobals.TestEZ 7 | 8 | -- Run all tests, collect results, and report to stdout. 9 | local result = TestEZ.TestBootstrap:run( 10 | { Packages.Picomatch }, 11 | TestEZ.Reporters.TextReporterQuiet 12 | ) 13 | 14 | if result.failureCount == 0 and #result.errors == 0 then 15 | ProcessService:ExitAsync(0) 16 | else 17 | ProcessService:ExitAsync(1) 18 | end 19 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox+testez" 2 | 3 | [config] 4 | empty_if = { comments_count = true } 5 | 6 | [rules] 7 | # remove this once the feature request here is implemented: https://github.com/Kampfkarren/selene/issues/181 8 | global_usage = "allow" 9 | # remove when the Luau type narrowing issues (and the workarounds) are resolved 10 | shadowing = "allow" 11 | # standard library functions are overridden for fake timers, also complains about internal services 12 | incorrect_standard_library_use = "allow" 13 | # prefer to align to upstream for readability 14 | multiple_statements = "allow" 15 | unused_variable = "allow" 16 | if_same_then_else = "allow" -------------------------------------------------------------------------------- /.vscode/launch.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "roblox-lrdb", 6 | "request": "launch", 7 | "name": "Luau tests", 8 | "program": "", 9 | "args": [ 10 | "run", 11 | "--load.model", 12 | "tests.project.json", 13 | "--debug.on", 14 | "--run", 15 | "bin/spec.lua", 16 | "--fastFlags.allOnLuau", 17 | "--fastFlags.overrides", 18 | "EnableLoadModule=true", 19 | "DebugDisableOptimizedBytecode=true" 20 | ], 21 | "cwd": "${workspaceFolder}", 22 | "stopOnEntry": true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Roblox 2 | /*.rbxl 3 | /*.rbxl.lock 4 | /*.rbxlx 5 | /*.rbxlx.lock 6 | /*.rbxm 7 | /*.rbxmx 8 | /*.sig 9 | roblox.toml 10 | 11 | # dependencies 12 | Packages/* 13 | !Packages/.robloxrc 14 | .DS_Store 15 | debug.log 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | !.vscode/launch.sample.json 33 | 34 | 35 | # misc 36 | /luacov.* 37 | /lcov* 38 | /site 39 | /rotriever.lock 40 | /output.txt 41 | .vscode/launch.json 42 | cachegrind.out.* 43 | callgrind.out.* 44 | -------------------------------------------------------------------------------- /src/__tests__/support/init.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/support/index.js 2 | 3 | -- ROBLOX FIXME: need a way to mock 4 | local path = { sep = "/" } -- require("path") 5 | -- Don't use "path.sep" here, as it's conceivable that it might have been 6 | -- modified somewhere by the user. Node.js only handles these two path separators 7 | -- with similar logic, and this is only for unit tests, so we should be fine. 8 | -- ROBLOX FIXME: need a test for platform 9 | local isWindows = false 10 | local sep = isWindows and "\\" or "/" 11 | return { 12 | windowsPathSep = function() 13 | path.sep = "\\" 14 | end, 15 | resetPathSep = function() 16 | path.sep = sep 17 | end, 18 | } 19 | -------------------------------------------------------------------------------- /src/__tests__/options.noglobstar.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.noglobstar.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("options.noglobstar", function() 9 | it("should disable extglob support when options.noglobstar is true", function() 10 | assert(isMatch("a/b/c", "**", { noglobstar = false })) 11 | assert(not isMatch("a/b/c", "**", { noglobstar = true })) 12 | assert(isMatch("a/b/c", "a/**", { noglobstar = false })) 13 | assert(not isMatch("a/b/c", "a/**", { noglobstar = true })) 14 | end) 15 | end) 16 | end 17 | -------------------------------------------------------------------------------- /bin/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | echo "Remove .robloxrc from dev dependencies" 6 | find Packages/Dev -name "*.robloxrc" | xargs rm -f 7 | find Packages/_Index -name "*.robloxrc" | xargs rm -f 8 | 9 | echo "Run static analysis" 10 | selene src 11 | roblox-cli analyze tests.project.json 12 | stylua -c src 13 | 14 | echo "Run tests" 15 | roblox-cli run --load.model tests.project.json --run bin/spec.lua --testService.errorExitCode=1 --fastFlags.allOnLuau --fastFlags.overrides EnableLoadModule=true 16 | 17 | # run the following command to update new snapshots 18 | # roblox-cli run --load.model tests.project.json --run bin/spec.lua --lua.globals=__DEV__=true --fastFlags.allOnLuau --fastFlags.overrides "UseDateTimeType3=true" "EnableLoadModule=true" "EnableDelayedTaskMethods=true" --load.asRobloxScript --fs.readwrite="$(pwd)" --lua.globals=UPDATESNAPSHOT="new" 19 | -------------------------------------------------------------------------------- /src/__tests__/parens.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/parens.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("parens (non-extglobs)", function() 9 | it("should support stars following parens", function() 10 | assert(isMatch("a", "(a)*")) 11 | assert(isMatch("az", "(a)*")) 12 | assert(not isMatch("zz", "(a)*")) 13 | assert(isMatch("ab", "(a|b)*")) 14 | assert(isMatch("abc", "(a|b)*")) 15 | assert(isMatch("aa", "(a)*")) 16 | assert(isMatch("aaab", "(a|b)*")) 17 | assert(isMatch("aaabbb", "(a|b)*")) 18 | end) 19 | 20 | it("should not match slashes with single stars", function() 21 | assert(not isMatch("a/b", "(a)*")) 22 | assert(not isMatch("a/b", "(a|b)*")) 23 | end) 24 | end) 25 | end 26 | -------------------------------------------------------------------------------- /src/__tests__/options.noextglob.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.noextglob.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("options.noextglob", function() 9 | it("should disable extglob support when options.noextglob is true", function() 10 | assert(isMatch("a+z", "a+(z)", { noextglob = true })) 11 | assert(not isMatch("az", "a+(z)", { noextglob = true })) 12 | assert(not isMatch("azz", "a+(z)", { noextglob = true })) 13 | assert(not isMatch("azzz", "a+(z)", { noextglob = true })) 14 | end) 15 | 16 | it("should work with noext alias to support minimatch", function() 17 | assert(isMatch("a+z", "a+(z)", { noext = true })) 18 | assert(not isMatch("az", "a+(z)", { noext = true })) 19 | assert(not isMatch("azz", "a+(z)", { noext = true })) 20 | assert(not isMatch("azzz", "a+(z)", { noext = true })) 21 | end) 22 | end) 23 | end 24 | -------------------------------------------------------------------------------- /src/__tests__/brackets.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/brackets.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("brackets", function() 9 | describe("trailing stars", function() 10 | itFIXME("should support stars following brackets", function() 11 | assert(isMatch("a", "[a]*")) 12 | assert(isMatch("aa", "[a]*")) 13 | assert(isMatch("aaa", "[a]*")) 14 | assert(isMatch("az", "[a-z]*")) 15 | assert(isMatch("zzz", "[a-z]*")) 16 | end) 17 | 18 | itFIXME("should match slashes defined in brackets", function() 19 | assert(isMatch("foo/bar", "foo[/]bar")) 20 | assert(isMatch("foo/bar/", "foo[/]bar[/]")) 21 | assert(isMatch("foo/bar/baz", "foo[/]bar[/]baz")) 22 | end) 23 | 24 | it("should not match slashes following brackets", function() 25 | assert(not isMatch("a/b", "[a]*")) 26 | end) 27 | end) 28 | end) 29 | end 30 | -------------------------------------------------------------------------------- /src/__tests__/options.expandRange.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.expandRange.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | -- ROBLOX deviation: not supported in Lua 8 | -- local fill = require("fill-range") 9 | local isMatch = require(PicomatchModule).isMatch 10 | describe("options.expandRange", function() 11 | it("should support a custom function for expanding ranges in brace patterns", function() 12 | assert(isMatch("a/c", "a/{a..c}", { 13 | expandRange = function(a, b) 14 | return ("([%s-%s])"):format(tostring(a), tostring(b)) 15 | end, 16 | })) 17 | assert(not isMatch("a/z", "a/{a..c}", { 18 | expandRange = function(a, b) 19 | return ("([%s-%s])"):format(tostring(a), tostring(b)) 20 | end, 21 | })) 22 | -- ROBLOX deviation START: not supported in Lua 23 | -- assert(isMatch("a/99", "a/{1..100}", { 24 | -- expandRange = function(self, a, b) 25 | -- return ("(%s)"):format(fill(a, b, { toRegex = true })) 26 | -- end, 27 | -- })) 28 | -- ROBLOX deviation END 29 | end) 30 | end) 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Roblox Corporation 2020-2023 4 | 5 | Copyright (c) 2017-present, Jon Schlinkert. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /testez.toml: -------------------------------------------------------------------------------- 1 | [[afterAll.args]] 2 | type = "function" 3 | 4 | [[afterEach.args]] 5 | type = "function" 6 | 7 | [[beforeAll.args]] 8 | type = "function" 9 | 10 | [[beforeEach.args]] 11 | type = "function" 12 | 13 | [[describe.args]] 14 | type = "string" 15 | 16 | [[describe.args]] 17 | type = "function" 18 | 19 | [[describeFOCUS.args]] 20 | type = "string" 21 | 22 | [[describeFOCUS.args]] 23 | type = "function" 24 | 25 | [[describeSKIP.args]] 26 | type = "string" 27 | 28 | [[describeSKIP.args]] 29 | type = "function" 30 | 31 | [[expect.args]] 32 | type = "any" 33 | 34 | [[FIXME.args]] 35 | type = "string" 36 | required = false 37 | 38 | [FOCUS] 39 | args = [] 40 | 41 | [[it.args]] 42 | type = "string" 43 | 44 | [[it.args]] 45 | type = "function" 46 | 47 | [[itFIXME.args]] 48 | type = "string" 49 | 50 | [[itFIXME.args]] 51 | type = "function" 52 | 53 | [[itFOCUS.args]] 54 | type = "string" 55 | 56 | [[itFOCUS.args]] 57 | type = "function" 58 | 59 | [[fit.args]] 60 | type = "string" 61 | 62 | [[fit.args]] 63 | type = "function" 64 | 65 | [[itSKIP.args]] 66 | type = "string" 67 | 68 | [[itSKIP.args]] 69 | type = "function" 70 | 71 | [[xit.args]] 72 | type = "string" 73 | 74 | [[xit.args]] 75 | type = "function" 76 | 77 | [SKIP] 78 | args = [] 79 | -------------------------------------------------------------------------------- /src/__tests__/support/match.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/support/match.js 2 | 3 | local CurrentModule = script.Parent 4 | local PicomatchModule = CurrentModule.Parent.Parent 5 | local Packages = PicomatchModule.Parent 6 | local LuauPolyfill = require(Packages.LuauPolyfill) 7 | local Boolean = LuauPolyfill.Boolean 8 | local Set = LuauPolyfill.Set 9 | 10 | type Object = LuauPolyfill.Object 11 | 12 | local picomatch = require(PicomatchModule) 13 | 14 | return function(list, pattern, options_: Object?) 15 | local options: Object = options_ or {} 16 | local isMatch = picomatch(pattern, options, true) 17 | local matches = Boolean.toJSBoolean(options.matches) and options.matches or Set.new() 18 | 19 | for _, item in ipairs(list) do 20 | local match = isMatch(item, true) 21 | 22 | if match and match.output and match.isMatch == true then 23 | matches:add(match.output) 24 | end 25 | end 26 | 27 | --[[ 28 | ROBLOX deviation START: using Set:ipairs because Array.concat doesn't work with Set 29 | original code: 30 | return [...matches] 31 | ]] 32 | local result = {} 33 | for _, value in matches:ipairs() do 34 | table.insert(result, value) 35 | end 36 | return result 37 | -- ROBLOX deviation: END 38 | end 39 | -------------------------------------------------------------------------------- /src/stringUtils.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX NOTE: no upstream 2 | 3 | -- ROBLOX TODO: implement in LuauPolyfill 4 | 5 | local CurrentModule = script.Parent 6 | local Packages = CurrentModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Array = LuauPolyfill.Array 9 | type Array = LuauPolyfill.Array 10 | 11 | local RegExp = require(Packages.RegExp) 12 | type RegExp = RegExp.RegExp 13 | 14 | local function stringReplace(str: string, regExp: RegExp, replFn: (...any) -> string) 15 | local v = str 16 | local match = regExp:exec(v) 17 | local offset = 0 18 | local replaceArr = {} 19 | while match ~= nil and match.index ~= nil do 20 | -- ROBLOX FIXME Luau: analyze complains about match type being `Array & {| index: number?, input: string?, n: number |}` instead of table 21 | local m = (match :: Array)[1] 22 | local args: Array = Array.slice(match, 1, match.n + 1) 23 | -- ROBLOX FIXME Luau: analyze doesn't recognize match.index as a number 24 | local index = (match.index :: any) + offset 25 | 26 | table.insert(args, index) 27 | 28 | local replace = replFn(table.unpack(args)) 29 | 30 | table.insert(replaceArr, { 31 | from = index, 32 | length = #m, 33 | value = replace, 34 | }) 35 | 36 | -- ROBLOX FIXME Luau: analyze doesn't recognize match.index as a number 37 | offset += #m + (match.index :: any) - 1 38 | v = str:sub(offset + 1) 39 | match = regExp:exec(v) 40 | end 41 | local result = str:sub(1) 42 | for _, rep in ipairs(Array.reverse(replaceArr)) do 43 | local from, length, value = rep.from, rep.length, rep.value 44 | local prefix = result:sub(1, from - 1) 45 | local suffix = result:sub(from + length) 46 | 47 | result = prefix .. value .. suffix 48 | end 49 | 50 | return result 51 | end 52 | 53 | return { 54 | stringReplace = stringReplace, 55 | } 56 | -------------------------------------------------------------------------------- /src/__tests__/malicious.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/malicious.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | 8 | local jestExpect = require(Packages.Dev.JestGlobals).expect 9 | 10 | local isMatch = require(PicomatchModule).isMatch 11 | local function repeat_(n) 12 | return string.rep("\\", n) 13 | end 14 | --[[* 15 | * These tests are based on minimatch unit tests 16 | ]] 17 | describe("handling of potential regex exploits", function() 18 | it("should support long escape sequences", function() 19 | -- ROBLOX FIXME: need a test for platform 20 | local isWindows = false 21 | if isWindows then 22 | assert(isMatch("\\A", ("%sA"):format(repeat_(65500))), "within the limits, and valid match") 23 | end 24 | assert(isMatch("A", ("!%sA"):format(repeat_(65500))), "within the limits, and valid match") 25 | assert(isMatch("A", ("!(%sA)"):format(repeat_(65500))), "within the limits, and valid match") 26 | assert(not isMatch("A", ("[!(%sA"):format(repeat_(65500))), "within the limits, but invalid regex") 27 | end) 28 | 29 | it("should throw an error when the pattern is too long", function() 30 | jestExpect(function() 31 | return isMatch("foo", string.rep("*", 65537)) 32 | end).toThrowError("exceeds maximum allowed") 33 | jestExpect(function() 34 | assert(not isMatch("A", ("!(%sA)"):format(repeat_(65536)))) 35 | end).toThrowError("Input length: 65540, exceeds maximum allowed length: 65536") 36 | end) 37 | 38 | it("should allow max bytes to be customized", function() 39 | jestExpect(function() 40 | assert(not isMatch("A", ("!(%sA)"):format(repeat_(500)), { maxLength = 499 })) 41 | end).toThrowError("Input length: 504, exceeds maximum allowed length: 499") 42 | end) 43 | end) 44 | end 45 | -------------------------------------------------------------------------------- /src/__tests__/non-globs.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/non-globs.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local support = require(CurrentModule.support) 8 | local isMatch = require(PicomatchModule).isMatch 9 | describe("non-globs", function() 10 | beforeAll(function() 11 | return support.resetPathSep() 12 | end) 13 | 14 | afterAll(function() 15 | return support.resetPathSep() 16 | end) 17 | 18 | afterEach(function() 19 | return support.resetPathSep() 20 | end) 21 | 22 | it("should match non-globs", function() 23 | assert(not isMatch("/ab", "/a")) 24 | assert(not isMatch("a/a", "a/b")) 25 | assert(not isMatch("a/a", "a/c")) 26 | assert(not isMatch("a/b", "a/c")) 27 | assert(not isMatch("a/c", "a/b")) 28 | assert(not isMatch("aaa", "aa")) 29 | assert(not isMatch("ab", "/a")) 30 | assert(not isMatch("ab", "a")) 31 | assert(isMatch("/a", "/a")) 32 | assert(isMatch("/a/", "/a/")) 33 | assert(isMatch("/a/a", "/a/a")) 34 | assert(isMatch("/a/a/", "/a/a/")) 35 | assert(isMatch("/a/a/a", "/a/a/a")) 36 | assert(isMatch("/a/a/a/", "/a/a/a/")) 37 | assert(isMatch("/a/a/a/a", "/a/a/a/a")) 38 | assert(isMatch("/a/a/a/a/a", "/a/a/a/a/a")) 39 | assert(isMatch("a", "a")) 40 | assert(isMatch("a/", "a/")) 41 | assert(isMatch("a/a", "a/a")) 42 | assert(isMatch("a/a/", "a/a/")) 43 | assert(isMatch("a/a/a", "a/a/a")) 44 | assert(isMatch("a/a/a/", "a/a/a/")) 45 | assert(isMatch("a/a/a/a", "a/a/a/a")) 46 | assert(isMatch("a/a/a/a/a", "a/a/a/a/a")) 47 | end) 48 | 49 | it("should match literal dots", function() 50 | assert(isMatch(".", ".")) 51 | assert(isMatch("..", "..")) 52 | assert(not isMatch("...", "..")) 53 | assert(isMatch("...", "...")) 54 | assert(isMatch("....", "....")) 55 | assert(not isMatch("....", "...")) 56 | end) 57 | 58 | it("should handle escaped characters as literals", function() 59 | assert(not isMatch("abc", "abc\\*")) 60 | assert(isMatch("abc*", "abc\\*")) 61 | end) 62 | 63 | itFIXME("should match windows paths", function() 64 | support.windowsPathSep() 65 | assert(isMatch("aaa\\bbb", "aaa/bbb")) 66 | assert(isMatch("aaa/bbb", "aaa/bbb")) 67 | support.resetPathSep() 68 | end) 69 | end) 70 | end 71 | -------------------------------------------------------------------------------- /src/__tests__/issue-related.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/issue-related.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("issue-related tests", function() 9 | it("should match with braces (see picomatch/issues#8)", function() 10 | assert(isMatch("directory/.test.txt", "{file.txt,directory/**/*}", { dot = true })) 11 | assert(isMatch("directory/test.txt", "{file.txt,directory/**/*}", { dot = true })) 12 | assert(not isMatch("directory/.test.txt", "{file.txt,directory/**/*}")) 13 | assert(isMatch("directory/test.txt", "{file.txt,directory/**/*}")) 14 | end) 15 | 16 | it("should match Japanese characters (see micromatch/issues#127)", function() 17 | assert(isMatch("フォルダ/aaa.js", "フ*/**/*")) 18 | assert(isMatch("フォルダ/aaa.js", "フォ*/**/*")) 19 | assert(isMatch("フォルダ/aaa.js", "フォル*/**/*")) 20 | assert(isMatch("フォルダ/aaa.js", "フ*ル*/**/*")) 21 | assert(isMatch("フォルダ/aaa.js", "フォルダ/**/*")) 22 | end) 23 | 24 | it("micromatch issue#15", function() 25 | assert(isMatch("a/b-c/d/e/z.js", "a/b-*/**/z.js")) 26 | assert(isMatch("z.js", "z*")) 27 | assert(isMatch("z.js", "**/z*")) 28 | assert(isMatch("z.js", "**/z*.js")) 29 | assert(isMatch("z.js", "**/*.js")) 30 | assert(isMatch("foo", "**/foo")) 31 | end) 32 | 33 | it("micromatch issue#23", function() 34 | assert(not isMatch("zzjs", "z*.js")) 35 | assert(not isMatch("zzjs", "*z.js")) 36 | end) 37 | 38 | it("micromatch issue#24", function() 39 | assert(not isMatch("a/b/c/d/", "a/b/**/f")) 40 | assert(isMatch("a", "a/**")) 41 | assert(isMatch("a", "**")) 42 | assert(isMatch("a/", "**")) 43 | assert(isMatch("a/b/c/d", "**")) 44 | assert(isMatch("a/b/c/d/", "**")) 45 | assert(isMatch("a/b/c/d/", "**/**")) 46 | assert(isMatch("a/b/c/d/", "**/b/**")) 47 | assert(isMatch("a/b/c/d/", "a/b/**")) 48 | assert(isMatch("a/b/c/d/", "a/b/**/")) 49 | assert(isMatch("a/b/c/d/e.f", "a/b/**/**/*.*")) 50 | assert(isMatch("a/b/c/d/e.f", "a/b/**/*.*")) 51 | assert(isMatch("a/b/c/d/g/e.f", "a/b/**/d/**/*.*")) 52 | assert(isMatch("a/b/c/d/g/g/e.f", "a/b/**/d/**/*.*")) 53 | end) 54 | 55 | itFIXME("micromatch issue#58 - only match nested dirs when `**` is the only thing in a segment", function() 56 | assert(not isMatch("a/b/c", "a/b**")) 57 | assert(not isMatch("a/c/b", "a/**b")) 58 | end) 59 | 60 | it("micromatch issue#79", function() 61 | assert(isMatch("a/foo.js", "**/foo.js")) 62 | assert(isMatch("foo.js", "**/foo.js")) 63 | assert(isMatch("a/foo.js", "**/foo.js", { dot = true })) 64 | assert(isMatch("foo.js", "**/foo.js", { dot = true })) 65 | end) 66 | end) 67 | end 68 | -------------------------------------------------------------------------------- /src/__tests__/wildmat.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/wildmat.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("Wildmat (git) tests", function() 9 | it("Basic wildmat features", function() 10 | assert(not isMatch("foo", "*f")) 11 | assert(not isMatch("foo", "??")) 12 | assert(not isMatch("foo", "bar")) 13 | assert(not isMatch("foobar", "foo\\*bar")) 14 | assert(isMatch("?a?b", "\\??\\?b")) 15 | assert(isMatch("aaaaaaabababab", "*ab")) 16 | assert(isMatch("foo", "*")) 17 | assert(isMatch("foo", "*foo*")) 18 | assert(isMatch("foo", "???")) 19 | assert(isMatch("foo", "f*")) 20 | assert(isMatch("foo", "foo")) 21 | assert(isMatch("foobar", "*ob*a*r*")) 22 | end) 23 | 24 | it("should support recursion", function() 25 | assert( 26 | not isMatch( 27 | "-adobe-courier-bold-o-normal--12-120-75-75-/-70-iso8859-1", 28 | "-*-*-*-*-*-*-12-*-*-*-m-*-*-*" 29 | ) 30 | ) 31 | assert( 32 | not isMatch( 33 | "-adobe-courier-bold-o-normal--12-120-75-75-X-70-iso8859-1", 34 | "-*-*-*-*-*-*-12-*-*-*-m-*-*-*" 35 | ) 36 | ) 37 | assert(not isMatch("ab/cXd/efXg/hi", "*X*i")) 38 | assert(not isMatch("ab/cXd/efXg/hi", "*Xg*i")) 39 | assert(not isMatch("abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txtz", "**/*a*b*g*n*t")) 40 | assert(not isMatch("foo", "*/*/*")) 41 | assert(not isMatch("foo", "fo")) 42 | assert(not isMatch("foo/bar", "*/*/*")) 43 | assert(not isMatch("foo/bar", "foo?bar")) 44 | assert(not isMatch("foo/bb/aa/rr", "*/*/*")) 45 | assert(not isMatch("foo/bba/arr", "foo*")) 46 | assert(not isMatch("foo/bba/arr", "foo**")) 47 | assert(not isMatch("foo/bba/arr", "foo/*")) 48 | -- ROBLOX FIXME 49 | -- assert(not isMatch("foo/bba/arr", "foo/**arr")) 50 | assert(not isMatch("foo/bba/arr", "foo/**z")) 51 | assert(not isMatch("foo/bba/arr", "foo/*arr")) 52 | assert(not isMatch("foo/bba/arr", "foo/*z")) 53 | assert( 54 | not isMatch( 55 | "XXX/adobe/courier/bold/o/normal//12/120/75/75/X/70/iso8859/1", 56 | "XXX/*/*/*/*/*/*/12/*/*/*/m/*/*/*" 57 | ) 58 | ) 59 | assert( 60 | isMatch("-adobe-courier-bold-o-normal--12-120-75-75-m-70-iso8859-1", "-*-*-*-*-*-*-12-*-*-*-m-*-*-*") 61 | ) 62 | assert(isMatch("ab/cXd/efXg/hi", "**/*X*/**/*i")) 63 | assert(isMatch("ab/cXd/efXg/hi", "*/*X*/*/*i")) 64 | assert(isMatch("abcd/abcdefg/abcdefghijk/abcdefghijklmnop.txt", "**/*a*b*g*n*t")) 65 | assert(isMatch("abcXdefXghi", "*X*i")) 66 | assert(isMatch("foo", "foo")) 67 | assert(isMatch("foo/bar", "foo/*")) 68 | assert(isMatch("foo/bar", "foo/bar")) 69 | -- ROBLOX FIXME 70 | -- assert(isMatch("foo/bar", "foo[/]bar")) 71 | assert(isMatch("foo/bb/aa/rr", "**/**/**")) 72 | assert(isMatch("foo/bba/arr", "*/*/*")) 73 | assert(isMatch("foo/bba/arr", "foo/**")) 74 | end) 75 | end) 76 | end 77 | -------------------------------------------------------------------------------- /src/__tests__/minimatch.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/minimatch.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local function format(str) 8 | return str:gsub("^%./", "", 1) 9 | end 10 | local Picomatch = require(PicomatchModule) 11 | local isMatch, makeRe = Picomatch.isMatch, Picomatch.makeRe 12 | describe("minimatch parity:", function() 13 | describe("minimatch issues (as of 12/7/2016)", function() 14 | it("https://github.com/isaacs/minimatch/issues/29", function() 15 | assert(isMatch("foo/bar.txt", "foo/**/*.txt")) 16 | assert(makeRe("foo/**/*.txt"):test("foo/bar.txt")) 17 | assert(not isMatch("n/!(axios)/**", "n/axios/a.js")) 18 | assert(not makeRe("n/!(axios)/**"):test("n/axios/a.js")) 19 | end) 20 | 21 | it("https://github.com/isaacs/minimatch/issues/30", function() 22 | assert(isMatch("foo/bar.js", "**/foo/**", { format = format })) 23 | assert(isMatch("./foo/bar.js", "./**/foo/**", { format = format })) 24 | assert(isMatch("./foo/bar.js", "**/foo/**", { format = format })) 25 | assert(isMatch("./foo/bar.txt", "foo/**/*.txt", { format = format })) 26 | assert(makeRe("./foo/**/*.txt"):test("foo/bar.txt")) 27 | assert(not isMatch("./foo/!(bar)/**", "foo/bar/a.js", { format = format })) 28 | assert(not makeRe("./foo/!(bar)/**"):test("foo/bar/a.js")) 29 | end) 30 | 31 | itFIXME("https://github.com/isaacs/minimatch/issues/50", function() 32 | assert(isMatch("foo/bar-[ABC].txt", "foo/**/*-\\[ABC\\].txt")) 33 | assert(not isMatch("foo/bar-[ABC].txt", "foo/**/*-\\[abc\\].txt")) 34 | assert(isMatch("foo/bar-[ABC].txt", "foo/**/*-\\[abc\\].txt", { nocase = true })) 35 | end) 36 | 37 | it( 38 | "https://github.com/isaacs/minimatch/issues/67 (should work consistently with `makeRe` and matcher functions)", 39 | function() 40 | local re = makeRe("node_modules/foobar/**/*.bar") 41 | assert(re:test("node_modules/foobar/foo.bar")) 42 | assert(isMatch("node_modules/foobar/foo.bar", "node_modules/foobar/**/*.bar")) 43 | end 44 | ) 45 | it("https://github.com/isaacs/minimatch/issues/75", function() 46 | assert(isMatch("foo/baz.qux.js", "foo/@(baz.qux).js")) 47 | assert(isMatch("foo/baz.qux.js", "foo/+(baz.qux).js")) 48 | assert(isMatch("foo/baz.qux.js", "foo/*(baz.qux).js")) 49 | assert(not isMatch("foo/baz.qux.js", "foo/!(baz.qux).js")) 50 | assert(not isMatch("foo/bar/baz.qux.js", "foo/*/!(baz.qux).js")) 51 | assert(not isMatch("foo/bar/bazqux.js", "**/!(bazqux).js")) 52 | assert(not isMatch("foo/bar/bazqux.js", "**/bar/!(bazqux).js")) 53 | assert(not isMatch("foo/bar/bazqux.js", "foo/**/!(bazqux).js")) 54 | assert(not isMatch("foo/bar/bazqux.js", "foo/**/!(bazqux)*.js")) 55 | assert(not isMatch("foo/bar/baz.qux.js", "foo/**/!(baz.qux)*.js")) 56 | assert(not isMatch("foo/bar/baz.qux.js", "foo/**/!(baz.qux).js")) 57 | assert(not isMatch("foobar.js", "!(foo)*.js")) 58 | assert(not isMatch("foo.js", "!(foo).js")) 59 | assert(not isMatch("foo.js", "!(foo)*.js")) 60 | end) 61 | 62 | itFIXME("https://github.com/isaacs/minimatch/issues/78", function() 63 | assert(isMatch("a\\b\\c.txt", "a/**/*.txt", { windows = true })) 64 | assert(isMatch("a/b/c.txt", "a/**/*.txt", { windows = true })) 65 | end) 66 | 67 | it("https://github.com/isaacs/minimatch/issues/82", function() 68 | assert(isMatch("./src/test/a.js", "**/test/**", { format = format })) 69 | assert(isMatch("src/test/a.js", "**/test/**")) 70 | end) 71 | 72 | it("https://github.com/isaacs/minimatch/issues/83", function() 73 | assert(not makeRe("foo/!(bar)/**"):test("foo/bar/a.js")) 74 | assert(not isMatch("foo/!(bar)/**", "foo/bar/a.js")) 75 | end) 76 | end) 77 | end) 78 | end 79 | -------------------------------------------------------------------------------- /src/__tests__/options.format.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.format.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Array = LuauPolyfill.Array 9 | local Object = LuauPolyfill.Object 10 | 11 | local jestExpect = require(Packages.Dev.JestGlobals).expect 12 | 13 | local match = require(CurrentModule.support.match) 14 | local isMatch = require(PicomatchModule).isMatch 15 | local function equal(actual, expected) 16 | jestExpect(Array.sort(Array.concat({}, actual))).toEqual( 17 | Array.sort(Array.concat({}, expected)) 18 | -- ROBLOX deviation: jestExpect doesn't accept message 19 | ) 20 | end 21 | describe("options.format", function() 22 | -- see https://github.com/isaacs/minimatch/issues/30 23 | it("should match the string returned by options.format", function() 24 | local opts = { 25 | format = function(str) 26 | return str:gsub("\\", "/"):gsub("^%./", "") 27 | end, 28 | strictSlashes = true, 29 | } 30 | local fixtures = { 31 | "a", 32 | "./a", 33 | "b", 34 | "a/a", 35 | "./a/b", 36 | "a/c", 37 | "./a/x", 38 | "./a/a/a", 39 | "a/a/b", 40 | "./a/a/a/a", 41 | "./a/a/a/a/a", 42 | "x/y", 43 | "./z/z", 44 | } 45 | assert(not isMatch("./.a", "*.a", opts)) 46 | assert(not isMatch("./.a", "./*.a", opts)) 47 | assert(not isMatch("./.a", "a/**/z/*.md", opts)) 48 | assert(not isMatch("./a/b/c/d/e/z/c.md", "./a/**/j/**/z/*.md", opts)) 49 | assert(not isMatch("./a/b/c/j/e/z/c.txt", "./a/**/j/**/z/*.md", opts)) 50 | assert(not isMatch("a/b/c/d/e/z/c.md", "./a/**/j/**/z/*.md", opts)) 51 | assert(isMatch("./.a", "./.a", opts)) 52 | assert(isMatch("./a/b/c.md", "a/**/*.md", opts)) 53 | assert(isMatch("./a/b/c/d/e/j/n/p/o/z/c.md", "./a/**/j/**/z/*.md", opts)) 54 | assert(isMatch("./a/b/c/d/e/z/c.md", "**/*.md", opts)) 55 | assert(isMatch("./a/b/c/d/e/z/c.md", "./a/**/z/*.md", opts)) 56 | assert(isMatch("./a/b/c/d/e/z/c.md", "a/**/z/*.md", opts)) 57 | assert(isMatch("./a/b/c/j/e/z/c.md", "./a/**/j/**/z/*.md", opts)) 58 | assert(isMatch("./a/b/c/j/e/z/c.md", "a/**/j/**/z/*.md", opts)) 59 | assert(isMatch("./a/b/z/.a", "./a/**/z/.a", opts)) 60 | assert(isMatch("./a/b/z/.a", "a/**/z/.a", opts)) 61 | assert(isMatch(".a", "./.a", opts)) 62 | assert(isMatch("a/b/c.md", "./a/**/*.md", opts)) 63 | assert(isMatch("a/b/c.md", "a/**/*.md", opts)) 64 | assert(isMatch("a/b/c/d/e/z/c.md", "a/**/z/*.md", opts)) 65 | assert(isMatch("a/b/c/j/e/z/c.md", "a/**/j/**/z/*.md", opts)) 66 | assert(isMatch("./a", "*", opts)) 67 | assert(isMatch("./foo/bar.js", "**/foo/**", opts)) 68 | assert(isMatch("./foo/bar.js", "./**/foo/**", opts)) 69 | assert(isMatch(".\\foo\\bar.js", "**/foo/**", Object.assign({}, opts, { windows = false }))) 70 | assert(isMatch(".\\foo\\bar.js", "./**/foo/**", opts)) 71 | equal(match(fixtures, "*", opts), { "a", "b" }) 72 | equal( 73 | match(fixtures, "**/a/**", opts), 74 | { "a/a", "a/c", "a/b", "a/x", "a/a/a", "a/a/b", "a/a/a/a", "a/a/a/a/a" } 75 | ) 76 | equal(match(fixtures, "*/*", opts), { "a/a", "a/b", "a/c", "a/x", "x/y", "z/z" }) 77 | equal(match(fixtures, "*/*/*", opts), { "a/a/a", "a/a/b" }) 78 | equal(match(fixtures, "*/*/*/*", opts), { "a/a/a/a" }) 79 | equal(match(fixtures, "*/*/*/*/*", opts), { "a/a/a/a/a" }) 80 | equal(match(fixtures, "*", opts), { "a", "b" }) 81 | equal( 82 | match(fixtures, "**/a/**", opts), 83 | { "a/a", "a/c", "a/b", "a/x", "a/a/a", "a/a/b", "a/a/a/a", "a/a/a/a/a" } 84 | ) 85 | equal(match(fixtures, "a/*/a", opts), { "a/a/a" }) 86 | equal(match(fixtures, "a/*", opts), { "a/a", "a/b", "a/c", "a/x" }) 87 | equal(match(fixtures, "a/*/*", opts), { "a/a/a", "a/a/b" }) 88 | equal(match(fixtures, "a/*/*/*", opts), { "a/a/a/a" }) 89 | equal(match(fixtures, "a/*/*/*/*", opts), { "a/a/a/a/a" }) 90 | equal(match(fixtures, "a/*/a", opts), { "a/a/a" }) 91 | end) 92 | end) 93 | end 94 | -------------------------------------------------------------------------------- /src/utils.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/lib/utils.js 2 | 3 | local CurrentModule = script.Parent 4 | local Packages = CurrentModule.Parent 5 | local LuauPolyfill = require(Packages.LuauPolyfill) 6 | local Array = LuauPolyfill.Array 7 | local Boolean = LuauPolyfill.Boolean 8 | local String = LuauPolyfill.String 9 | type Object = LuauPolyfill.Object 10 | 11 | local exports = {} 12 | 13 | -- ROBLOX deviation: skipping path 14 | -- local path = require("path") 15 | -- ROBLOX FIXME: make proper platform check 16 | -- local win32 = process.platform == "win32" 17 | local win32 = false 18 | local Constants = require(CurrentModule.constants) 19 | -- ROBLOX TODO START: implement missing RegExp when 'g' flag available (or reimplement without RegExp) 20 | local REGEX_SPECIAL_CHARS = Constants.REGEX_SPECIAL_CHARS 21 | -- local REGEX_BACKSLASH, REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL = 22 | -- Constants.REGEX_BACKSLASH, 23 | -- Constants.REGEX_REMOVE_BACKSLASH, 24 | -- Constants.REGEX_SPECIAL_CHARS, 25 | local REGEX_SPECIAL_CHARS_GLOBAL = Constants.REGEX_SPECIAL_CHARS_GLOBAL 26 | -- ROBLOX TODO END 27 | 28 | function exports.isObject(val) 29 | return val ~= nil and typeof(val) == "table" and not Array.isArray(val) 30 | end 31 | function exports.hasRegexChars(str) 32 | return string.match(str, REGEX_SPECIAL_CHARS) ~= nil 33 | end 34 | function exports.isRegexChar(str: string) 35 | return #str == 1 and exports.hasRegexChars(str) 36 | end 37 | -- ROBLOX deviation START: additional dependencies 38 | local String_replace = require(CurrentModule.stringUtils).stringReplace 39 | -- ROBLOX deviation END 40 | 41 | function exports.escapeRegex(str): string 42 | -- ROBLOX deviation: using custom String_replace function 43 | return String_replace(str, REGEX_SPECIAL_CHARS_GLOBAL, function(m) 44 | return "\\" .. m 45 | end) 46 | -- return str:replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1") 47 | -- ROBLOX deviation END 48 | end 49 | function exports.toPosixSlashes(str): string 50 | error("toPosixSlashes not implemented") 51 | -- return str:replace(REGEX_BACKSLASH, "/") 52 | end 53 | 54 | function exports.removeBackslashes(str): string 55 | error("removeBackslashes not implemented") 56 | -- return str:replace(REGEX_REMOVE_BACKSLASH, function(match) 57 | -- return match == "\\" and "" or match 58 | -- end) 59 | end 60 | -- ROBLOX TODO END 61 | 62 | function exports.supportsLookbehinds() 63 | -- ROBLOX CHECK: do we suppoert lookbehind in Lua? 64 | -- ROBLOX deviation: no equivalent check in Lua. Returning always false 65 | return false 66 | end 67 | 68 | function exports.isWindows(options: Object?): boolean 69 | if typeof(options) == "table" and typeof(options.windows) == "boolean" then 70 | return options.windows 71 | end 72 | --[[ 73 | ROBLOX deviation: not using path.sep as it's not available 74 | original code: 75 | return win32 === true || path.sep === '\\'; 76 | ]] 77 | return win32 == true 78 | end 79 | 80 | function exports.escapeLast(input: string, char: string, lastIdx: number?) 81 | local idx = String.lastIndexOf(input, char, lastIdx) 82 | if idx == -1 then 83 | return input 84 | end 85 | if input:sub(idx - 1, idx - 1) == "\\" then 86 | return exports.escapeLast(input, char, idx - 1) 87 | end 88 | return ("%s%s"):format(String.slice(input, 1, idx), String.slice(input, idx)) 89 | end 90 | 91 | function exports.removePrefix(input: string, state_: Object?) 92 | local state: Object = state_ or {} 93 | 94 | local output = input 95 | if String.startsWith(output, "./") then 96 | output = String.slice(output, 3) 97 | state.prefix = "./" 98 | end 99 | return output 100 | end 101 | function exports.wrapOutput(input, state_: Object?, options_: Object?) 102 | local state: Object = state_ or {} 103 | local options: Object = options_ or {} 104 | 105 | local prepend = if Boolean.toJSBoolean(options.contains) then "" else "^" 106 | local append = if Boolean.toJSBoolean(options.contains) then "" else "$" 107 | 108 | local output = ("%s(?:%s)%s"):format(prepend, input, append) 109 | if state.negated == true then 110 | output = ("(?:^(?!%s).*$)"):format(output) 111 | end 112 | return output 113 | end 114 | 115 | return exports 116 | -------------------------------------------------------------------------------- /src/__tests__/options.ignore.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.ignore.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Array = LuauPolyfill.Array 9 | local Object = LuauPolyfill.Object 10 | 11 | local jestExpect = require(Packages.Dev.JestGlobals).expect 12 | 13 | local match = require(CurrentModule.support.match) 14 | local isMatch = require(PicomatchModule).isMatch 15 | describe("options.ignore", function() 16 | it("should not match ignored patterns", function() 17 | assert(isMatch("a+b/src/glimini.js", "a+b/src/*.js", { ignore = { "**/f*" } })) 18 | assert(not isMatch("a+b/src/glimini.js", "a+b/src/*.js", { ignore = { "**/g*" } })) 19 | assert(isMatch("+b/src/glimini.md", "+b/src/*", { ignore = { "**/*.js" } })) 20 | assert(not isMatch("+b/src/glimini.js", "+b/src/*", { ignore = { "**/*.js" } })) 21 | end) 22 | local negations = { "a/a", "a/b", "a/c", "a/d", "a/e", "b/a", "b/b", "b/c" } 23 | local globs = Array.sort({ 24 | ".a", 25 | ".a/a", 26 | ".a/a/a", 27 | ".a/a/a/a", 28 | "a", 29 | "a/.a", 30 | "a/a", 31 | "a/a/.a", 32 | "a/a/a", 33 | "a/a/a/a", 34 | "a/a/a/a/a", 35 | "a/a/b", 36 | "a/b", 37 | "a/b/c", 38 | "a/c", 39 | "a/x", 40 | "b", 41 | "b/b/b", 42 | "b/b/c", 43 | "c/c/c", 44 | "e/f/g", 45 | "h/i/a", 46 | "x/x/x", 47 | "x/y", 48 | "z/z", 49 | "z/z/z", 50 | }) 51 | it("should filter out ignored patterns", function() 52 | local opts = { ignore = { "a/**" }, strictSlashes = true } 53 | local dotOpts = Object.assign({}, opts, { dot = true }) 54 | jestExpect(match(globs, "*", opts)).toEqual({ "a", "b" }) 55 | jestExpect(match(globs, "*", Object.assign({}, opts, { strictSlashes = false }))).toEqual({ "b" }) 56 | jestExpect(match(globs, "*", { ignore = "**/a" })).toEqual({ "b" }) 57 | jestExpect(match(globs, "*/*", opts)).toEqual({ "x/y", "z/z" }) 58 | jestExpect(match(globs, "*/*/*", opts)).toEqual({ 59 | "b/b/b", 60 | "b/b/c", 61 | "c/c/c", 62 | "e/f/g", 63 | "h/i/a", 64 | "x/x/x", 65 | "z/z/z", 66 | }) 67 | jestExpect(match(globs, "*/*/*/*", opts)).toEqual({}) 68 | jestExpect(match(globs, "*/*/*/*/*", opts)).toEqual({}) 69 | jestExpect(match(globs, "a/*", opts)).toEqual({}) 70 | jestExpect(match(globs, "**/*/x", opts)).toEqual({ "x/x/x" }) 71 | -- ROBLOX FIXME 72 | -- jestExpect(match(globs, "**/*/[b-z]", opts)).toEqual({ 73 | -- "b/b/b", 74 | -- "b/b/c", 75 | -- "c/c/c", 76 | -- "e/f/g", 77 | -- "x/x/x", 78 | -- "x/y", 79 | -- "z/z", 80 | -- "z/z/z", 81 | -- }) 82 | jestExpect(match(globs, "*", { ignore = "**/a", dot = true })).toEqual({ ".a", "b" }) 83 | jestExpect(match(globs, "*", dotOpts)).toEqual({ ".a", "a", "b" }) 84 | jestExpect(match(globs, "*/*", dotOpts)).toEqual(Array.sort({ ".a/a", "x/y", "z/z" })) 85 | jestExpect(match(globs, "*/*/*", dotOpts)).toEqual( 86 | Array.sort({ ".a/a/a", "b/b/b", "b/b/c", "c/c/c", "e/f/g", "h/i/a", "x/x/x", "z/z/z" }) 87 | ) 88 | jestExpect(match(globs, "*/*/*/*", dotOpts)).toEqual({ ".a/a/a/a" }) 89 | jestExpect(match(globs, "*/*/*/*/*", dotOpts)).toEqual({}) 90 | jestExpect(match(globs, "a/*", dotOpts)).toEqual({}) 91 | jestExpect(match(globs, "**/*/x", dotOpts)).toEqual({ "x/x/x" }) -- see https://github.com/jonschlinkert/micromatch/issues/79 92 | jestExpect(match({ "foo.js", "a/foo.js" }, "**/foo.js")).toEqual({ "foo.js", "a/foo.js" }) 93 | jestExpect(match({ "foo.js", "a/foo.js" }, "**/foo.js", { dot = true })).toEqual({ "foo.js", "a/foo.js" }) 94 | jestExpect(match(negations, "!b/a", opts)).toEqual({ "b/b", "b/c" }) 95 | jestExpect(match(negations, "!b/(a)", opts)).toEqual({ "b/b", "b/c" }) 96 | jestExpect(match(negations, "!(b/(a))", opts)).toEqual({ "b/b", "b/c" }) 97 | jestExpect(match(negations, "!(b/a)", opts)).toEqual({ "b/b", "b/c" }) 98 | jestExpect(match(negations, "**")).toEqual( 99 | negations 100 | -- ROBLOX deviation: jestExpect doesn't accept message argument 101 | -- , "nothing is ignored" 102 | ) 103 | jestExpect(match(negations, "**", { ignore = { "*/b", "*/a" } })).toEqual({ "a/c", "a/d", "a/e", "b/c" }) 104 | jestExpect(match(negations, "**", { ignore = { "**" } })).toEqual({}) 105 | end) 106 | end) 107 | end 108 | -------------------------------------------------------------------------------- /src/__tests__/options.onMatch.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.onMatch.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Array = LuauPolyfill.Array 9 | local String = LuauPolyfill.String 10 | 11 | local jestExpect = require(Packages.Dev.JestGlobals).expect 12 | 13 | local match = require(CurrentModule.support.match) 14 | local picomatch = require(PicomatchModule) 15 | local isMatch = picomatch.isMatch 16 | local function equal(actual, expected) 17 | jestExpect(Array.sort(Array.concat({}, actual))).toEqual( 18 | Array.sort(Array.concat({}, expected)) 19 | -- ROBLOX deviation: jestExpect doesn't accept message 20 | ) 21 | end 22 | local function format(str: string) 23 | return str:gsub("^%./", "") 24 | end 25 | local function options() 26 | return { 27 | format = format, 28 | onMatch = function(ref, matches) 29 | local _pattern, _regex, _input, output: string = ref.pattern, ref.regex, ref.input, ref.output 30 | if #output > 2 and (String.startsWith(output, "./") or String.startsWith(output, ".\\")) then 31 | output = String.slice(output, 2) 32 | end 33 | if matches ~= nil then 34 | matches:add(output) 35 | end 36 | end, 37 | } 38 | end 39 | describe("options.onMatch", function() 40 | it("should call options.onMatch on each matching string", function() 41 | local fixtures = { 42 | "a", 43 | "./a", 44 | "b", 45 | "a/a", 46 | "./a/b", 47 | "a/c", 48 | "./a/x", 49 | "./a/a/a", 50 | "a/a/b", 51 | "./a/a/a/a", 52 | "./a/a/a/a/a", 53 | "x/y", 54 | "./z/z", 55 | } 56 | assert(not isMatch("./.a", "*.a", { format = format })) 57 | assert(not isMatch("./.a", "./*.a", { format = format })) 58 | assert(not isMatch("./.a", "a/**/z/*.md", { format = format })) 59 | assert(not isMatch("./a/b/c/d/e/z/c.md", "./a/**/j/**/z/*.md", { format = format })) 60 | assert(not isMatch("./a/b/c/j/e/z/c.txt", "./a/**/j/**/z/*.md", { format = format })) 61 | assert(not isMatch("a/b/c/d/e/z/c.md", "./a/**/j/**/z/*.md", { format = format })) 62 | assert(isMatch("./.a", "./.a", { format = format })) 63 | assert(isMatch("./a/b/c.md", "a/**/*.md", { format = format })) 64 | assert(isMatch("./a/b/c/d/e/j/n/p/o/z/c.md", "./a/**/j/**/z/*.md", { format = format })) 65 | assert(isMatch("./a/b/c/d/e/z/c.md", "**/*.md", { format = format })) 66 | assert(isMatch("./a/b/c/d/e/z/c.md", "./a/**/z/*.md", { format = format })) 67 | assert(isMatch("./a/b/c/d/e/z/c.md", "a/**/z/*.md", { format = format })) 68 | assert(isMatch("./a/b/c/j/e/z/c.md", "./a/**/j/**/z/*.md", { format = format })) 69 | assert(isMatch("./a/b/c/j/e/z/c.md", "a/**/j/**/z/*.md", { format = format })) 70 | assert(isMatch("./a/b/z/.a", "./a/**/z/.a", { format = format })) 71 | assert(isMatch("./a/b/z/.a", "a/**/z/.a", { format = format })) 72 | assert(isMatch(".a", "./.a", { format = format })) 73 | assert(isMatch("a/b/c.md", "./a/**/*.md", { format = format })) 74 | assert(isMatch("a/b/c.md", "a/**/*.md", { format = format })) 75 | assert(isMatch("a/b/c/d/e/z/c.md", "a/**/z/*.md", { format = format })) 76 | assert(isMatch("a/b/c/j/e/z/c.md", "a/**/j/**/z/*.md", { format = format })) 77 | equal(match(fixtures, "*", options()), { "a", "b" }) 78 | equal( 79 | match(fixtures, "**/a/**", options()), 80 | { "a", "a/a", "a/c", "a/b", "a/x", "a/a/a", "a/a/b", "a/a/a/a", "a/a/a/a/a" } 81 | ) 82 | equal(match(fixtures, "*/*", options()), { "a/a", "a/b", "a/c", "a/x", "x/y", "z/z" }) 83 | equal(match(fixtures, "*/*/*", options()), { "a/a/a", "a/a/b" }) 84 | equal(match(fixtures, "*/*/*/*", options()), { "a/a/a/a" }) 85 | equal(match(fixtures, "*/*/*/*/*", options()), { "a/a/a/a/a" }) 86 | equal(match(fixtures, "./*", options()), { "a", "b" }) 87 | equal( 88 | match(fixtures, "./**/a/**", options()), 89 | { "a", "a/a", "a/b", "a/c", "a/x", "a/a/a", "a/a/b", "a/a/a/a", "a/a/a/a/a" } 90 | ) 91 | equal(match(fixtures, "./a/*/a", options()), { "a/a/a" }) 92 | equal(match(fixtures, "a/*", options()), { "a/a", "a/b", "a/c", "a/x" }) 93 | equal(match(fixtures, "a/*/*", options()), { "a/a/a", "a/a/b" }) 94 | equal(match(fixtures, "a/*/*/*", options()), { "a/a/a/a" }) 95 | equal(match(fixtures, "a/*/*/*/*", options()), { "a/a/a/a/a" }) 96 | equal(match(fixtures, "a/*/a", options()), { "a/a/a" }) 97 | end) 98 | end) 99 | end 100 | -------------------------------------------------------------------------------- /src/constants.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/lib/constants.js 2 | 3 | local CurrentModule = script.Parent 4 | local Packages = CurrentModule.Parent 5 | local LuauPolyfill = require(Packages.LuauPolyfill) 6 | local Object = LuauPolyfill.Object 7 | 8 | local RegExp = require(Packages.RegExp) 9 | 10 | -- ROBLOX deviation: skipping path 11 | -- local path = require("path") 12 | local WIN_SLASH = "\\\\/" 13 | local WIN_NO_SLASH = ("[^%s]"):format(WIN_SLASH) 14 | --[[* 15 | * Posix glob regex 16 | ]] 17 | local DOT_LITERAL = "\\." 18 | local PLUS_LITERAL = "\\+" 19 | local QMARK_LITERAL = "\\?" 20 | local SLASH_LITERAL = "\\/" 21 | local ONE_CHAR = "(?=.)" 22 | local QMARK = "[^/]" 23 | local END_ANCHOR = ("(?:%s|$)"):format(SLASH_LITERAL) 24 | local START_ANCHOR = ("(?:^|%s)"):format(SLASH_LITERAL) 25 | local DOTS_SLASH = ("%s{1,2}%s"):format(DOT_LITERAL, END_ANCHOR) 26 | local NO_DOT = ("(?!%s)"):format(DOT_LITERAL) 27 | local NO_DOTS = ("(?!%s%s)"):format(START_ANCHOR, DOTS_SLASH) 28 | local NO_DOT_SLASH = ("(?!%s{0,1}%s)"):format(DOT_LITERAL, END_ANCHOR) 29 | local NO_DOTS_SLASH = ("(?!%s)"):format(DOTS_SLASH) 30 | local QMARK_NO_DOT = ("[^.%s]"):format(SLASH_LITERAL) 31 | local STAR = ("%s*?"):format(QMARK) 32 | local POSIX_CHARS = { 33 | DOT_LITERAL = DOT_LITERAL, 34 | PLUS_LITERAL = PLUS_LITERAL, 35 | QMARK_LITERAL = QMARK_LITERAL, 36 | SLASH_LITERAL = SLASH_LITERAL, 37 | ONE_CHAR = ONE_CHAR, 38 | QMARK = QMARK, 39 | END_ANCHOR = END_ANCHOR, 40 | DOTS_SLASH = DOTS_SLASH, 41 | NO_DOT = NO_DOT, 42 | NO_DOTS = NO_DOTS, 43 | NO_DOT_SLASH = NO_DOT_SLASH, 44 | NO_DOTS_SLASH = NO_DOTS_SLASH, 45 | QMARK_NO_DOT = QMARK_NO_DOT, 46 | STAR = STAR, 47 | START_ANCHOR = START_ANCHOR, 48 | } 49 | --[[* 50 | * Windows glob regex 51 | ]] 52 | local WINDOWS_CHARS = Object.assign({}, POSIX_CHARS, { 53 | SLASH_LITERAL = ("[%s]"):format(WIN_SLASH), 54 | QMARK = WIN_NO_SLASH, 55 | STAR = ("%s*?"):format(WIN_NO_SLASH), 56 | DOTS_SLASH = ("%s{1,2}(?:[%s]|$)"):format(DOT_LITERAL, WIN_SLASH), 57 | NO_DOT = ("(?!%s)"):format(DOT_LITERAL), 58 | NO_DOTS = ("(?!(?:^|[%s])%s{1,2}(?:[%s]|$))"):format(WIN_SLASH, DOT_LITERAL, WIN_SLASH), 59 | NO_DOT_SLASH = ("(?!%s{0,1}(?:[%s]|$))"):format(DOT_LITERAL, WIN_SLASH), 60 | NO_DOTS_SLASH = ("(?!%s{1,2}(?:[%s]|$))"):format(DOT_LITERAL, WIN_SLASH), 61 | QMARK_NO_DOT = ("[^.%s]"):format(WIN_SLASH), 62 | START_ANCHOR = ("(?:^|[%s])"):format(WIN_SLASH), 63 | END_ANCHOR = ("(?:[%s]|$)"):format(WIN_SLASH), 64 | }) 65 | --[[* 66 | * POSIX Bracket Regex 67 | ]] 68 | local POSIX_REGEX_SOURCE = { 69 | alnum = "a-zA-Z0-9", 70 | alpha = "a-zA-Z", 71 | ascii = "\\x00-\\x7F", 72 | blank = " \\t", 73 | cntrl = "\\x00-\\x1F\\x7F", 74 | digit = "0-9", 75 | graph = "\\x21-\\x7E", 76 | lower = "a-z", 77 | print = "\\x20-\\x7E ", 78 | punct = "\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~", 79 | space = " \\t\\r\\n\\v\\f", 80 | upper = "A-Z", 81 | word = "A-Za-z0-9_", 82 | xdigit = "A-Fa-f0-9", 83 | } 84 | 85 | return { 86 | MAX_LENGTH = 1024 * 64, 87 | POSIX_REGEX_SOURCE = POSIX_REGEX_SOURCE, 88 | 89 | -- regular expressions 90 | -- ROBLOX TODO: no "g" flag supported yet 91 | -- REGEX_BACKSLASH = RegExp("\\\\(?![*+?^${}(|)[\\]])", "g"), 92 | REGEX_NON_SPECIAL_CHARS = "^[^@![%].,$*+?^{}()|\\/]+", 93 | REGEX_SPECIAL_CHARS = "[-*+?.^${}(|)[%]]", 94 | -- ROBLOX TODO START: no "g" flag supported yet 95 | REGEX_SPECIAL_CHARS_BACKREF = RegExp("(\\\\?)((\\W)(\\3*))"), 96 | -- ROBLOX TODO: no "g" flag supported yet 97 | REGEX_SPECIAL_CHARS_GLOBAL = RegExp("([-*+?.^${}(|)[\\]])"), 98 | -- REGEX_REMOVE_BACKSLASH = RegExp("(?:\\[.*?[^\\\\]\\]|\\\\(?=.))", "g"), 99 | -- ROBLOX TODO END 100 | 101 | -- Replace globs with equivalent patterns to reduce parsing time. 102 | REPLACEMENTS = { 103 | ["***"] = "*", 104 | ["**/**"] = "**", 105 | ["**/**/**"] = "**", 106 | }, 107 | 108 | -- Digits 109 | CHAR_0 = 48, --[[ 0 ]] 110 | CHAR_9 = 57, --[[ 9 ]] 111 | 112 | -- Alphabet chars. 113 | CHAR_UPPERCASE_A = 65, --[[ A ]] 114 | CHAR_LOWERCASE_A = 97, --[[ a ]] 115 | CHAR_UPPERCASE_Z = 90, --[[ Z ]] 116 | CHAR_LOWERCASE_Z = 122, --[[ z ]] 117 | CHAR_LEFT_PARENTHESES = 40, --[[ ( ]] 118 | CHAR_RIGHT_PARENTHESES = 41, --[[ ) ]] 119 | CHAR_ASTERISK = 42, --[[ * ]] 120 | 121 | -- Non-alphabetic chars. 122 | CHAR_AMPERSAND = 38, --[[ & ]] 123 | CHAR_AT = 64, --[[ @ ]] 124 | CHAR_BACKWARD_SLASH = 92, --[[ \ ]] 125 | CHAR_CARRIAGE_RETURN = 13, --[[ \r ]] 126 | CHAR_CIRCUMFLEX_ACCENT = 94, --[[ ^ ]] 127 | CHAR_COLON = 58, --[[ : ]] 128 | CHAR_COMMA = 44, --[[ , ]] 129 | CHAR_DOT = 46, --[[ . ]] 130 | CHAR_DOUBLE_QUOTE = 34, --[[ " ]] 131 | CHAR_EQUAL = 61, --[[ = ]] 132 | CHAR_EXCLAMATION_MARK = 33, --[[ ! ]] 133 | CHAR_FORM_FEED = 12, --[[ \f ]] 134 | CHAR_FORWARD_SLASH = 47, --[[ / ]] 135 | CHAR_GRAVE_ACCENT = 96, --[[ ` ]] 136 | CHAR_HASH = 35, --[[ # ]] 137 | CHAR_HYPHEN_MINUS = 45, --[[ - ]] 138 | CHAR_LEFT_ANGLE_BRACKET = 60, --[[ < ]] 139 | CHAR_LEFT_CURLY_BRACE = 123, --[[ { ]] 140 | CHAR_LEFT_SQUARE_BRACKET = 91, --[[ [ ]] 141 | CHAR_LINE_FEED = 10, --[[ \n ]] 142 | CHAR_NO_BREAK_SPACE = 160, --[[ \u00A0 ]] 143 | CHAR_PERCENT = 37, --[[ % ]] 144 | CHAR_PLUS = 43, --[[ + ]] 145 | CHAR_QUESTION_MARK = 63, --[[ ? ]] 146 | CHAR_RIGHT_ANGLE_BRACKET = 62, --[[ > ]] 147 | CHAR_RIGHT_CURLY_BRACE = 125, --[[ } ]] 148 | CHAR_RIGHT_SQUARE_BRACKET = 93, --[[ ] ]] 149 | CHAR_SEMICOLON = 59, --[[ ; ]] 150 | CHAR_SINGLE_QUOTE = 39, --[[ ' ]] 151 | CHAR_SPACE = 32, --[[ ]] 152 | CHAR_TAB = 9, --[[ \t ]] 153 | CHAR_UNDERSCORE = 95, --[[ _ ]] 154 | CHAR_VERTICAL_LINE = 124, --[[ | ]] 155 | CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279, --[[ \uFEFF ]] 156 | 157 | -- ROBLOX FIXME 158 | SEP = "/", -- path.sep, 159 | 160 | --[[* 161 | * Create EXTGLOB_CHARS 162 | ]] 163 | 164 | extglobChars = function(chars) 165 | return { 166 | ["!"] = { type = "negate", open = "(?:(?!(?:", close = ("))%s)"):format(chars.STAR) }, 167 | ["?"] = { type = "qmark", open = "(?:", close = ")?" }, 168 | ["+"] = { type = "plus", open = "(?:", close = ")+" }, 169 | ["*"] = { type = "star", open = "(?:", close = ")*" }, 170 | ["@"] = { type = "at", open = "(?:", close = ")" }, 171 | } 172 | end, 173 | 174 | --[[* 175 | * Create GLOB_CHARS 176 | ]] 177 | 178 | globChars = function(win32) 179 | return if win32 == true then WINDOWS_CHARS else POSIX_CHARS 180 | end, 181 | } 182 | -------------------------------------------------------------------------------- /src/__tests__/qmarks.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/qmarks.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | 8 | local jestExpect = require(Packages.Dev.JestGlobals).expect 9 | 10 | local match = require(CurrentModule.support.match) 11 | local isMatch = require(PicomatchModule).isMatch 12 | describe("qmarks and stars", function() 13 | it("should match question marks with question marks", function() 14 | jestExpect(match({ "?", "??", "???" }, "?")).toEqual({ "?" }) 15 | jestExpect(match({ "?", "??", "???" }, "??")).toEqual({ "??" }) 16 | jestExpect(match({ "?", "??", "???" }, "???")).toEqual({ "???" }) 17 | end) 18 | 19 | it("should match question marks and stars with question marks and stars", function() 20 | jestExpect(match({ "?", "??", "???" }, "?*")).toEqual({ "?", "??", "???" }) 21 | jestExpect(match({ "?", "??", "???" }, "*?")).toEqual({ "?", "??", "???" }) 22 | jestExpect(match({ "?", "??", "???" }, "?*?")).toEqual({ "??", "???" }) 23 | jestExpect(match({ "?*", "?*?", "?*?*?" }, "?*")).toEqual({ "?*", "?*?", "?*?*?" }) 24 | jestExpect(match({ "?*", "?*?", "?*?*?" }, "*?")).toEqual({ "?*", "?*?", "?*?*?" }) 25 | jestExpect(match({ "?*", "?*?", "?*?*?" }, "?*?")).toEqual({ "?*", "?*?", "?*?*?" }) 26 | end) 27 | 28 | it("should support consecutive stars and question marks", function() 29 | jestExpect(match({ "aaa", "aac", "abc" }, "a*?c")).toEqual({ "aac", "abc" }) 30 | jestExpect(match({ "abc", "abb", "acc" }, "a**?c")).toEqual({ "abc", "acc" }) 31 | jestExpect(match({ "abc", "aaaabbbbbbccccc" }, "a*****?c")).toEqual({ "abc", "aaaabbbbbbccccc" }) 32 | jestExpect(match({ "a", "ab", "abc", "abcd" }, "*****?")).toEqual({ "a", "ab", "abc", "abcd" }) 33 | jestExpect(match({ "a", "ab", "abc", "abcd" }, "*****??")).toEqual({ "ab", "abc", "abcd" }) 34 | jestExpect(match({ "a", "ab", "abc", "abcd" }, "?*****??")).toEqual({ "abc", "abcd" }) 35 | jestExpect(match({ "abc", "abb", "zzz" }, "?*****?c")).toEqual({ "abc" }) 36 | jestExpect(match({ "abc", "bbb", "zzz" }, "?***?****?")).toEqual({ "abc", "bbb", "zzz" }) 37 | jestExpect(match({ "abc", "bbb", "zzz" }, "?***?****c")).toEqual({ "abc" }) 38 | jestExpect(match({ "abc" }, "*******?")).toEqual({ "abc" }) 39 | jestExpect(match({ "abc" }, "*******c")).toEqual({ "abc" }) 40 | jestExpect(match({ "abc" }, "?***?****")).toEqual({ "abc" }) 41 | jestExpect(match({ "abcdecdhjk" }, "a****c**?**??*****")).toEqual({ "abcdecdhjk" }) 42 | jestExpect(match({ "abcdecdhjk" }, "a**?**cd**?**??***k")).toEqual({ "abcdecdhjk" }) 43 | jestExpect(match({ "abcdecdhjk" }, "a**?**cd**?**??***k**")).toEqual({ "abcdecdhjk" }) 44 | jestExpect(match({ "abcdecdhjk" }, "a**?**cd**?**??k")).toEqual({ "abcdecdhjk" }) 45 | jestExpect(match({ "abcdecdhjk" }, "a**?**cd**?**??k***")).toEqual({ "abcdecdhjk" }) 46 | jestExpect(match({ "abcdecdhjk" }, "a*cd**?**??k")).toEqual({ "abcdecdhjk" }) 47 | end) 48 | 49 | it("should match backslashes with question marks when not on windows", function() 50 | -- ROBLOX FIXME: need a test for platform 51 | local isWindows = false 52 | if isWindows then 53 | assert(not isMatch("aaa\\\\bbb", "aaa?bbb")) 54 | assert(isMatch("aaa\\\\bbb", "aaa??bbb")) 55 | assert(isMatch("aaa\\bbb", "aaa?bbb")) 56 | end 57 | end) 58 | 59 | it("should match one character per question mark", function() 60 | local fixtures = { "a", "aa", "ab", "aaa", "abcdefg" } 61 | jestExpect(match(fixtures, "?")).toEqual({ "a" }) 62 | jestExpect(match(fixtures, "??")).toEqual({ "aa", "ab" }) 63 | jestExpect(match(fixtures, "???")).toEqual({ "aaa" }) 64 | jestExpect(match({ "a/", "/a/", "/a/b/", "/a/b/c/", "/a/b/c/d/" }, "??")).toEqual({}) 65 | jestExpect(match({ "a/b/c.md" }, "a/?/c.md")).toEqual({ "a/b/c.md" }) 66 | jestExpect(match({ "a/bb/c.md" }, "a/?/c.md")).toEqual({}) 67 | jestExpect(match({ "a/bb/c.md" }, "a/??/c.md")).toEqual({ "a/bb/c.md" }) 68 | jestExpect(match({ "a/bbb/c.md" }, "a/??/c.md")).toEqual({}) 69 | jestExpect(match({ "a/bbb/c.md" }, "a/???/c.md")).toEqual({ "a/bbb/c.md" }) 70 | jestExpect(match({ "a/bbbb/c.md" }, "a/????/c.md")).toEqual({ "a/bbbb/c.md" }) 71 | end) 72 | 73 | it("should not match slashes question marks", function() 74 | local fixtures = { "//", "a/", "/a", "/a/", "aa", "/aa", "a/a", "aaa", "/aaa" } 75 | jestExpect(match(fixtures, "/?")).toEqual({ "/a" }) 76 | jestExpect(match(fixtures, "/??")).toEqual({ "/aa" }) 77 | jestExpect(match(fixtures, "/???")).toEqual({ "/aaa" }) 78 | jestExpect(match(fixtures, "/?/")).toEqual({ "/a/" }) 79 | jestExpect(match(fixtures, "??")).toEqual({ "aa" }) 80 | jestExpect(match(fixtures, "?/?")).toEqual({ "a/a" }) 81 | jestExpect(match(fixtures, "???")).toEqual({ "aaa" }) 82 | jestExpect(match(fixtures, "a?a")).toEqual({ "aaa" }) 83 | jestExpect(match(fixtures, "aa?")).toEqual({ "aaa" }) 84 | jestExpect(match(fixtures, "?aa")).toEqual({ "aaa" }) 85 | end) 86 | 87 | it("should support question marks and stars between slashes", function() 88 | jestExpect(match({ "a/b.bb/c/d/efgh.ijk/e" }, "a/*/?/**/e")).toEqual({ "a/b.bb/c/d/efgh.ijk/e" }) 89 | jestExpect(match({ "a/b/c/d/e" }, "a/?/c/?/*/e")).toEqual({}) 90 | jestExpect(match({ "a/b/c/d/e/e" }, "a/?/c/?/*/e")).toEqual({ "a/b/c/d/e/e" }) 91 | jestExpect(match({ "a/b/c/d/efgh.ijk/e" }, "a/*/?/**/e")).toEqual({ "a/b/c/d/efgh.ijk/e" }) 92 | jestExpect(match({ "a/b/c/d/efghijk/e" }, "a/*/?/**/e")).toEqual({ "a/b/c/d/efghijk/e" }) 93 | jestExpect(match({ "a/b/c/d/efghijk/e" }, "a/?/**/e")).toEqual({ "a/b/c/d/efghijk/e" }) 94 | jestExpect(match({ "a/b/c/d/efghijk/e" }, "a/?/c/?/*/e")).toEqual({ "a/b/c/d/efghijk/e" }) 95 | jestExpect(match({ "a/bb/e" }, "a/?/**/e")).toEqual({}) 96 | jestExpect(match({ "a/bb/e" }, "a/?/e")).toEqual({}) 97 | jestExpect(match({ "a/bbb/c/d/efgh.ijk/e" }, "a/*/?/**/e")).toEqual({ "a/bbb/c/d/efgh.ijk/e" }) 98 | end) 99 | 100 | it("should match no more than one character between slashes", function() 101 | local fixtures = { "a/a", "a/a/a", "a/aa/a", "a/aaa/a", "a/aaaa/a", "a/aaaaa/a" } 102 | jestExpect(match(fixtures, "?/?")).toEqual({ "a/a" }) 103 | jestExpect(match(fixtures, "?/???/?")).toEqual({ "a/aaa/a" }) 104 | jestExpect(match(fixtures, "?/????/?")).toEqual({ "a/aaaa/a" }) 105 | jestExpect(match(fixtures, "?/?????/?")).toEqual({ "a/aaaaa/a" }) 106 | jestExpect(match(fixtures, "a/?")).toEqual({ "a/a" }) 107 | jestExpect(match(fixtures, "a/?/a")).toEqual({ "a/a/a" }) 108 | jestExpect(match(fixtures, "a/??/a")).toEqual({ "a/aa/a" }) 109 | jestExpect(match(fixtures, "a/???/a")).toEqual({ "a/aaa/a" }) 110 | jestExpect(match(fixtures, "a/????/a")).toEqual({ "a/aaaa/a" }) 111 | jestExpect(match(fixtures, "a/????a/a")).toEqual({ "a/aaaaa/a" }) 112 | end) 113 | 114 | itFIXME("should not match non-leading dots with question marks", function() 115 | local fixtures = { ".", ".a", "a", "aa", "a.a", "aa.a", "aaa", "aaa.a", "aaaa.a", "aaaaa" } 116 | jestExpect(match(fixtures, "?")).toEqual({ "a" }) 117 | jestExpect(match(fixtures, ".?")).toEqual({ ".a" }) 118 | jestExpect(match(fixtures, "?a")).toEqual({ "aa" }) 119 | jestExpect(match(fixtures, "??")).toEqual({ "aa" }) 120 | jestExpect(match(fixtures, "?a?")).toEqual({ "aaa" }) 121 | jestExpect(match(fixtures, "aaa?a")).toEqual({ "aaa.a", "aaaaa" }) 122 | jestExpect(match(fixtures, "a?a?a")).toEqual({ "aaa.a", "aaaaa" }) 123 | jestExpect(match(fixtures, "a???a")).toEqual({ "aaa.a", "aaaaa" }) 124 | jestExpect(match(fixtures, "a?????")).toEqual({ "aaaa.a" }) 125 | end) 126 | 127 | it("should match non-leading dots with question marks when options.dot is true", function() 128 | local fixtures = { ".", ".a", "a", "aa", "a.a", "aa.a", ".aa", "aaa.a", "aaaa.a", "aaaaa" } 129 | local opts = { dot = true } 130 | jestExpect(match(fixtures, "?", opts)).toEqual({ ".", "a" }) 131 | jestExpect(match(fixtures, ".?", opts)).toEqual({ ".a" }) 132 | jestExpect(match(fixtures, "?a", opts)).toEqual({ ".a", "aa" }) 133 | jestExpect(match(fixtures, "??", opts)).toEqual({ ".a", "aa" }) 134 | jestExpect(match(fixtures, "?a?", opts)).toEqual({ ".aa" }) 135 | end) 136 | end) 137 | end 138 | -------------------------------------------------------------------------------- /src/__tests__/braces.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/braces.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | 8 | local jestExpect = require(Packages.Dev.JestGlobals).expect 9 | 10 | -- ROBLOX deviation: not supported in Lua 11 | -- local fill = require("fill-range") 12 | local match = require(CurrentModule.support.match) 13 | local isMatch = require(PicomatchModule).isMatch 14 | describe("braces", function() 15 | itFIXME("should not match with brace patterns when disabled", function() 16 | jestExpect(match({ "a", "b", "c" }, "{a,b,c,d}")).toEqual({ "a", "b", "c" }) 17 | jestExpect(match({ "a", "b", "c" }, "{a,b,c,d}", { nobrace = true })).toEqual({}) 18 | jestExpect(match({ "1", "2", "3" }, "{1..2}", { nobrace = true })).toEqual({}) 19 | assert(not isMatch("a/a", "a/{a,b}", { nobrace = true })) 20 | assert(not isMatch("a/b", "a/{a,b}", { nobrace = true })) 21 | assert(not isMatch("a/c", "a/{a,b}", { nobrace = true })) 22 | assert(not isMatch("b/b", "a/{a,b}", { nobrace = true })) 23 | assert(not isMatch("b/b", "a/{a,b,c}", { nobrace = true })) 24 | assert(not isMatch("a/c", "a/{a,b,c}", { nobrace = true })) 25 | assert(not isMatch("a/a", "a/{a..c}", { nobrace = true })) 26 | assert(not isMatch("a/b", "a/{a..c}", { nobrace = true })) 27 | assert(not isMatch("a/c", "a/{a..c}", { nobrace = true })) 28 | end) 29 | 30 | it("should treat single-set braces as literals", function() 31 | assert(isMatch("a {abc} b", "a {abc} b")) 32 | assert(isMatch("a {a-b-c} b", "a {a-b-c} b")) 33 | assert(isMatch("a {a.c} b", "a {a.c} b")) 34 | end) 35 | 36 | it("should match literal braces when escaped", function() 37 | assert(isMatch("a {1,2}", "a \\{1,2\\}")) 38 | assert(isMatch("a {a..b}", "a \\{a..b\\}")) 39 | end) 40 | 41 | it("should match using brace patterns", function() 42 | assert(not isMatch("a/c", "a/{a,b}")) 43 | assert(not isMatch("b/b", "a/{a,b,c}")) 44 | assert(not isMatch("b/b", "a/{a,b}")) 45 | assert(isMatch("a/a", "a/{a,b}")) 46 | assert(isMatch("a/b", "a/{a,b}")) 47 | assert(isMatch("a/c", "a/{a,b,c}")) 48 | end) 49 | 50 | it("should support brace ranges", function() 51 | assert(isMatch("a/a", "a/{a..c}")) 52 | assert(isMatch("a/b", "a/{a..c}")) 53 | assert(isMatch("a/c", "a/{a..c}")) 54 | end) 55 | 56 | it("should support Kleene stars", function() 57 | assert(isMatch("ab", "{ab,c}*")) 58 | assert(isMatch("abab", "{ab,c}*")) 59 | assert(isMatch("abc", "{ab,c}*")) 60 | assert(isMatch("c", "{ab,c}*")) 61 | assert(isMatch("cab", "{ab,c}*")) 62 | assert(isMatch("cc", "{ab,c}*")) 63 | assert(isMatch("ababab", "{ab,c}*")) 64 | assert(isMatch("ababc", "{ab,c}*")) 65 | assert(isMatch("abcab", "{ab,c}*")) 66 | assert(isMatch("abcc", "{ab,c}*")) 67 | assert(isMatch("cabab", "{ab,c}*")) 68 | assert(isMatch("cabc", "{ab,c}*")) 69 | assert(isMatch("ccab", "{ab,c}*")) 70 | assert(isMatch("ccc", "{ab,c}*")) 71 | end) 72 | 73 | itFIXME("should not convert braces inside brackets", function() 74 | assert(isMatch("foo{}baz", "foo[{a,b}]+baz")) 75 | assert(isMatch("{a}{b}{c}", "[abc{}]+")) 76 | end) 77 | 78 | it("should support braces containing slashes", function() 79 | assert(isMatch("a", "{/,}a/**")) 80 | assert(isMatch("aa.txt", "a{a,b/}*.txt")) 81 | assert(isMatch("ab/.txt", "a{a,b/}*.txt")) 82 | assert(isMatch("ab/a.txt", "a{a,b/}*.txt")) 83 | assert(isMatch("a/", "a/**{/,}")) 84 | assert(isMatch("a/a", "a/**{/,}")) 85 | assert(isMatch("a/a/", "a/**{/,}")) 86 | end) 87 | 88 | it("should support braces with empty elements", function() 89 | assert(not isMatch("abc.txt", "a{,b}.txt")) 90 | assert(not isMatch("abc.txt", "a{a,b,}.txt")) 91 | assert(not isMatch("abc.txt", "a{b,}.txt")) 92 | assert(isMatch("a.txt", "a{,b}.txt")) 93 | assert(isMatch("a.txt", "a{b,}.txt")) 94 | assert(isMatch("aa.txt", "a{a,b,}.txt")) 95 | assert(isMatch("aa.txt", "a{a,b,}.txt")) 96 | assert(isMatch("ab.txt", "a{,b}.txt")) 97 | assert(isMatch("ab.txt", "a{b,}.txt")) 98 | end) 99 | 100 | it("should support braces with slashes and empty elements", function() 101 | assert(isMatch("a.txt", "a{,/}*.txt")) 102 | assert(isMatch("ab.txt", "a{,/}*.txt")) 103 | assert(isMatch("a/b.txt", "a{,/}*.txt")) 104 | assert(isMatch("a/ab.txt", "a{,/}*.txt")) 105 | end) 106 | 107 | it("should support braces with stars", function() 108 | assert(isMatch("a.txt", "a{,.*{foo,db},\\(bar\\)}.txt")) 109 | assert(not isMatch("adb.txt", "a{,.*{foo,db},\\(bar\\)}.txt")) 110 | assert(isMatch("a.db.txt", "a{,.*{foo,db},\\(bar\\)}.txt")) 111 | assert(isMatch("a.txt", "a{,*.{foo,db},\\(bar\\)}.txt")) 112 | assert(not isMatch("adb.txt", "a{,*.{foo,db},\\(bar\\)}.txt")) 113 | assert(isMatch("a.db.txt", "a{,*.{foo,db},\\(bar\\)}.txt")) 114 | assert(isMatch("a", "a{,.*{foo,db},\\(bar\\)}")) 115 | assert(not isMatch("adb", "a{,.*{foo,db},\\(bar\\)}")) 116 | assert(isMatch("a.db", "a{,.*{foo,db},\\(bar\\)}")) 117 | assert(isMatch("a", "a{,*.{foo,db},\\(bar\\)}")) 118 | assert(not isMatch("adb", "a{,*.{foo,db},\\(bar\\)}")) 119 | assert(isMatch("a.db", "a{,*.{foo,db},\\(bar\\)}")) 120 | assert(not isMatch("a", "{,.*{foo,db},\\(bar\\)}")) 121 | assert(not isMatch("adb", "{,.*{foo,db},\\(bar\\)}")) 122 | assert(not isMatch("a.db", "{,.*{foo,db},\\(bar\\)}")) 123 | assert(isMatch(".db", "{,.*{foo,db},\\(bar\\)}")) 124 | assert(not isMatch("a", "{,*.{foo,db},\\(bar\\)}")) 125 | assert(isMatch("a", "{*,*.{foo,db},\\(bar\\)}")) 126 | assert(not isMatch("adb", "{,*.{foo,db},\\(bar\\)}")) 127 | assert(isMatch("a.db", "{,*.{foo,db},\\(bar\\)}")) 128 | end) 129 | 130 | it("should support braces in patterns with globstars", function() 131 | assert(not isMatch("a/b/c/xyz.md", "a/b/**/c{d,e}/**/xyz.md")) 132 | assert(not isMatch("a/b/d/xyz.md", "a/b/**/c{d,e}/**/xyz.md")) 133 | assert(isMatch("a/b/cd/xyz.md", "a/b/**/c{d,e}/**/xyz.md")) 134 | assert(isMatch("a/b/c/xyz.md", "a/b/**/{c,d,e}/**/xyz.md")) 135 | assert(isMatch("a/b/d/xyz.md", "a/b/**/{c,d,e}/**/xyz.md")) 136 | end) 137 | 138 | it("should support braces with globstars, slashes and empty elements", function() 139 | assert(isMatch("a.txt", "a{,/**/}*.txt")) 140 | assert(isMatch("a/b.txt", "a{,/**/,/}*.txt")) 141 | assert(isMatch("a/x/y.txt", "a{,/**/}*.txt")) 142 | assert(not isMatch("a/x/y/z", "a{,/**/}*.txt")) 143 | end) 144 | 145 | it("should support braces with globstars and empty elements", function() 146 | assert(isMatch("a/b/foo/bar/baz.qux", "a/b{,/**}/bar{,/**}/*.*")) 147 | assert(isMatch("a/b/bar/baz.qux", "a/b{,/**}/bar{,/**}/*.*")) 148 | end) 149 | 150 | it("should support Kleene plus", function() 151 | assert(isMatch("ab", "{ab,c}+")) 152 | assert(isMatch("abab", "{ab,c}+")) 153 | assert(isMatch("abc", "{ab,c}+")) 154 | assert(isMatch("c", "{ab,c}+")) 155 | assert(isMatch("cab", "{ab,c}+")) 156 | assert(isMatch("cc", "{ab,c}+")) 157 | assert(isMatch("ababab", "{ab,c}+")) 158 | assert(isMatch("ababc", "{ab,c}+")) 159 | assert(isMatch("abcab", "{ab,c}+")) 160 | assert(isMatch("abcc", "{ab,c}+")) 161 | assert(isMatch("cabab", "{ab,c}+")) 162 | assert(isMatch("cabc", "{ab,c}+")) 163 | assert(isMatch("ccab", "{ab,c}+")) 164 | assert(isMatch("ccc", "{ab,c}+")) 165 | assert(isMatch("ccc", "{a,b,c}+")) 166 | assert(isMatch("a", "{a,b,c}+")) 167 | assert(isMatch("b", "{a,b,c}+")) 168 | assert(isMatch("c", "{a,b,c}+")) 169 | assert(isMatch("aa", "{a,b,c}+")) 170 | assert(isMatch("ab", "{a,b,c}+")) 171 | assert(isMatch("ac", "{a,b,c}+")) 172 | assert(isMatch("ba", "{a,b,c}+")) 173 | assert(isMatch("bb", "{a,b,c}+")) 174 | assert(isMatch("bc", "{a,b,c}+")) 175 | assert(isMatch("ca", "{a,b,c}+")) 176 | assert(isMatch("cb", "{a,b,c}+")) 177 | assert(isMatch("cc", "{a,b,c}+")) 178 | assert(isMatch("aaa", "{a,b,c}+")) 179 | assert(isMatch("aab", "{a,b,c}+")) 180 | assert(isMatch("abc", "{a,b,c}+")) 181 | end) 182 | 183 | it("should support braces", function() 184 | assert(isMatch("a", "{a,b,c}")) 185 | assert(isMatch("b", "{a,b,c}")) 186 | assert(isMatch("c", "{a,b,c}")) 187 | assert(not isMatch("aa", "{a,b,c}")) 188 | assert(not isMatch("bb", "{a,b,c}")) 189 | assert(not isMatch("cc", "{a,b,c}")) 190 | end) 191 | -- ROBLOX deviation START: not supported in Lua 192 | itSKIP("should match special chars and expand ranges in parentheses", function() 193 | -- local function expandRange(a, b) 194 | -- return ("(%s)"):format(fill(a, b, { toRegex = true })) 195 | -- end 196 | -- assert(not isMatch("foo/bar - 1", "*/* {4..10}", { expandRange = expandRange })) 197 | -- assert( 198 | -- not Boolean.toJSBoolean( 199 | -- isMatch("foo/bar - copy (1)", "*/* - * \\({4..10}\\)", { expandRange = expandRange }) 200 | -- ) 201 | -- ) 202 | -- assert(not isMatch("foo/bar (1)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 203 | -- assert(isMatch("foo/bar (4)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 204 | -- assert(isMatch("foo/bar (7)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 205 | -- assert(not isMatch("foo/bar (42)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 206 | -- assert(isMatch("foo/bar (42)", "*/* \\({4..43}\\)", { expandRange = expandRange })) 207 | -- assert(isMatch("foo/bar - copy [1]", "*/* \\[{0..5}\\]", { expandRange = expandRange })) 208 | -- assert(isMatch("foo/bar - foo + bar - copy [1]", "*/* \\[{0..5}\\]", { expandRange = expandRange })) 209 | -- assert(not isMatch("foo/bar - 1", "*/* \\({4..10}\\)", { expandRange = expandRange })) 210 | -- assert( 211 | -- not Boolean.toJSBoolean( 212 | -- isMatch("foo/bar - copy (1)", "*/* \\({4..10}\\)", { expandRange = expandRange }) 213 | -- ) 214 | -- ) 215 | -- assert(not isMatch("foo/bar (1)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 216 | -- assert(isMatch("foo/bar (4)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 217 | -- assert(isMatch("foo/bar (7)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 218 | -- assert(not isMatch("foo/bar (42)", "*/* \\({4..10}\\)", { expandRange = expandRange })) 219 | -- assert( 220 | -- not Boolean.toJSBoolean( 221 | -- isMatch("foo/bar - copy [1]", "*/* \\({4..10}\\)", { expandRange = expandRange }) 222 | -- ) 223 | -- ) 224 | -- assert( 225 | -- not Boolean.toJSBoolean( 226 | -- isMatch("foo/bar - foo + bar - copy [1]", "*/* \\({4..10}\\)", { expandRange = expandRange }) 227 | -- ) 228 | -- ) 229 | end) 230 | -- ROBLOX deviation END 231 | end) 232 | end 233 | -------------------------------------------------------------------------------- /src/__tests__/negation.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/negation.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe('negation patterns - "!"', function() 9 | it('should patterns with a leading "!" as negated/inverted globs', function() 10 | assert(not isMatch("abc", "!*")) 11 | assert(not isMatch("abc", "!abc")) 12 | assert(not isMatch("bar.md", "*!.md")) 13 | assert(not isMatch("bar.md", "foo!.md")) 14 | assert(not isMatch("foo!.md", "\\!*!*.md")) 15 | assert(not isMatch("foo!bar.md", "\\!*!*.md")) 16 | assert(isMatch("!foo!.md", "*!*.md")) 17 | assert(isMatch("!foo!.md", "\\!*!*.md")) 18 | assert(isMatch("abc", "!*foo")) 19 | assert(isMatch("abc", "!foo*")) 20 | assert(isMatch("abc", "!xyz")) 21 | assert(isMatch("ba!r.js", "*!*.*")) 22 | assert(isMatch("bar.md", "*.md")) 23 | assert(isMatch("foo!.md", "*!*.*")) 24 | assert(isMatch("foo!.md", "*!*.md")) 25 | assert(isMatch("foo!.md", "*!.md")) 26 | assert(isMatch("foo!.md", "*.md")) 27 | assert(isMatch("foo!.md", "foo!.md")) 28 | assert(isMatch("foo!bar.md", "*!*.md")) 29 | assert(isMatch("foobar.md", "*b*.md")) 30 | end) 31 | 32 | it('should treat non-leading "!" as literal characters', function() 33 | assert(not isMatch("a", "a!!b")) 34 | assert(not isMatch("aa", "a!!b")) 35 | assert(not isMatch("a/b", "a!!b")) 36 | assert(not isMatch("a!b", "a!!b")) 37 | assert(isMatch("a!!b", "a!!b")) 38 | assert(not isMatch("a/!!/b", "a!!b")) 39 | end) 40 | 41 | it("should support negation in globs that have no other special characters", function() 42 | assert(not isMatch("a/b", "!a/b")) 43 | assert(isMatch("a", "!a/b")) 44 | assert(isMatch("a.b", "!a/b")) 45 | assert(isMatch("a/a", "!a/b")) 46 | assert(isMatch("a/c", "!a/b")) 47 | assert(isMatch("b/a", "!a/b")) 48 | assert(isMatch("b/b", "!a/b")) 49 | assert(isMatch("b/c", "!a/b")) 50 | end) 51 | 52 | it("should support multiple leading ! to toggle negation", function() 53 | assert(not isMatch("abc", "!abc")) 54 | assert(isMatch("abc", "!!abc")) 55 | assert(not isMatch("abc", "!!!abc")) 56 | assert(isMatch("abc", "!!!!abc")) 57 | assert(not isMatch("abc", "!!!!!abc")) 58 | assert(isMatch("abc", "!!!!!!abc")) 59 | assert(not isMatch("abc", "!!!!!!!abc")) 60 | assert(isMatch("abc", "!!!!!!!!abc")) 61 | end) 62 | 63 | it("should support negation extglobs after leading !", function() 64 | assert(not isMatch("abc", "!(abc)")) 65 | assert(isMatch("abc", "!!(abc)")) 66 | assert(not isMatch("abc", "!!!(abc)")) 67 | assert(isMatch("abc", "!!!!(abc)")) 68 | assert(not isMatch("abc", "!!!!!(abc)")) 69 | assert(isMatch("abc", "!!!!!!(abc)")) 70 | assert(not isMatch("abc", "!!!!!!!(abc)")) 71 | assert(isMatch("abc", "!!!!!!!!(abc)")) 72 | end) 73 | 74 | it("should support negation with globs", function() 75 | assert(not isMatch("a/a", "!(*/*)")) 76 | assert(not isMatch("a/b", "!(*/*)")) 77 | assert(not isMatch("a/c", "!(*/*)")) 78 | assert(not isMatch("b/a", "!(*/*)")) 79 | assert(not isMatch("b/b", "!(*/*)")) 80 | assert(not isMatch("b/c", "!(*/*)")) 81 | assert(not isMatch("a/b", "!(*/b)")) 82 | assert(not isMatch("b/b", "!(*/b)")) 83 | assert(not isMatch("a/b", "!(a/b)")) 84 | assert(not isMatch("a", "!*")) 85 | assert(not isMatch("a.b", "!*")) 86 | assert(not isMatch("a/a", "!*/*")) 87 | assert(not isMatch("a/b", "!*/*")) 88 | assert(not isMatch("a/c", "!*/*")) 89 | assert(not isMatch("b/a", "!*/*")) 90 | assert(not isMatch("b/b", "!*/*")) 91 | assert(not isMatch("b/c", "!*/*")) 92 | assert(not isMatch("a/b", "!*/b")) 93 | assert(not isMatch("b/b", "!*/b")) 94 | assert(not isMatch("a/c", "!*/c")) 95 | assert(not isMatch("a/c", "!*/c")) 96 | assert(not isMatch("b/c", "!*/c")) 97 | assert(not isMatch("b/c", "!*/c")) 98 | assert(not isMatch("bar", "!*a*")) 99 | assert(not isMatch("fab", "!*a*")) 100 | assert(not isMatch("a/a", "!a/(*)")) 101 | assert(not isMatch("a/b", "!a/(*)")) 102 | assert(not isMatch("a/c", "!a/(*)")) 103 | assert(not isMatch("a/b", "!a/(b)")) 104 | assert(not isMatch("a/a", "!a/*")) 105 | assert(not isMatch("a/b", "!a/*")) 106 | assert(not isMatch("a/c", "!a/*")) 107 | assert(not isMatch("fab", "!f*b")) 108 | assert(isMatch("a", "!(*/*)")) 109 | assert(isMatch("a.b", "!(*/*)")) 110 | assert(isMatch("a", "!(*/b)")) 111 | assert(isMatch("a.b", "!(*/b)")) 112 | assert(isMatch("a/a", "!(*/b)")) 113 | assert(isMatch("a/c", "!(*/b)")) 114 | assert(isMatch("b/a", "!(*/b)")) 115 | assert(isMatch("b/c", "!(*/b)")) 116 | assert(isMatch("a", "!(a/b)")) 117 | assert(isMatch("a.b", "!(a/b)")) 118 | assert(isMatch("a/a", "!(a/b)")) 119 | assert(isMatch("a/c", "!(a/b)")) 120 | assert(isMatch("b/a", "!(a/b)")) 121 | assert(isMatch("b/b", "!(a/b)")) 122 | assert(isMatch("b/c", "!(a/b)")) 123 | assert(isMatch("a/a", "!*")) 124 | assert(isMatch("a/b", "!*")) 125 | assert(isMatch("a/c", "!*")) 126 | assert(isMatch("b/a", "!*")) 127 | assert(isMatch("b/b", "!*")) 128 | assert(isMatch("b/c", "!*")) 129 | assert(isMatch("a", "!*/*")) 130 | assert(isMatch("a.b", "!*/*")) 131 | assert(isMatch("a", "!*/b")) 132 | assert(isMatch("a.b", "!*/b")) 133 | assert(isMatch("a/a", "!*/b")) 134 | assert(isMatch("a/c", "!*/b")) 135 | assert(isMatch("b/a", "!*/b")) 136 | assert(isMatch("b/c", "!*/b")) 137 | assert(isMatch("a", "!*/c")) 138 | assert(isMatch("a.b", "!*/c")) 139 | assert(isMatch("a/a", "!*/c")) 140 | assert(isMatch("a/b", "!*/c")) 141 | assert(isMatch("b/a", "!*/c")) 142 | assert(isMatch("b/b", "!*/c")) 143 | assert(isMatch("foo", "!*a*")) 144 | assert(isMatch("a", "!a/(*)")) 145 | assert(isMatch("a.b", "!a/(*)")) 146 | assert(isMatch("b/a", "!a/(*)")) 147 | assert(isMatch("b/b", "!a/(*)")) 148 | assert(isMatch("b/c", "!a/(*)")) 149 | assert(isMatch("a", "!a/(b)")) 150 | assert(isMatch("a.b", "!a/(b)")) 151 | assert(isMatch("a/a", "!a/(b)")) 152 | assert(isMatch("a/c", "!a/(b)")) 153 | assert(isMatch("b/a", "!a/(b)")) 154 | assert(isMatch("b/b", "!a/(b)")) 155 | assert(isMatch("b/c", "!a/(b)")) 156 | assert(isMatch("a", "!a/*")) 157 | assert(isMatch("a.b", "!a/*")) 158 | assert(isMatch("b/a", "!a/*")) 159 | assert(isMatch("b/b", "!a/*")) 160 | assert(isMatch("b/c", "!a/*")) 161 | assert(isMatch("bar", "!f*b")) 162 | assert(isMatch("foo", "!f*b")) 163 | end) 164 | 165 | it("should negate files with extensions", function() 166 | assert(not isMatch(".md", "!.md")) 167 | assert(isMatch("a.js", "!**/*.md")) 168 | assert(not isMatch("b.md", "!**/*.md")) 169 | assert(isMatch("c.txt", "!**/*.md")) 170 | assert(isMatch("a.js", "!*.md")) 171 | assert(not isMatch("b.md", "!*.md")) 172 | assert(isMatch("c.txt", "!*.md")) 173 | assert(not isMatch("abc.md", "!*.md")) 174 | assert(isMatch("abc.txt", "!*.md")) 175 | assert(not isMatch("foo.md", "!*.md")) 176 | assert(isMatch("foo.md", "!.md")) 177 | end) 178 | 179 | it("should support negated single stars", function() 180 | assert(isMatch("a.js", "!*.md")) 181 | assert(isMatch("b.txt", "!*.md")) 182 | assert(not isMatch("c.md", "!*.md")) 183 | assert(not isMatch("a/a/a.js", "!a/*/a.js")) 184 | assert(not isMatch("a/b/a.js", "!a/*/a.js")) 185 | assert(not isMatch("a/c/a.js", "!a/*/a.js")) 186 | assert(not isMatch("a/a/a/a.js", "!a/*/*/a.js")) 187 | assert(isMatch("b/a/b/a.js", "!a/*/*/a.js")) 188 | assert(isMatch("c/a/c/a.js", "!a/*/*/a.js")) 189 | assert(not isMatch("a/a.txt", "!a/a*.txt")) 190 | assert(isMatch("a/b.txt", "!a/a*.txt")) 191 | assert(isMatch("a/c.txt", "!a/a*.txt")) 192 | assert(not isMatch("a.a.txt", "!a.a*.txt")) 193 | assert(isMatch("a.b.txt", "!a.a*.txt")) 194 | assert(isMatch("a.c.txt", "!a.a*.txt")) 195 | assert(not isMatch("a/a.txt", "!a/*.txt")) 196 | assert(not isMatch("a/b.txt", "!a/*.txt")) 197 | assert(not isMatch("a/c.txt", "!a/*.txt")) 198 | end) 199 | 200 | it("should support negated globstars (multiple stars)", function() 201 | assert(isMatch("a.js", "!*.md")) 202 | assert(isMatch("b.txt", "!*.md")) 203 | assert(not isMatch("c.md", "!*.md")) 204 | assert(not isMatch("a/a/a.js", "!**/a.js")) 205 | assert(not isMatch("a/b/a.js", "!**/a.js")) 206 | assert(not isMatch("a/c/a.js", "!**/a.js")) 207 | assert(isMatch("a/a/b.js", "!**/a.js")) 208 | assert(not isMatch("a/a/a/a.js", "!a/**/a.js")) 209 | assert(isMatch("b/a/b/a.js", "!a/**/a.js")) 210 | assert(isMatch("c/a/c/a.js", "!a/**/a.js")) 211 | assert(isMatch("a/b.js", "!**/*.md")) 212 | assert(isMatch("a.js", "!**/*.md")) 213 | assert(not isMatch("a/b.md", "!**/*.md")) 214 | assert(not isMatch("a.md", "!**/*.md")) 215 | assert(not isMatch("a/b.js", "**/*.md")) 216 | assert(not isMatch("a.js", "**/*.md")) 217 | assert(isMatch("a/b.md", "**/*.md")) 218 | assert(isMatch("a.md", "**/*.md")) 219 | assert(isMatch("a/b.js", "!**/*.md")) 220 | assert(isMatch("a.js", "!**/*.md")) 221 | assert(not isMatch("a/b.md", "!**/*.md")) 222 | assert(not isMatch("a.md", "!**/*.md")) 223 | assert(isMatch("a/b.js", "!*.md")) 224 | assert(isMatch("a.js", "!*.md")) 225 | assert(isMatch("a/b.md", "!*.md")) 226 | assert(not isMatch("a.md", "!*.md")) 227 | assert(isMatch("a.js", "!**/*.md")) 228 | assert(not isMatch("b.md", "!**/*.md")) 229 | assert(isMatch("c.txt", "!**/*.md")) 230 | end) 231 | 232 | itFIXME("should not negate when inside quoted strings", function() 233 | assert(not isMatch("foo.md", '"!*".md')) 234 | assert(isMatch('"!*".md', '"!*".md')) 235 | assert(isMatch("!*.md", '"!*".md')) 236 | assert(not isMatch("foo.md", '"!*".md', { keepQuotes = true })) 237 | assert(isMatch('"!*".md', '"!*".md', { keepQuotes = true })) 238 | assert(not isMatch("!*.md", '"!*".md', { keepQuotes = true })) 239 | assert(not isMatch("foo.md", '"**".md')) 240 | assert(isMatch('"**".md', '"**".md')) 241 | assert(isMatch("**.md", '"**".md')) 242 | assert(not isMatch("foo.md", '"**".md', { keepQuotes = true })) 243 | assert(isMatch('"**".md', '"**".md', { keepQuotes = true })) 244 | assert(not isMatch("**.md", '"**".md', { keepQuotes = true })) 245 | end) 246 | 247 | it("should negate dotfiles", function() 248 | assert(not isMatch(".dotfile.md", "!.*.md")) 249 | assert(isMatch(".dotfile.md", "!*.md")) 250 | assert(isMatch(".dotfile.txt", "!*.md")) 251 | assert(isMatch(".dotfile.txt", "!*.md")) 252 | assert(isMatch("a/b/.dotfile", "!*.md")) 253 | assert(not isMatch(".gitignore", "!.gitignore")) 254 | assert(isMatch("a", "!.gitignore")) 255 | assert(isMatch("b", "!.gitignore")) 256 | end) 257 | 258 | it("should not match slashes with a single star", function() 259 | assert(isMatch("foo/bar.md", "!*.md")) 260 | assert(not isMatch("foo.md", "!*.md")) 261 | end) 262 | 263 | it("should match nested directories with globstars", function() 264 | assert(not isMatch("a", "!a/**")) 265 | assert(not isMatch("a/", "!a/**")) 266 | assert(not isMatch("a/b", "!a/**")) 267 | assert(not isMatch("a/b/c", "!a/**")) 268 | assert(isMatch("b", "!a/**")) 269 | assert(isMatch("b/c", "!a/**")) 270 | assert(isMatch("foo", "!f*b")) 271 | assert(isMatch("bar", "!f*b")) 272 | assert(not isMatch("fab", "!f*b")) 273 | end) 274 | end) 275 | end 276 | -------------------------------------------------------------------------------- /src/scan.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/lib/scan.js 2 | 3 | local CurrentModule = script.Parent 4 | local Packages = CurrentModule.Parent 5 | local LuauPolyfill = require(Packages.LuauPolyfill) 6 | local Boolean = LuauPolyfill.Boolean 7 | local String = LuauPolyfill.String 8 | type Object = LuauPolyfill.Object 9 | type Token = any 10 | 11 | local utils = require(CurrentModule.utils) 12 | local Constants = require(CurrentModule.constants) 13 | local CHAR_ASTERISK, CHAR_AT, CHAR_BACKWARD_SLASH, CHAR_COMMA, CHAR_DOT, CHAR_EXCLAMATION_MARK, CHAR_FORWARD_SLASH, CHAR_LEFT_CURLY_BRACE, CHAR_LEFT_PARENTHESES, CHAR_LEFT_SQUARE_BRACKET, CHAR_PLUS, CHAR_QUESTION_MARK, CHAR_RIGHT_CURLY_BRACE, CHAR_RIGHT_PARENTHESES, CHAR_RIGHT_SQUARE_BRACKET = 14 | Constants.CHAR_ASTERISK, --[[ * ]] 15 | Constants.CHAR_AT, --[[ @ ]] 16 | Constants.CHAR_BACKWARD_SLASH, --[[ \ ]] 17 | Constants.CHAR_COMMA, --[[ , ]] 18 | Constants.CHAR_DOT, --[[ . ]] 19 | Constants.CHAR_EXCLAMATION_MARK, --[[ ! ]] 20 | Constants.CHAR_FORWARD_SLASH, --[[ / ]] 21 | Constants.CHAR_LEFT_CURLY_BRACE, --[[ { ]] 22 | Constants.CHAR_LEFT_PARENTHESES, --[[ ( ]] 23 | Constants.CHAR_LEFT_SQUARE_BRACKET, --[[ [ ]] 24 | Constants.CHAR_PLUS, --[[ + ]] 25 | Constants.CHAR_QUESTION_MARK, --[[ ? ]] 26 | Constants.CHAR_RIGHT_CURLY_BRACE, --[[ } ]] 27 | Constants.CHAR_RIGHT_PARENTHESES, --[[ ) ]] 28 | Constants.CHAR_RIGHT_SQUARE_BRACKET --[[ ] ]] 29 | 30 | local function isPathSeparator(code) 31 | return code == CHAR_FORWARD_SLASH or code == CHAR_BACKWARD_SLASH 32 | end 33 | 34 | local function depth(token) 35 | if token.isPrefix ~= true then 36 | token.depth = if Boolean.toJSBoolean(token.isGlobstar) then math.huge else 1 37 | end 38 | end 39 | 40 | --[[* 41 | * Quickly scans a glob pattern and returns an object with a handful of 42 | * useful properties, like `isGlob`, `path` (the leading non-glob, if it exists), 43 | * `glob` (the actual pattern), `negated` (true if the path starts with `!` but not 44 | * with `!(`) and `negatedExtglob` (true if the path starts with `!(`). 45 | * 46 | * ```js 47 | * const pm = require('picomatch'); 48 | * console.log(pm.scan('foo/bar/*.js')); 49 | * { isGlob: true, input: 'foo/bar/*.js', base: 'foo/bar', glob: '*.js' } 50 | * ``` 51 | * @param {String} `str` 52 | * @param {Object} `options` 53 | * @return {Object} Returns an object with tokens and regex source string. 54 | * @api public 55 | ]] 56 | 57 | local function scan(input: string, options: Object?) 58 | local opts: Object = options or {} 59 | 60 | local length = #input + 1 61 | local scanToEnd = opts.parts == true or opts.scanToEnd == true 62 | local slashes = {} 63 | local tokens = {} 64 | local parts = {} 65 | 66 | local str = input 67 | local index = 0 68 | local start = 1 69 | local lastIndex = 1 70 | local isBrace = false 71 | local isBracket = false 72 | local isGlob = false 73 | local isExtglob = false 74 | local isGlobstar = false 75 | local braceEscaped = false 76 | local backslashes = false 77 | local negated = false 78 | local negatedExtglob = false 79 | local finished = false 80 | local braces = 0 81 | local prev 82 | local code 83 | -- ROBLOX FIXME: specify type explicitely to avoid type narrowing 84 | local token: Token = { value = "", depth = 0, isGlob = false } 85 | 86 | local function eos() 87 | return index >= length 88 | end 89 | local function peek() 90 | return String.charCodeAt(str, index + 1) 91 | end 92 | local function advance() 93 | prev = code 94 | index += 1 95 | return String.charCodeAt(str, index) 96 | end 97 | while index < length do 98 | code = advance() 99 | local next 100 | 101 | if code == CHAR_BACKWARD_SLASH then 102 | token.backslashes = true 103 | backslashes = token.backslashes 104 | code = advance() 105 | 106 | if code == CHAR_LEFT_CURLY_BRACE then 107 | braceEscaped = true 108 | end 109 | continue 110 | end 111 | 112 | if braceEscaped == true or code == CHAR_LEFT_CURLY_BRACE then 113 | braces += 1 114 | while eos() ~= true and Boolean.toJSBoolean((function() 115 | code = advance() 116 | return code 117 | end)()) do 118 | if code == CHAR_BACKWARD_SLASH then 119 | token.backslashes = true 120 | backslashes = token.backslashes 121 | advance() 122 | continue 123 | end 124 | 125 | if code == CHAR_LEFT_CURLY_BRACE then 126 | braces += 1 127 | continue 128 | end 129 | 130 | if 131 | braceEscaped ~= true 132 | and code == CHAR_DOT 133 | and (function() 134 | code = advance() 135 | return code 136 | end)() == CHAR_DOT 137 | then 138 | token.isBrace = true 139 | isBrace = token.isBrace 140 | token.isGlob = true 141 | isGlob = token.isGlob 142 | finished = true 143 | 144 | if scanToEnd == true then 145 | continue 146 | end 147 | 148 | break 149 | end 150 | 151 | if braceEscaped ~= true and code == CHAR_COMMA then 152 | token.isBrace = true 153 | isBrace = token.isBrace 154 | token.isGlob = true 155 | isGlob = token.isGlob 156 | finished = true 157 | 158 | if scanToEnd == true then 159 | continue 160 | end 161 | 162 | break 163 | end 164 | 165 | if code == CHAR_RIGHT_CURLY_BRACE then 166 | braces -= 1 167 | 168 | if braces == 0 then 169 | braceEscaped = false 170 | token.isBrace = true 171 | isBrace = token.isBrace 172 | finished = true 173 | break 174 | end 175 | end 176 | end 177 | 178 | if scanToEnd == true then 179 | continue 180 | end 181 | 182 | break 183 | end 184 | 185 | if code == CHAR_FORWARD_SLASH then 186 | table.insert(slashes, index) 187 | table.insert(tokens, token) 188 | token = { value = "", depth = 0, isGlob = false } 189 | 190 | if finished == true then 191 | continue 192 | end 193 | if prev == CHAR_DOT and index == start + 1 then 194 | start += 2 195 | continue 196 | end 197 | 198 | lastIndex = index + 1 199 | continue 200 | end 201 | 202 | if opts.noext ~= true then 203 | local isExtglobChar = code == CHAR_PLUS 204 | or code == CHAR_AT 205 | or code == CHAR_ASTERISK 206 | or code == CHAR_QUESTION_MARK 207 | or code == CHAR_EXCLAMATION_MARK 208 | 209 | if isExtglobChar == true and peek() == CHAR_LEFT_PARENTHESES then 210 | token.isGlob = true 211 | isGlob = token.isGlob 212 | token.isExtglob = true 213 | isExtglob = token.isExtglob 214 | finished = true 215 | if code == CHAR_EXCLAMATION_MARK and index == start then 216 | negatedExtglob = true 217 | end 218 | 219 | if scanToEnd == true then 220 | while 221 | eos() ~= true 222 | and Boolean.toJSBoolean((function() 223 | code = advance() 224 | return code 225 | end)()) 226 | do 227 | if code == CHAR_BACKWARD_SLASH then 228 | token.backslashes = true 229 | backslashes = token.backslashes 230 | code = advance() 231 | continue 232 | end 233 | 234 | if code == CHAR_RIGHT_PARENTHESES then 235 | token.isGlob = true 236 | isGlob = token.isGlob 237 | finished = true 238 | break 239 | end 240 | end 241 | continue 242 | end 243 | break 244 | end 245 | end 246 | 247 | if code == CHAR_ASTERISK then 248 | if prev == CHAR_ASTERISK then 249 | token.isGlobstar = true 250 | isGlobstar = token.isGlobstar 251 | end 252 | token.isGlob = true 253 | isGlob = token.isGlob 254 | finished = true 255 | 256 | if scanToEnd == true then 257 | continue 258 | end 259 | break 260 | end 261 | 262 | if code == CHAR_QUESTION_MARK then 263 | token.isGlob = true 264 | isGlob = token.isGlob 265 | finished = true 266 | 267 | if scanToEnd == true then 268 | continue 269 | end 270 | break 271 | end 272 | 273 | if code == CHAR_LEFT_SQUARE_BRACKET then 274 | while eos() ~= true and Boolean.toJSBoolean((function() 275 | next = advance() 276 | return next 277 | end)()) do 278 | if next == CHAR_BACKWARD_SLASH then 279 | token.backslashes = true 280 | backslashes = token.backslashes 281 | advance() 282 | continue 283 | end 284 | 285 | if next == CHAR_RIGHT_SQUARE_BRACKET then 286 | token.isBracket = true 287 | isBracket = token.isBracket 288 | token.isGlob = true 289 | isGlob = token.isGlob 290 | finished = true 291 | break 292 | end 293 | end 294 | 295 | if scanToEnd == true then 296 | continue 297 | end 298 | 299 | break 300 | end 301 | 302 | if opts.nonegate ~= true and code == CHAR_EXCLAMATION_MARK and index == start then 303 | token.negated = true 304 | negated = token.negated 305 | start += 1 306 | continue 307 | end 308 | 309 | if opts.noparen ~= true and code == CHAR_LEFT_PARENTHESES then 310 | token.isGlob = true 311 | isGlob = token.isGlob 312 | 313 | if scanToEnd == true then 314 | while 315 | eos() ~= true 316 | and Boolean.toJSBoolean((function() 317 | code = advance() 318 | return code 319 | end)()) 320 | do 321 | if code == CHAR_LEFT_PARENTHESES then 322 | token.backslashes = true 323 | backslashes = token.backslashes 324 | code = advance() 325 | continue 326 | end 327 | 328 | if code == CHAR_RIGHT_PARENTHESES then 329 | finished = true 330 | break 331 | end 332 | end 333 | continue 334 | end 335 | break 336 | end 337 | 338 | if isGlob == true then 339 | finished = true 340 | 341 | if scanToEnd == true then 342 | continue 343 | end 344 | 345 | break 346 | end 347 | end 348 | 349 | if opts.noext == true then 350 | isExtglob = false 351 | isGlob = false 352 | end 353 | 354 | local base = str 355 | local prefix = "" 356 | local glob = "" 357 | 358 | if start > 1 then 359 | prefix = String.slice(str, 1, start) 360 | str = String.slice(str, start) 361 | lastIndex -= start 362 | end 363 | 364 | if Boolean.toJSBoolean(base) and isGlob == true and lastIndex > 1 then 365 | base = String.slice(str, 1, lastIndex) 366 | glob = String.slice(str, lastIndex) 367 | elseif isGlob == true then 368 | base = "" 369 | glob = str 370 | else 371 | base = str 372 | end 373 | 374 | if Boolean.toJSBoolean(base) and base ~= "" and base ~= "/" and base ~= str then 375 | if isPathSeparator(String.charCodeAt(base, #base)) then 376 | base = String.slice(base, 1, -1) 377 | end 378 | end 379 | 380 | if opts.unescape == true then 381 | if Boolean.toJSBoolean(glob) then 382 | glob = utils.removeBackslashes(glob) 383 | end 384 | 385 | if Boolean.toJSBoolean(base) and backslashes == true then 386 | base = utils.removeBackslashes(base) 387 | end 388 | end 389 | 390 | local state = { 391 | prefix = prefix, 392 | input = input, 393 | start = start, 394 | base = base, 395 | glob = glob, 396 | isBrace = isBrace, 397 | isBracket = isBracket, 398 | isGlob = isGlob, 399 | isExtglob = isExtglob, 400 | isGlobstar = isGlobstar, 401 | negated = negated, 402 | negatedExtglob = negatedExtglob, 403 | } 404 | 405 | if opts.tokens == true then 406 | state.maxDepth = 0 407 | if not isPathSeparator(code) then 408 | table.insert(tokens, token) 409 | end 410 | state.tokens = tokens 411 | end 412 | 413 | if opts.parts == true or opts.tokens == true then 414 | local prevIndex 415 | 416 | for idx = 1, #slashes do 417 | local n = if Boolean.toJSBoolean(prevIndex) then prevIndex + 1 else start 418 | local i = slashes[idx] 419 | local value = String.slice(input, n, i) 420 | if Boolean.toJSBoolean(opts.tokens) then 421 | if idx == 1 and start ~= 1 then 422 | tokens[idx].isPrefix = true 423 | tokens[idx].value = prefix 424 | else 425 | tokens[idx].value = value 426 | end 427 | depth(tokens[idx]) 428 | state.maxDepth += tokens[idx].depth 429 | end 430 | if idx ~= 1 or value ~= "" then 431 | table.insert(parts, value) 432 | end 433 | prevIndex = i 434 | end 435 | 436 | if Boolean.toJSBoolean(prevIndex) and prevIndex + 1 < #input then 437 | local value = String.slice(input, prevIndex + 1) 438 | table.insert(parts, value) 439 | 440 | if Boolean.toJSBoolean(opts.tokens) then 441 | tokens[#tokens].value = value 442 | depth(tokens[#tokens]) 443 | state.maxDepth += tokens[#tokens].depth 444 | end 445 | end 446 | 447 | state.slashes = slashes 448 | state.parts = parts 449 | end 450 | 451 | return state 452 | end 453 | 454 | return scan 455 | -------------------------------------------------------------------------------- /src/__tests__/options.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/options.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Array = LuauPolyfill.Array 9 | local Object = LuauPolyfill.Object 10 | 11 | local jestExpect = require(Packages.Dev.JestGlobals).expect 12 | 13 | local support = require(CurrentModule.support) 14 | local match = require(CurrentModule.support.match) 15 | local isMatch = require(PicomatchModule).isMatch 16 | describe("options", function() 17 | beforeEach(function() 18 | return support.windowsPathSep() 19 | end) 20 | afterEach(function() 21 | return support.resetPathSep() 22 | end) 23 | 24 | describe("options.matchBase", function() 25 | itFIXME("should match the basename of file paths when `options.matchBase` is true", function() 26 | jestExpect(match({ "a/b/c/d.md" }, "*.md")).toEqual( 27 | {} 28 | -- ROBLOX deviation: jestExpect doesn't accept message 29 | -- , "should not match multiple levels" 30 | ) 31 | jestExpect(match({ "a/b/c/foo.md" }, "*.md")).toEqual( 32 | {} 33 | -- ROBLOX deviation: jestExpect doesn't accept message 34 | --, "should not match multiple levels" 35 | ) 36 | jestExpect(match({ "ab", "acb", "acb/", "acb/d/e", "x/y/acb", "x/y/acb/d" }, "a?b")).toEqual( 37 | { "acb" } 38 | -- ROBLOX deviation: jestExpect doesn't accept message 39 | -- ,"should not match multiple levels" 40 | ) 41 | jestExpect(match({ "a/b/c/d.md" }, "*.md", { matchBase = true })).toEqual({ "a/b/c/d.md" }) 42 | jestExpect(match({ "a/b/c/foo.md" }, "*.md", { matchBase = true })).toEqual({ "a/b/c/foo.md" }) 43 | jestExpect(match({ "x/y/acb", "acb/", "acb/d/e", "x/y/acb/d" }, "a?b", { matchBase = true })).toEqual({ 44 | "x/y/acb", 45 | "acb/", 46 | }) 47 | end) 48 | 49 | itFIXME("should work with negation patterns", function() 50 | assert(isMatch("./x/y.js", "*.js", { matchBase = true })) 51 | assert(not isMatch("./x/y.js", "!*.js", { matchBase = true })) 52 | assert(isMatch("./x/y.js", "**/*.js", { matchBase = true })) 53 | assert(not isMatch("./x/y.js", "!**/*.js", { matchBase = true })) 54 | end) 55 | end) 56 | 57 | describe("options.flags", function() 58 | it("should be case-sensitive by default", function() 59 | jestExpect(match({ "a/b/d/e.md" }, "a/b/D/*.md")).toEqual( 60 | {} 61 | -- ROBLOX deviation: jestExpect doesn't accept message 62 | -- , "should not match a dirname" 63 | ) 64 | jestExpect(match({ "a/b/c/e.md" }, "A/b/*/E.md")).toEqual( 65 | {} 66 | -- ROBLOX deviation: jestExpect doesn't accept message 67 | -- , "should not match a basename" 68 | ) 69 | jestExpect(match({ "a/b/c/e.md" }, "A/b/C/*.MD")).toEqual( 70 | {} 71 | -- ROBLOX deviation: jestExpect doesn't accept message 72 | -- , "should not match a file extension" 73 | ) 74 | end) 75 | 76 | itFIXME("should not be case-sensitive when `i` is set on `options.flags`", function() 77 | jestExpect(match({ "a/b/d/e.md" }, "a/b/D/*.md", { flags = "i" })).toEqual({ "a/b/d/e.md" }) 78 | jestExpect(match({ "a/b/c/e.md" }, "A/b/*/E.md", { flags = "i" })).toEqual({ "a/b/c/e.md" }) 79 | jestExpect(match({ "a/b/c/e.md" }, "A/b/C/*.MD", { flags = "i" })).toEqual({ "a/b/c/e.md" }) 80 | end) 81 | end) 82 | 83 | describe("options.nocase", function() 84 | itFIXME("should not be case-sensitive when `options.nocase` is true", function() 85 | jestExpect(match({ "a/b/c/e.md" }, "A/b/*/E.md", { nocase = true })).toEqual({ "a/b/c/e.md" }) 86 | jestExpect(match({ "a/b/c/e.md" }, "A/b/C/*.MD", { nocase = true })).toEqual({ "a/b/c/e.md" }) 87 | jestExpect(match({ "a/b/c/e.md" }, "A/b/C/*.md", { nocase = true })).toEqual({ "a/b/c/e.md" }) 88 | jestExpect(match({ "a/b/d/e.md" }, "a/b/D/*.md", { nocase = true })).toEqual({ "a/b/d/e.md" }) 89 | end) 90 | 91 | itFIXME("should not double-set `i` when both `nocase` and the `i` flag are set", function() 92 | local opts = { nocase = true, flags = "i" } 93 | jestExpect(match({ "a/b/d/e.md" }, "a/b/D/*.md", opts)).toEqual({ "a/b/d/e.md" }) 94 | jestExpect(match({ "a/b/c/e.md" }, "A/b/*/E.md", opts)).toEqual({ "a/b/c/e.md" }) 95 | jestExpect(match({ "a/b/c/e.md" }, "A/b/C/*.MD", opts)).toEqual({ "a/b/c/e.md" }) 96 | end) 97 | end) 98 | 99 | describe("options.noextglob", function() 100 | it("should match literal parens when noextglob is true (issue #116)", function() 101 | assert(isMatch("a/(dir)", "a/(dir)", { noextglob = true })) 102 | end) 103 | 104 | it("should not match extglobs when noextglob is true", function() 105 | assert(not isMatch("ax", "?(a*|b)", { noextglob = true })) 106 | jestExpect(match({ "a.j.js", "a.md.js" }, "*.*(j).js", { noextglob = true })).toEqual({ "a.j.js" }) 107 | jestExpect(match({ "a/z", "a/b", "a/!(z)" }, "a/!(z)", { noextglob = true })).toEqual({ "a/!(z)" }) 108 | jestExpect(match({ "a/z", "a/b" }, "a/!(z)", { noextglob = true })).toEqual({}) 109 | jestExpect(match({ "c/a/v" }, "c/!(z)/v", { noextglob = true })).toEqual({}) 110 | jestExpect(match({ "c/z/v", "c/a/v" }, "c/!(z)/v", { noextglob = true })).toEqual({}) 111 | jestExpect(match({ "c/z/v", "c/a/v" }, "c/@(z)/v", { noextglob = true })).toEqual({}) 112 | jestExpect(match({ "c/z/v", "c/a/v" }, "c/+(z)/v", { noextglob = true })).toEqual({}) 113 | jestExpect(match({ "c/z/v", "c/a/v" }, "c/*(z)/v", { noextglob = true })).toEqual({ "c/z/v" }) 114 | jestExpect(match({ "c/z/v", "z", "zf", "fz" }, "?(z)", { noextglob = true })).toEqual({ "fz" }) 115 | jestExpect(match({ "c/z/v", "z", "zf", "fz" }, "+(z)", { noextglob = true })).toEqual({}) 116 | jestExpect(match({ "c/z/v", "z", "zf", "fz" }, "*(z)", { noextglob = true })).toEqual({ 117 | "z", 118 | "fz", 119 | }) 120 | jestExpect(match({ "cz", "abz", "az" }, "a@(z)", { noextglob = true })).toEqual({}) 121 | jestExpect(match({ "cz", "abz", "az" }, "a*@(z)", { noextglob = true })).toEqual({}) 122 | jestExpect(match({ "cz", "abz", "az" }, "a!(z)", { noextglob = true })).toEqual({}) 123 | jestExpect(match({ "cz", "abz", "az", "azz" }, "a?(z)", { noextglob = true })).toEqual({ 124 | "abz", 125 | "azz", 126 | }) 127 | jestExpect(match({ "cz", "abz", "az", "azz", "a+z" }, "a+(z)", { noextglob = true })).toEqual({ 128 | "a+z", 129 | }) 130 | jestExpect(match({ "cz", "abz", "az" }, "a*(z)", { noextglob = true })).toEqual({ "abz", "az" }) 131 | jestExpect(match({ "cz", "abz", "az" }, "a**(z)", { noextglob = true })).toEqual({ "abz", "az" }) 132 | jestExpect(match({ "cz", "abz", "az" }, "a*!(z)", { noextglob = true })).toEqual({}) 133 | end) 134 | end) 135 | 136 | describe("options.unescape", function() 137 | itFIXME("should remove backslashes in glob patterns:", function() 138 | local fixtures = { "abc", "/a/b/c", "\\a\\b\\c" } 139 | jestExpect(match(fixtures, "\\a\\b\\c")).toEqual({ "/a/b/c" }) 140 | jestExpect(match(fixtures, "\\a\\b\\c", { unescape = true })).toEqual({ "abc", "/a/b/c" }) 141 | jestExpect(match(fixtures, "\\a\\b\\c", { unescape = false })).toEqual({ "/a/b/c" }) 142 | end) 143 | end) 144 | 145 | describe("options.nonegate", function() 146 | it("should support the `nonegate` option:", function() 147 | jestExpect(match({ "a/a/a", "a/b/a", "b/b/a", "c/c/a", "c/c/b" }, "!**/a")).toEqual({ "c/c/b" }) 148 | jestExpect(match({ "a.md", "!a.md", "a.txt" }, "!*.md", { nonegate = true })).toEqual({ "!a.md" }) 149 | jestExpect( 150 | match({ "!a/a/a", "!a/a", "a/b/a", "b/b/a", "!c/c/a", "!c/a" }, "!**/a", { nonegate = true }) 151 | ).toEqual({ "!a/a", "!c/a" }) 152 | jestExpect(match({ "!*.md", ".dotfile.txt", "a/b/.dotfile" }, "!*.md", { nonegate = true })).toEqual({ 153 | "!*.md", 154 | }) 155 | end) 156 | end) 157 | 158 | describe("options.windows", function() 159 | itFIXME("should windows file paths by default", function() 160 | jestExpect(match({ "a\\b\\c.md" }, "**/*.md")).toEqual({ "a/b/c.md" }) 161 | jestExpect(match({ "a\\b\\c.md" }, "**/*.md", { windows = false })).toEqual({ "a\\b\\c.md" }) 162 | end) 163 | 164 | itFIXME("should windows absolute paths", function() 165 | jestExpect(match({ "E:\\a\\b\\c.md" }, "E:/**/*.md")).toEqual({ "E:/a/b/c.md" }) 166 | jestExpect(match({ "E:\\a\\b\\c.md" }, "E:/**/*.md", { windows = false })).toEqual({}) 167 | end) 168 | 169 | it("should strip leading `./`", function() 170 | local fixtures = Array.sort({ 171 | "./a", 172 | "./a/a/a", 173 | "./a/a/a/a", 174 | "./a/a/a/a/a", 175 | "./a/b", 176 | "./a/x", 177 | "./z/z", 178 | "a", 179 | "a/a", 180 | "a/a/b", 181 | "a/c", 182 | "b", 183 | "x/y", 184 | }) 185 | local function format(str) 186 | return str:gsub("^%./", "") 187 | end 188 | local opts = { format = format } 189 | jestExpect(match(fixtures, "*", opts)).toEqual({ "a", "b" }) 190 | jestExpect(match(fixtures, "**/a/**", opts)).toEqual({ 191 | "a", 192 | "a/a/a", 193 | "a/a/a/a", 194 | "a/a/a/a/a", 195 | "a/b", 196 | "a/x", 197 | "a/a", 198 | "a/a/b", 199 | "a/c", 200 | }) 201 | jestExpect(match(fixtures, "*/*", opts)).toEqual({ "a/b", "a/x", "z/z", "a/a", "a/c", "x/y" }) 202 | jestExpect(match(fixtures, "*/*/*", opts)).toEqual({ "a/a/a", "a/a/b" }) 203 | jestExpect(match(fixtures, "*/*/*/*", opts)).toEqual({ "a/a/a/a" }) 204 | jestExpect(match(fixtures, "*/*/*/*/*", opts)).toEqual({ "a/a/a/a/a" }) 205 | jestExpect(match(fixtures, "./*", opts)).toEqual({ "a", "b" }) 206 | jestExpect(match(fixtures, "./**/a/**", opts)).toEqual({ 207 | "a", 208 | "a/a/a", 209 | "a/a/a/a", 210 | "a/a/a/a/a", 211 | "a/b", 212 | "a/x", 213 | "a/a", 214 | "a/a/b", 215 | "a/c", 216 | }) 217 | jestExpect(match(fixtures, "./a/*/a", opts)).toEqual({ "a/a/a" }) 218 | jestExpect(match(fixtures, "a/*", opts)).toEqual({ "a/b", "a/x", "a/a", "a/c" }) 219 | jestExpect(match(fixtures, "a/*/*", opts)).toEqual({ "a/a/a", "a/a/b" }) 220 | jestExpect(match(fixtures, "a/*/*/*", opts)).toEqual({ "a/a/a/a" }) 221 | jestExpect(match(fixtures, "a/*/*/*/*", opts)).toEqual({ "a/a/a/a/a" }) 222 | jestExpect(match(fixtures, "a/*/a", opts)).toEqual({ "a/a/a" }) 223 | jestExpect(match(fixtures, "*", Object.assign({}, opts, { windows = false }))).toEqual({ 224 | "a", 225 | "b", 226 | }) 227 | jestExpect(match(fixtures, "**/a/**", Object.assign({}, opts, { windows = false }))).toEqual({ 228 | "a", 229 | "a/a/a", 230 | "a/a/a/a", 231 | "a/a/a/a/a", 232 | "a/b", 233 | "a/x", 234 | "a/a", 235 | "a/a/b", 236 | "a/c", 237 | }) 238 | jestExpect(match(fixtures, "*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 239 | "a/b", 240 | "a/x", 241 | "z/z", 242 | "a/a", 243 | "a/c", 244 | "x/y", 245 | }) 246 | jestExpect(match(fixtures, "*/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 247 | "a/a/a", 248 | "a/a/b", 249 | }) 250 | jestExpect(match(fixtures, "*/*/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 251 | "a/a/a/a", 252 | }) 253 | jestExpect(match(fixtures, "*/*/*/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 254 | "a/a/a/a/a", 255 | }) 256 | jestExpect(match(fixtures, "./*", Object.assign({}, opts, { windows = false }))).toEqual({ "a", "b" }) 257 | jestExpect(match(fixtures, "./**/a/**", Object.assign({}, opts, { windows = false }))).toEqual({ 258 | "a", 259 | "a/a/a", 260 | "a/a/a/a", 261 | "a/a/a/a/a", 262 | "a/b", 263 | "a/x", 264 | "a/a", 265 | "a/a/b", 266 | "a/c", 267 | }) 268 | jestExpect(match(fixtures, "./a/*/a", Object.assign({}, opts, { windows = false }))).toEqual({ 269 | "a/a/a", 270 | }) 271 | jestExpect(match(fixtures, "a/*", Object.assign({}, opts, { windows = false }))).toEqual({ 272 | "a/b", 273 | "a/x", 274 | "a/a", 275 | "a/c", 276 | }) 277 | jestExpect(match(fixtures, "a/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 278 | "a/a/a", 279 | "a/a/b", 280 | }) 281 | jestExpect(match(fixtures, "a/*/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 282 | "a/a/a/a", 283 | }) 284 | jestExpect(match(fixtures, "a/*/*/*/*", Object.assign({}, opts, { windows = false }))).toEqual({ 285 | "a/a/a/a/a", 286 | }) 287 | jestExpect(match(fixtures, "a/*/a", Object.assign({}, opts, { windows = false }))).toEqual({ "a/a/a" }) 288 | end) 289 | end) 290 | 291 | describe("windows", function() 292 | itFIXME("should convert file paths to posix slashes", function() 293 | jestExpect(match({ "a\\b\\c.md" }, "**/*.md")).toEqual({ "a/b/c.md" }) 294 | jestExpect(match({ "a\\b\\c.md" }, "**/*.md", { windows = false })).toEqual({ "a\\b\\c.md" }) 295 | end) 296 | 297 | itFIXME("should convert absolute paths to posix slashes", function() 298 | jestExpect(match({ "E:\\a\\b\\c.md" }, "E:/**/*.md")).toEqual({ "E:/a/b/c.md" }) 299 | jestExpect(match({ "E:\\a\\b\\c.md" }, "E:/**/*.md", { windows = false })).toEqual({}) 300 | end) 301 | end) 302 | end) 303 | end 304 | -------------------------------------------------------------------------------- /src/__tests__/stars.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/stars.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | 7 | local isMatch = require(PicomatchModule).isMatch 8 | describe("stars", function() 9 | describe("issue related", function() 10 | it("should respect dots defined in glob pattern (micromatch/#23)", function() 11 | assert(isMatch("z.js", "z*")) 12 | assert(not isMatch("zzjs", "z*.js")) 13 | assert(not isMatch("zzjs", "*z.js")) 14 | end) 15 | end) 16 | 17 | describe("single stars", function() 18 | it("should match anything except slashes and leading dots", function() 19 | assert(not isMatch("a/b/c/z.js", "*.js")) 20 | assert(not isMatch("a/b/z.js", "*.js")) 21 | assert(not isMatch("a/z.js", "*.js")) 22 | assert(isMatch("z.js", "*.js")) 23 | assert(not isMatch("a/.ab", "*/*")) 24 | assert(not isMatch(".ab", "*")) 25 | assert(isMatch("z.js", "z*.js")) 26 | assert(isMatch("a/z", "*/*")) 27 | assert(isMatch("a/z.js", "*/z*.js")) 28 | assert(isMatch("a/z.js", "a/z*.js")) 29 | assert(isMatch("ab", "*")) 30 | assert(isMatch("abc", "*")) 31 | assert(not isMatch("bar", "f*")) 32 | assert(not isMatch("foo", "*r")) 33 | assert(not isMatch("foo", "b*")) 34 | assert(not isMatch("foo/bar", "*")) 35 | assert(isMatch("abc", "*c")) 36 | assert(isMatch("abc", "a*")) 37 | assert(isMatch("abc", "a*c")) 38 | assert(isMatch("bar", "*r")) 39 | assert(isMatch("bar", "b*")) 40 | assert(isMatch("foo", "f*")) 41 | end) 42 | 43 | it("should match spaces", function() 44 | assert(isMatch("one abc two", "*abc*")) 45 | assert(isMatch("a b", "a*b")) 46 | end) 47 | 48 | it("should support multiple non-consecutive stars in a path segment", function() 49 | assert(not isMatch("foo", "*a*")) 50 | assert(isMatch("bar", "*a*")) 51 | assert(isMatch("oneabctwo", "*abc*")) 52 | assert(not isMatch("a-b.c-d", "*-bc-*")) 53 | assert(isMatch("a-b.c-d", "*-*.*-*")) 54 | assert(isMatch("a-b.c-d", "*-b*c-*")) 55 | assert(isMatch("a-b.c-d", "*-b.c-*")) 56 | assert(isMatch("a-b.c-d", "*.*")) 57 | assert(isMatch("a-b.c-d", "*.*-*")) 58 | assert(isMatch("a-b.c-d", "*.*-d")) 59 | assert(isMatch("a-b.c-d", "*.c-*")) 60 | assert(isMatch("a-b.c-d", "*b.*d")) 61 | assert(isMatch("a-b.c-d", "a*.c*")) 62 | assert(isMatch("a-b.c-d", "a-*.*-d")) 63 | assert(isMatch("a.b", "*.*")) 64 | assert(isMatch("a.b", "*.b")) 65 | assert(isMatch("a.b", "a.*")) 66 | assert(isMatch("a.b", "a.b")) 67 | end) 68 | 69 | it("should support multiple stars in a segment", function() 70 | assert(not isMatch("a-b.c-d", "**-bc-**")) 71 | assert(isMatch("a-b.c-d", "**-**.**-**")) 72 | assert(isMatch("a-b.c-d", "**-b**c-**")) 73 | assert(isMatch("a-b.c-d", "**-b.c-**")) 74 | assert(isMatch("a-b.c-d", "**.**")) 75 | assert(isMatch("a-b.c-d", "**.**-**")) 76 | assert(isMatch("a-b.c-d", "**.**-d")) 77 | assert(isMatch("a-b.c-d", "**.c-**")) 78 | assert(isMatch("a-b.c-d", "**b.**d")) 79 | assert(isMatch("a-b.c-d", "a**.c**")) 80 | assert(isMatch("a-b.c-d", "a-**.**-d")) 81 | assert(isMatch("a.b", "**.**")) 82 | assert(isMatch("a.b", "**.b")) 83 | assert(isMatch("a.b", "a.**")) 84 | assert(isMatch("a.b", "a.b")) 85 | end) 86 | 87 | it("should return true when one of the given patterns matches the string", function() 88 | assert(isMatch("/ab", "*/*")) 89 | assert(isMatch(".", ".")) 90 | assert(not isMatch("a/.b", "a/")) 91 | assert(isMatch("/ab", "/*")) 92 | assert(isMatch("/ab", "/??")) 93 | assert(isMatch("/ab", "/?b")) 94 | assert(isMatch("/cd", "/*")) 95 | assert(isMatch("a", "a")) 96 | assert(isMatch("a/.b", "a/.*")) 97 | assert(isMatch("a/b", "?/?")) 98 | assert(isMatch("a/b/c/d/e/j/n/p/o/z/c.md", "a/**/j/**/z/*.md")) 99 | assert(isMatch("a/b/c/d/e/z/c.md", "a/**/z/*.md")) 100 | assert(isMatch("a/b/c/xyz.md", "a/b/c/*.md")) 101 | assert(isMatch("a/b/c/xyz.md", "a/b/c/*.md")) 102 | assert(isMatch("a/b/z/.a", "a/*/z/.a")) 103 | assert(not isMatch("a/b/z/.a", "bz")) 104 | assert(isMatch("a/bb.bb/aa/b.b/aa/c/xyz.md", "a/**/c/*.md")) 105 | assert(isMatch("a/bb.bb/aa/bb/aa/c/xyz.md", "a/**/c/*.md")) 106 | assert(isMatch("a/bb.bb/c/xyz.md", "a/*/c/*.md")) 107 | assert(isMatch("a/bb/c/xyz.md", "a/*/c/*.md")) 108 | assert(isMatch("a/bbbb/c/xyz.md", "a/*/c/*.md")) 109 | assert(isMatch("aaa", "*")) 110 | assert(isMatch("ab", "*")) 111 | assert(isMatch("ab", "ab")) 112 | end) 113 | 114 | it("should return false when the path does not match the pattern", function() 115 | assert(not isMatch("/ab", { "*/" })) 116 | assert(not isMatch("/ab", { "*/a" })) 117 | assert(not isMatch("/ab", { "/" })) 118 | assert(not isMatch("/ab", { "/?" })) 119 | assert(not isMatch("/ab", { "/a" })) 120 | assert(not isMatch("/ab", { "?/?" })) 121 | assert(not isMatch("/ab", { "a/*" })) 122 | assert(not isMatch("a/.b", { "a/" })) 123 | assert(not isMatch("a/b/c", { "a/*" })) 124 | assert(not isMatch("a/b/c", { "a/b" })) 125 | assert(not isMatch("a/b/c/d/e/z/c.md", { "b/c/d/e" })) 126 | assert(not isMatch("a/b/z/.a", { "b/z" })) 127 | assert(not isMatch("ab", { "*/*" })) 128 | assert(not isMatch("ab", { "/a" })) 129 | assert(not isMatch("ab", { "a" })) 130 | assert(not isMatch("ab", { "b" })) 131 | assert(not isMatch("ab", { "c" })) 132 | assert(not isMatch("abcd", { "ab" })) 133 | assert(not isMatch("abcd", { "bc" })) 134 | assert(not isMatch("abcd", { "c" })) 135 | assert(not isMatch("abcd", { "cd" })) 136 | assert(not isMatch("abcd", { "d" })) 137 | assert(not isMatch("abcd", { "f" })) 138 | assert(not isMatch("ef", { "/*" })) 139 | end) 140 | 141 | it("should match a path segment for each single star", function() 142 | assert(not isMatch("aaa", "*/*/*")) 143 | assert(not isMatch("aaa/bb/aa/rr", "*/*/*")) 144 | assert(not isMatch("aaa/bba/ccc", "aaa*")) 145 | assert(not isMatch("aaa/bba/ccc", "aaa**")) 146 | assert(not isMatch("aaa/bba/ccc", "aaa/*")) 147 | assert(not isMatch("aaa/bba/ccc", "aaa/*ccc")) 148 | assert(not isMatch("aaa/bba/ccc", "aaa/*z")) 149 | assert(not isMatch("aaa/bbb", "*/*/*")) 150 | assert(not isMatch("ab/zzz/ejkl/hi", "*/*jk*/*i")) 151 | assert(isMatch("aaa/bba/ccc", "*/*/*")) 152 | assert(isMatch("aaa/bba/ccc", "aaa/**")) 153 | assert(isMatch("aaa/bbb", "aaa/*")) 154 | assert(isMatch("ab/zzz/ejkl/hi", "*/*z*/*/*i")) 155 | assert(isMatch("abzzzejklhi", "*j*i")) 156 | end) 157 | 158 | it("should support single globs (*)", function() 159 | assert(isMatch("a", "*")) 160 | assert(isMatch("b", "*")) 161 | assert(not isMatch("a/a", "*")) 162 | assert(not isMatch("a/a/a", "*")) 163 | assert(not isMatch("a/a/b", "*")) 164 | assert(not isMatch("a/a/a/a", "*")) 165 | assert(not isMatch("a/a/a/a/a", "*")) 166 | assert(not isMatch("a", "*/*")) 167 | assert(isMatch("a/a", "*/*")) 168 | assert(not isMatch("a/a/a", "*/*")) 169 | assert(not isMatch("a", "*/*/*")) 170 | assert(not isMatch("a/a", "*/*/*")) 171 | assert(isMatch("a/a/a", "*/*/*")) 172 | assert(not isMatch("a/a/a/a", "*/*/*")) 173 | assert(not isMatch("a", "*/*/*/*")) 174 | assert(not isMatch("a/a", "*/*/*/*")) 175 | assert(not isMatch("a/a/a", "*/*/*/*")) 176 | assert(isMatch("a/a/a/a", "*/*/*/*")) 177 | assert(not isMatch("a/a/a/a/a", "*/*/*/*")) 178 | assert(not isMatch("a", "*/*/*/*/*")) 179 | assert(not isMatch("a/a", "*/*/*/*/*")) 180 | assert(not isMatch("a/a/a", "*/*/*/*/*")) 181 | assert(not isMatch("a/a/b", "*/*/*/*/*")) 182 | assert(not isMatch("a/a/a/a", "*/*/*/*/*")) 183 | assert(isMatch("a/a/a/a/a", "*/*/*/*/*")) 184 | assert(not isMatch("a/a/a/a/a/a", "*/*/*/*/*")) 185 | assert(not isMatch("a", "a/*")) 186 | assert(isMatch("a/a", "a/*")) 187 | assert(not isMatch("a/a/a", "a/*")) 188 | assert(not isMatch("a/a/a/a", "a/*")) 189 | assert(not isMatch("a/a/a/a/a", "a/*")) 190 | assert(not isMatch("a", "a/*/*")) 191 | assert(not isMatch("a/a", "a/*/*")) 192 | assert(isMatch("a/a/a", "a/*/*")) 193 | assert(not isMatch("b/a/a", "a/*/*")) 194 | assert(not isMatch("a/a/a/a", "a/*/*")) 195 | assert(not isMatch("a/a/a/a/a", "a/*/*")) 196 | assert(not isMatch("a", "a/*/*/*")) 197 | assert(not isMatch("a/a", "a/*/*/*")) 198 | assert(not isMatch("a/a/a", "a/*/*/*")) 199 | assert(isMatch("a/a/a/a", "a/*/*/*")) 200 | assert(not isMatch("a/a/a/a/a", "a/*/*/*")) 201 | assert(not isMatch("a", "a/*/*/*/*")) 202 | assert(not isMatch("a/a", "a/*/*/*/*")) 203 | assert(not isMatch("a/a/a", "a/*/*/*/*")) 204 | assert(not isMatch("a/a/b", "a/*/*/*/*")) 205 | assert(not isMatch("a/a/a/a", "a/*/*/*/*")) 206 | assert(isMatch("a/a/a/a/a", "a/*/*/*/*")) 207 | assert(not isMatch("a", "a/*/a")) 208 | assert(not isMatch("a/a", "a/*/a")) 209 | assert(isMatch("a/a/a", "a/*/a")) 210 | assert(not isMatch("a/a/b", "a/*/a")) 211 | assert(not isMatch("a/a/a/a", "a/*/a")) 212 | assert(not isMatch("a/a/a/a/a", "a/*/a")) 213 | assert(not isMatch("a", "a/*/b")) 214 | assert(not isMatch("a/a", "a/*/b")) 215 | assert(not isMatch("a/a/a", "a/*/b")) 216 | assert(isMatch("a/a/b", "a/*/b")) 217 | assert(not isMatch("a/a/a/a", "a/*/b")) 218 | assert(not isMatch("a/a/a/a/a", "a/*/b")) 219 | end) 220 | 221 | it("should only match a single folder per star when globstars are used", function() 222 | assert(not isMatch("a", "*/**/a")) 223 | assert(not isMatch("a/a/b", "*/**/a")) 224 | assert(isMatch("a/a", "*/**/a")) 225 | assert(isMatch("a/a/a", "*/**/a")) 226 | assert(isMatch("a/a/a/a", "*/**/a")) 227 | assert(isMatch("a/a/a/a/a", "*/**/a")) 228 | end) 229 | 230 | it("should not match a trailing slash when a star is last char", function() 231 | assert(not isMatch("a", "*/")) 232 | assert(not isMatch("a", "*/*")) 233 | assert(not isMatch("a", "a/*")) 234 | assert(not isMatch("a/", "*/*")) 235 | assert(not isMatch("a/", "a/*")) 236 | assert(not isMatch("a/a", "*")) 237 | assert(not isMatch("a/a", "*/")) 238 | assert(not isMatch("a/x/y", "*/")) 239 | assert(not isMatch("a/x/y", "*/*")) 240 | assert(not isMatch("a/x/y", "a/*")) 241 | assert(not isMatch("a/", "*", { strictSlashes = true })) 242 | assert(isMatch("a/", "*")) 243 | assert(isMatch("a", "*")) 244 | assert(isMatch("a/", "*/")) 245 | assert(isMatch("a/", "*{,/}")) 246 | assert(isMatch("a/a", "*/*")) 247 | assert(isMatch("a/a", "a/*")) 248 | end) 249 | 250 | it("should work with file extensions", function() 251 | assert(not isMatch("a.txt", "a/**/*.txt")) 252 | assert(isMatch("a/x/y.txt", "a/**/*.txt")) 253 | assert(not isMatch("a/x/y/z", "a/**/*.txt")) 254 | assert(not isMatch("a.txt", "a/*.txt")) 255 | assert(isMatch("a/b.txt", "a/*.txt")) 256 | assert(not isMatch("a/x/y.txt", "a/*.txt")) 257 | assert(not isMatch("a/x/y/z", "a/*.txt")) 258 | assert(isMatch("a.txt", "a*.txt")) 259 | assert(not isMatch("a/b.txt", "a*.txt")) 260 | assert(not isMatch("a/x/y.txt", "a*.txt")) 261 | assert(not isMatch("a/x/y/z", "a*.txt")) 262 | assert(isMatch("a.txt", "*.txt")) 263 | assert(not isMatch("a/b.txt", "*.txt")) 264 | assert(not isMatch("a/x/y.txt", "*.txt")) 265 | assert(not isMatch("a/x/y/z", "*.txt")) 266 | end) 267 | 268 | it("should not match slashes when globstars are not exclusive in a path segment", function() 269 | assert(not isMatch("foo/baz/bar", "foo**bar")) 270 | assert(isMatch("foobazbar", "foo**bar")) 271 | end) 272 | 273 | it("should match slashes when defined in braces", function() 274 | assert(isMatch("foo", "foo{,/**}")) 275 | end) 276 | 277 | it("should correctly match slashes", function() 278 | assert(not isMatch("a/b", "a*")) 279 | assert(not isMatch("a/a/bb", "a/**/b")) 280 | assert(not isMatch("a/bb", "a/**/b")) 281 | assert(not isMatch("foo", "*/**")) 282 | assert(not isMatch("foo/bar", "**/")) 283 | assert(not isMatch("foo/bar", "**/*/")) 284 | assert(not isMatch("foo/bar", "*/*/")) 285 | assert(not isMatch("foo/bar/", "**/*", { strictSlashes = true })) 286 | assert(isMatch("/home/foo/..", "**/..")) 287 | assert(isMatch("a", "**/a")) 288 | assert(isMatch("a/a", "**")) 289 | assert(isMatch("a/a", "a/**")) 290 | assert(isMatch("a/", "a/**")) 291 | assert(isMatch("a", "a/**")) 292 | assert(not isMatch("a/a", "**/")) 293 | assert(isMatch("a", "**/a/**")) 294 | assert(isMatch("a", "a/**")) 295 | assert(not isMatch("a/a", "**/")) 296 | assert(isMatch("a/a", "*/**/a")) 297 | assert(isMatch("a", "a/**")) 298 | assert(isMatch("foo/", "*/**")) 299 | assert(isMatch("foo/bar", "**/*")) 300 | assert(isMatch("foo/bar", "*/*")) 301 | assert(isMatch("foo/bar", "*/**")) 302 | assert(isMatch("foo/bar/", "**/")) 303 | assert(isMatch("foo/bar/", "**/*")) 304 | assert(isMatch("foo/bar/", "**/*/")) 305 | assert(isMatch("foo/bar/", "*/**")) 306 | assert(isMatch("foo/bar/", "*/*/")) 307 | assert(not isMatch("bar/baz/foo", "*/foo")) 308 | assert(not isMatch("deep/foo/bar", "**/bar/*")) 309 | assert(not isMatch("deep/foo/bar/baz/x", "*/bar/**")) 310 | assert(not isMatch("ef", "/*")) 311 | assert(not isMatch("foo/bar", "foo?bar")) 312 | assert(not isMatch("foo/bar/baz", "**/bar*")) 313 | assert(not isMatch("foo/bar/baz", "**/bar**")) 314 | assert(not isMatch("foo/baz/bar", "foo**bar")) 315 | assert(not isMatch("foo/baz/bar", "foo*bar")) 316 | assert(isMatch("foo", "foo/**")) 317 | assert(isMatch("/ab", "/*")) 318 | assert(isMatch("/cd", "/*")) 319 | assert(isMatch("/ef", "/*")) 320 | assert(isMatch("a/b/j/c/z/x.md", "a/**/j/**/z/*.md")) 321 | assert(isMatch("a/j/z/x.md", "a/**/j/**/z/*.md")) 322 | assert(isMatch("bar/baz/foo", "**/foo")) 323 | assert(isMatch("deep/foo/bar/baz", "**/bar/*")) 324 | assert(isMatch("deep/foo/bar/baz/", "**/bar/**")) 325 | assert(isMatch("deep/foo/bar/baz/x", "**/bar/*/*")) 326 | assert(isMatch("foo/b/a/z/bar", "foo/**/**/bar")) 327 | assert(isMatch("foo/b/a/z/bar", "foo/**/bar")) 328 | assert(isMatch("foo/bar", "foo/**/**/bar")) 329 | assert(isMatch("foo/bar", "foo/**/bar")) 330 | assert(isMatch("foo/bar/baz/x", "*/bar/**")) 331 | assert(isMatch("foo/baz/bar", "foo/**/**/bar")) 332 | assert(isMatch("foo/baz/bar", "foo/**/bar")) 333 | assert(isMatch("XXX/foo", "**/foo")) 334 | end) 335 | 336 | it('should ignore leading "./" when defined on pattern', function() 337 | assert(isMatch("ab", "./*")) 338 | assert(not isMatch("ab", "./*/")) 339 | assert(isMatch("ab/", "./*/")) 340 | end) 341 | 342 | it("should optionally match trailing slashes with braces", function() 343 | assert(isMatch("foo", "**/*")) 344 | assert(isMatch("foo", "**/*{,/}")) 345 | assert(isMatch("foo/", "**/*{,/}")) 346 | assert(isMatch("foo/bar", "**/*{,/}")) 347 | assert(isMatch("foo/bar/", "**/*{,/}")) 348 | end) 349 | end) 350 | end) 351 | end 352 | -------------------------------------------------------------------------------- /src/picomatch.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/lib/picomatch.js 2 | 3 | local CurrentModule = script.Parent 4 | local Packages = CurrentModule.Parent 5 | local LuauPolyfill = require(Packages.LuauPolyfill) 6 | local Array = LuauPolyfill.Array 7 | local Boolean = LuauPolyfill.Boolean 8 | local Error = LuauPolyfill.Error 9 | local Object = LuauPolyfill.Object 10 | type Array = LuauPolyfill.Array 11 | type Object = LuauPolyfill.Object 12 | 13 | local RegExp = require(Packages.RegExp) 14 | type RegExp = RegExp.RegExp 15 | 16 | -- ROBLOX deviation: skipping path 17 | -- local path = require("path") 18 | local scan = require(CurrentModule.scan) 19 | local parse = require(CurrentModule.parse) 20 | local utils = require(CurrentModule.utils) 21 | local constants = require(CurrentModule.constants) 22 | local function isObject(val) 23 | return typeof(val) == "table" and not Array.isArray(val) 24 | end 25 | 26 | --[[* 27 | * Creates a matcher function from one or more glob patterns. The 28 | * returned function takes a string to match as its first argument, 29 | * and returns true if the string is a match. The returned matcher 30 | * function also takes a boolean as the second argument that, when true, 31 | * returns an object with additional information. 32 | * 33 | * ```js 34 | * const picomatch = require('picomatch'); 35 | * // picomatch(glob[, options]); 36 | * 37 | * const isMatch = picomatch('*.!(*a)'); 38 | * console.log(isMatch('a.a')); //=> false 39 | * console.log(isMatch('a.b')); //=> true 40 | * ``` 41 | * @name picomatch 42 | * @param {String|Array} `globs` One or more glob patterns. 43 | * @param {Object=} `options` 44 | * @return {Function=} Returns a matcher function. 45 | * @api public 46 | ]] 47 | 48 | -- ROBLOX deviation START: defining picomatch as callable table 49 | local picomatch_ 50 | local picomatch = setmetatable({}, { 51 | __call = function(_self: any, glob: any, options: Object?, returnState: boolean?) 52 | return picomatch_(glob, options, returnState) 53 | end, 54 | }) 55 | 56 | function picomatch_( 57 | glob: any, 58 | options: Object?, 59 | returnState_: boolean? 60 | ): typeof(setmetatable({ state = {} :: any }, { 61 | __call = function(_self: any, str: string, returnObject: boolean?): any 62 | return nil :: any 63 | end, 64 | })) -- ROBLOX deviation END 65 | local returnState = returnState_ or false 66 | 67 | if Array.isArray(glob) then 68 | local fns = Array.map(glob, function(input) 69 | return picomatch(input, options, returnState) 70 | end) 71 | -- ROBLOX deviation START: defining arrayMatcher as callable table 72 | local arrayMatcher_ 73 | local arrayMatcher = setmetatable({}, { 74 | __call = function(_self: any, str: string) 75 | return arrayMatcher_(str) 76 | end, 77 | }) 78 | 79 | function arrayMatcher_(str: string) -- ROBLOX deviation: END 80 | for _, isMatch in ipairs(fns) do 81 | local state = isMatch(str) 82 | if state then 83 | return state 84 | end 85 | end 86 | return false 87 | end 88 | return arrayMatcher 89 | end 90 | 91 | local isState = isObject(glob) and Boolean.toJSBoolean(glob.tokens) and Boolean.toJSBoolean(glob.input) 92 | 93 | if glob == "" or typeof(glob) ~= "string" and not isState then 94 | error(Error.new("TypeError: Expected pattern to be a non-empty string")) 95 | end 96 | 97 | local opts = options or {} :: Object 98 | local posix = utils.isWindows(options) 99 | -- ROBLOX Luau FIXME: needs normalization to avoid Type 'RegExp' could not be converted into 'RegExp & {| state: any? |}' 100 | local regex: RegExp & { state: any? } = if isState 101 | then picomatch.compileRe(glob, options) :: any 102 | else picomatch.makeRe(glob, options, false, true) 103 | 104 | local state = regex.state 105 | regex.state = nil 106 | 107 | local isIgnored: (str: string) -> boolean 108 | function isIgnored(_) 109 | return false 110 | end 111 | if Boolean.toJSBoolean(opts.ignore) then 112 | local ignoreOpts = Object.assign( 113 | {}, 114 | options, 115 | { ignore = Object.None, onMatch = Object.None, onResult = Object.None } 116 | ) 117 | -- ROBLOX FIXME Luau: Callable table can't be assigned to a function 118 | isIgnored = picomatch(opts.ignore, ignoreOpts, returnState) :: any 119 | end 120 | 121 | -- ROBLOX deviation START: defining matcher as callable table 122 | local matcher_ 123 | local matcher = setmetatable({}, { 124 | __call = function(_self: any, input: string, returnObject: boolean?) 125 | return matcher_(input, returnObject) 126 | end, 127 | }) 128 | 129 | function matcher_(input: string, returnObject_: boolean?) -- ROBLOX deviation END 130 | local returnObject = returnObject_ or false 131 | 132 | local ref = picomatch.test(input, regex, options, { glob = glob, posix = posix }) 133 | local isMatch, match, output = ref.isMatch, ref.match, ref.output 134 | local result = { 135 | glob = glob, 136 | state = state, 137 | regex = regex, 138 | posix = posix, 139 | input = input, 140 | output = output, 141 | match = match, 142 | isMatch = isMatch, 143 | } 144 | 145 | if typeof(opts.onResult) == "function" then 146 | opts.onResult(result) 147 | end 148 | 149 | if isMatch == false then 150 | result.isMatch = false 151 | return if returnObject then result else false 152 | end 153 | 154 | if isIgnored(input) then 155 | if typeof(opts.onIgnore) == "function" then 156 | opts.onIgnore(result) 157 | end 158 | result.isMatch = false 159 | return if returnObject then result else false 160 | end 161 | 162 | if typeof(opts.onMatch) == "function" then 163 | opts.onMatch(result) 164 | end 165 | return if returnObject then result else true 166 | end 167 | 168 | if Boolean.toJSBoolean(returnState) then 169 | matcher.state = state 170 | end 171 | return matcher 172 | end 173 | 174 | --[[* 175 | * Test `input` with the given `regex`. This is used by the main 176 | * `picomatch()` function to test the input string. 177 | * 178 | * ```js 179 | * const picomatch = require('picomatch'); 180 | * // picomatch.test(input, regex[, options]); 181 | * 182 | * console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\/([^/]*?))$/)); 183 | * // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' } 184 | * ``` 185 | * @param {String} `input` String to test. 186 | * @param {RegExp} `regex` 187 | * @return {Object} Returns an object with matching info. 188 | * @api public 189 | ]] 190 | 191 | function picomatch.test(input: string, regex: RegExp, options: Object?, ref_: Object?) 192 | local ref: Object = ref_ or {} 193 | local glob, posix = ref.glob, ref.posix 194 | 195 | if typeof(input) ~= "string" then 196 | error(Error.new("TypeError: Expected input to be a string")) 197 | end 198 | 199 | if input == "" then 200 | return { isMatch = false, output = "" } 201 | end 202 | 203 | local opts: Object = options or {} 204 | local format = if Boolean.toJSBoolean(opts.format) 205 | then opts.format 206 | else if Boolean.toJSBoolean(posix) then utils.toPosixSlashes else nil 207 | local match = input == glob 208 | local output = if match and Boolean.toJSBoolean(format) then format(input) else input 209 | 210 | if match == false then 211 | output = if Boolean.toJSBoolean(format) then format(input) else input 212 | match = output == glob 213 | end 214 | 215 | if match == false or opts.capture == true then 216 | if opts.matchBase == true or opts.basename == true then 217 | match = picomatch.matchBase(input, regex, options, posix) 218 | else 219 | -- ROBLOX FIXME: Luau narrows the type to boolean and doesn't allow for result of RegExp:exec to be assigned 220 | match = regex:exec(output) :: any 221 | end 222 | end 223 | 224 | return { isMatch = Boolean.toJSBoolean(match), match = match, output = output } 225 | end 226 | 227 | --[[* 228 | * Match the basename of a filepath. 229 | * 230 | * ```js 231 | * const picomatch = require('picomatch'); 232 | * // picomatch.matchBase(input, glob[, options]); 233 | * console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true 234 | * ``` 235 | * @param {String} `input` String to test. 236 | * @param {RegExp|String} `glob` Glob pattern or regex created by [.makeRe](#makeRe). 237 | * @return {Boolean} 238 | * @api public 239 | ]] 240 | 241 | -- ROBLOX TODO START: implement when possible 242 | function picomatch.matchBase(input, glob, options, posix_: boolean?): boolean 243 | error("matchBase not implemented") 244 | -- local _posix = posix_ or utils.isWindows(options) 245 | 246 | -- local regex = if instanceof(glob, RegExp) then glob else picomatch.makeRe(glob, options) 247 | -- -- ROBLOX FIXME: return regex:test(path:basename(input)) 248 | -- return regex:test(input) 249 | end 250 | -- ROBLOX TODO END 251 | 252 | --[[* 253 | * Returns true if **any** of the given glob `patterns` match the specified `string`. 254 | * 255 | * ```js 256 | * const picomatch = require('picomatch'); 257 | * // picomatch.isMatch(string, patterns[, options]); 258 | * 259 | * console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true 260 | * console.log(picomatch.isMatch('a.a', 'b.*')); //=> false 261 | * ``` 262 | * @param {String|Array} str The string to test. 263 | * @param {String|Array} patterns One or more glob patterns to use for matching. 264 | * @param {Object} [options] See available [options](#options). 265 | * @return {Boolean} Returns true if any patterns match `str` 266 | * @api public 267 | ]] 268 | 269 | function picomatch.isMatch(str: string, patterns, options: Object?) 270 | return picomatch(patterns, options)(str) 271 | end 272 | 273 | --[[* 274 | * Parse a glob pattern to create the source string for a regular 275 | * expression. 276 | * 277 | * ```js 278 | * const picomatch = require('picomatch'); 279 | * const result = picomatch.parse(pattern[, options]); 280 | * ``` 281 | * @param {String} `pattern` 282 | * @param {Object} `options` 283 | * @return {Object} Returns an object with useful properties and output to be used as a regex source string. 284 | * @api public 285 | ]] 286 | 287 | function picomatch.parse(pattern, options: Object?) 288 | if Array.isArray(pattern) then 289 | return Array.map(pattern, function(p) 290 | return picomatch.parse(p, options) 291 | end) 292 | end 293 | return parse(pattern, Object.assign({}, options, { fastpaths = false })) 294 | end 295 | 296 | --[[* 297 | * Scan a glob pattern to separate the pattern into segments. 298 | * 299 | * ```js 300 | * const picomatch = require('picomatch'); 301 | * // picomatch.scan(input[, options]); 302 | * 303 | * const result = picomatch.scan('!./foo/*.js'); 304 | * console.log(result); 305 | * { prefix: '!./', 306 | * input: '!./foo/*.js', 307 | * start: 3, 308 | * base: 'foo', 309 | * glob: '*.js', 310 | * isBrace: false, 311 | * isBracket: false, 312 | * isGlob: true, 313 | * isExtglob: false, 314 | * isGlobstar: false, 315 | * negated: true } 316 | * ``` 317 | * @param {String} `input` Glob pattern to scan. 318 | * @param {Object} `options` 319 | * @return {Object} Returns an object with 320 | * @api public 321 | ]] 322 | 323 | function picomatch.scan(input, options: Object?) 324 | return scan(input, options) 325 | end 326 | 327 | --[[* 328 | * Compile a regular expression from the `state` object returned by the 329 | * [parse()](#parse) method. 330 | * 331 | * @param {Object} `state` 332 | * @param {Object} `options` 333 | * @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser. 334 | * @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging. 335 | * @return {RegExp} 336 | * @api public 337 | ]] 338 | 339 | picomatch.compileRe = function(state: Object, options: Object?, returnOutput_: boolean?, returnState_: boolean?): RegExp 340 | local returnOutput = returnOutput_ or false 341 | local returnState = returnState_ or false 342 | 343 | if returnOutput == true then 344 | return state.output 345 | end 346 | 347 | local opts: Object = options or {} 348 | local prepend = if Boolean.toJSBoolean(opts.contains) then "" else "^" 349 | local append = if Boolean.toJSBoolean(opts.contains) then "" else "$" 350 | 351 | local source = ("%s(?:%s)%s"):format(prepend, tostring(state.output), append) 352 | if typeof(state) == "table" and state.negated == true then 353 | source = ("^(?!%s).*$"):format(source) 354 | end 355 | 356 | local regex = picomatch.toRegex(source, options) 357 | if returnState == true then 358 | regex.state = state 359 | end 360 | 361 | return regex 362 | end 363 | 364 | --[[* 365 | * Create a regular expression from a parsed glob pattern. 366 | * 367 | * ```js 368 | * const picomatch = require('picomatch'); 369 | * const state = picomatch.parse('*.js'); 370 | * // picomatch.compileRe(state[, options]); 371 | * 372 | * console.log(picomatch.compileRe(state)); 373 | * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ 374 | * ``` 375 | * @param {String} `state` The object returned from the `.parse` method. 376 | * @param {Object} `options` 377 | * @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result. 378 | * @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression. 379 | * @return {RegExp} Returns a regex created from the given pattern. 380 | * @api public 381 | ]] 382 | 383 | function picomatch.makeRe(input: string, options_: Object?, returnOutput_: boolean?, returnState_: boolean?): RegExp 384 | local options: Object = options_ or {} 385 | local returnOutput = returnOutput_ or false 386 | local returnState = returnState_ or false 387 | 388 | if not Boolean.toJSBoolean(input) or typeof(input) ~= "string" then 389 | error(Error.new("TypeError: Expected a non-empty string")) 390 | end 391 | 392 | local parsed = { negated = false, fastpaths = true } 393 | 394 | if options.fastpaths ~= false and (string.sub(input, 1, 1) == "." or string.sub(input, 1, 1) == "*") then 395 | parsed.output = parse.fastpaths(input, options) 396 | end 397 | 398 | if not Boolean.toJSBoolean(parsed.output) then 399 | parsed = parse(input, options) 400 | end 401 | 402 | return picomatch.compileRe(parsed, options, returnOutput, returnState) 403 | end 404 | 405 | --[[* 406 | * Create a regular expression from the given regex source string. 407 | * 408 | * ```js 409 | * const picomatch = require('picomatch'); 410 | * // picomatch.toRegex(source[, options]); 411 | * 412 | * const { output } = picomatch.parse('*.js'); 413 | * console.log(picomatch.toRegex(output)); 414 | * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ 415 | * ``` 416 | * @param {String} `source` Regular expression source string. 417 | * @param {Object} `options` 418 | * @return {RegExp} 419 | * @api public 420 | ]] 421 | 422 | function picomatch.toRegex(source: string, options: Object?) 423 | local ok, result = pcall(function() 424 | local opts: Object = options or {} 425 | return RegExp( 426 | source, 427 | Boolean.toJSBoolean(opts.flags) and opts.flags or if Boolean.toJSBoolean(opts.nocase) then "i" else "" 428 | ) 429 | end) 430 | if not ok then 431 | local err = result 432 | if options ~= nil and options.debug == true then 433 | error(err) 434 | end 435 | return RegExp("$^") 436 | end 437 | return result 438 | end 439 | 440 | --[[* 441 | * Picomatch constants. 442 | * @return {Object} 443 | ]] 444 | 445 | picomatch.constants = constants 446 | 447 | --[[* 448 | * Expose "picomatch" 449 | ]] 450 | 451 | return picomatch 452 | -------------------------------------------------------------------------------- /src/__tests__/regex-features.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/regex-features.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | local LuauPolyfill = require(Packages.LuauPolyfill) 8 | local Boolean = LuauPolyfill.Boolean 9 | -- ROBLOX deviation: not supported in Lua 10 | -- local version = process.version 11 | 12 | local utils = require(PicomatchModule.utils) 13 | local isMatch = require(PicomatchModule).isMatch 14 | describe("regex features", function() 15 | describe("word boundaries", function() 16 | itFIXME("should support word boundaries", function() 17 | assert(isMatch("a", "a\\b")) 18 | end) 19 | 20 | itFIXME("should support word boundaries in parens", function() 21 | assert(isMatch("a", "(a\\b)")) 22 | end) 23 | end) 24 | 25 | describe("regex lookarounds", function() 26 | it("should support regex lookbehinds", function() 27 | if Boolean.toJSBoolean(utils.supportsLookbehinds()) then 28 | assert(isMatch("foo/cbaz", "foo/*(??@[\\]^_`{|}~ \\t\\r\\n\\v\\fA-ZA-Fa-f0-9]" 42 | ) 43 | jestExpect( 44 | convert("[^[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:lower:][:space:][:upper:][:xdigit:]]") 45 | ).toEqual("(?=.)[^a-zA-Z0-9a-zA-Z \\t\\x00-\\x1F\\x7F0-9a-z \\t\\r\\n\\v\\fA-ZA-Fa-f0-9]") 46 | jestExpect(convert("[a-c[:digit:]x-z]")).toEqual("(?=.)[a-c0-9x-z]") 47 | jestExpect(convert("[_[:alpha:]][_[:alnum:]][_[:alnum:]]*")).toEqual( 48 | "(?=.)[_a-zA-Z][_a-zA-Z0-9][_a-zA-Z0-9]*", 49 | {} 50 | ) 51 | end) 52 | end) 53 | 54 | describe(".isMatch", function() 55 | itFIXME("should support POSIX.2 character classes", function() 56 | assert(isMatch("e", "[[:xdigit:]]")) 57 | assert(isMatch("a", "[[:alpha:]123]")) 58 | assert(isMatch("1", "[[:alpha:]123]")) 59 | assert(not isMatch("5", "[[:alpha:]123]")) 60 | assert(isMatch("A", "[[:alpha:]123]")) 61 | assert(isMatch("A", "[[:alpha:]]")) 62 | assert(not isMatch("9", "[[:alpha:]]")) 63 | assert(isMatch("b", "[[:alpha:]]")) 64 | assert(not isMatch("A", "[![:alpha:]]")) 65 | assert(isMatch("9", "[![:alpha:]]")) 66 | assert(not isMatch("b", "[![:alpha:]]")) 67 | assert(not isMatch("A", "[^[:alpha:]]")) 68 | assert(isMatch("9", "[^[:alpha:]]")) 69 | assert(not isMatch("b", "[^[:alpha:]]")) 70 | assert(not isMatch("A", "[[:digit:]]")) 71 | assert(isMatch("9", "[[:digit:]]")) 72 | assert(not isMatch("b", "[[:digit:]]")) 73 | assert(isMatch("A", "[^[:digit:]]")) 74 | assert(not isMatch("9", "[^[:digit:]]")) 75 | assert(isMatch("b", "[^[:digit:]]")) 76 | assert(isMatch("A", "[![:digit:]]")) 77 | assert(not isMatch("9", "[![:digit:]]")) 78 | assert(isMatch("b", "[![:digit:]]")) 79 | assert(isMatch("a", "[[:lower:]]")) 80 | assert(not isMatch("A", "[[:lower:]]")) 81 | assert(not isMatch("9", "[[:lower:]]")) 82 | assert(isMatch("a", "[:alpha:]"), "invalid posix bracket, but valid char class") 83 | assert(isMatch("l", "[:alpha:]"), "invalid posix bracket, but valid char class") 84 | assert(isMatch("p", "[:alpha:]"), "invalid posix bracket, but valid char class") 85 | assert(isMatch("h", "[:alpha:]"), "invalid posix bracket, but valid char class") 86 | assert(isMatch(":", "[:alpha:]"), "invalid posix bracket, but valid char class") 87 | assert(not isMatch("b", "[:alpha:]")("invalid posix bracket, but valid char class")) 88 | end) 89 | 90 | itFIXME("should support multiple posix brackets in one character class", function() 91 | assert(isMatch("9", "[[:lower:][:digit:]]")) 92 | assert(isMatch("a", "[[:lower:][:digit:]]")) 93 | assert(not isMatch("A", "[[:lower:][:digit:]]")) 94 | assert(not isMatch("aa", "[[:lower:][:digit:]]")) 95 | assert(not isMatch("99", "[[:lower:][:digit:]]")) 96 | assert(not isMatch("a9", "[[:lower:][:digit:]]")) 97 | assert(not isMatch("9a", "[[:lower:][:digit:]]")) 98 | assert(not isMatch("aA", "[[:lower:][:digit:]]")) 99 | assert(not isMatch("9A", "[[:lower:][:digit:]]")) 100 | assert(isMatch("aa", "[[:lower:][:digit:]]+")) 101 | assert(isMatch("99", "[[:lower:][:digit:]]+")) 102 | assert(isMatch("a9", "[[:lower:][:digit:]]+")) 103 | assert(isMatch("9a", "[[:lower:][:digit:]]+")) 104 | assert(not isMatch("aA", "[[:lower:][:digit:]]+")) 105 | assert(not isMatch("9A", "[[:lower:][:digit:]]+")) 106 | assert(isMatch("a", "[[:lower:][:digit:]]*")) 107 | assert(not isMatch("A", "[[:lower:][:digit:]]*")) 108 | assert(not isMatch("AA", "[[:lower:][:digit:]]*")) 109 | assert(isMatch("aa", "[[:lower:][:digit:]]*")) 110 | assert(isMatch("aaa", "[[:lower:][:digit:]]*")) 111 | assert(isMatch("999", "[[:lower:][:digit:]]*")) 112 | end) 113 | 114 | itFIXME("should match word characters", function() 115 | assert(not isMatch("a c", "a[[:word:]]+c")) 116 | assert(not isMatch("a.c", "a[[:word:]]+c")) 117 | assert(not isMatch("a.xy.zc", "a[[:word:]]+c")) 118 | assert(not isMatch("a.zc", "a[[:word:]]+c")) 119 | assert(not isMatch("abq", "a[[:word:]]+c")) 120 | assert(not isMatch("axy zc", "a[[:word:]]+c")) 121 | assert(not isMatch("axy", "a[[:word:]]+c")) 122 | assert(not isMatch("axy.zc", "a[[:word:]]+c")) 123 | assert(isMatch("a123c", "a[[:word:]]+c")) 124 | assert(isMatch("a1c", "a[[:word:]]+c")) 125 | assert(isMatch("abbbbc", "a[[:word:]]+c")) 126 | assert(isMatch("abbbc", "a[[:word:]]+c")) 127 | assert(isMatch("abbc", "a[[:word:]]+c")) 128 | assert(isMatch("abc", "a[[:word:]]+c")) 129 | assert(not isMatch("a c", "a[[:word:]]+")) 130 | assert(not isMatch("a.c", "a[[:word:]]+")) 131 | assert(not isMatch("a.xy.zc", "a[[:word:]]+")) 132 | assert(not isMatch("a.zc", "a[[:word:]]+")) 133 | assert(not isMatch("axy zc", "a[[:word:]]+")) 134 | assert(not isMatch("axy.zc", "a[[:word:]]+")) 135 | assert(isMatch("a123c", "a[[:word:]]+")) 136 | assert(isMatch("a1c", "a[[:word:]]+")) 137 | assert(isMatch("abbbbc", "a[[:word:]]+")) 138 | assert(isMatch("abbbc", "a[[:word:]]+")) 139 | assert(isMatch("abbc", "a[[:word:]]+")) 140 | assert(isMatch("abc", "a[[:word:]]+")) 141 | assert(isMatch("abq", "a[[:word:]]+")) 142 | assert(isMatch("axy", "a[[:word:]]+")) 143 | assert(isMatch("axyzc", "a[[:word:]]+")) 144 | assert(isMatch("axyzc", "a[[:word:]]+")) 145 | end) 146 | 147 | itFIXME("should not create an invalid posix character class:", function() 148 | jestExpect(convert("[:al:]")).toEqual("(?:\\[:al:\\]|[:al:])") 149 | jestExpect(convert("[abc[:punct:][0-9]")).toEqual( 150 | "(?=.)[abc\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~\\[0-9]" 151 | ) 152 | end) 153 | 154 | itFIXME("should return `true` when the pattern matches:", function() 155 | assert(isMatch("a", "[[:lower:]]")) 156 | assert(isMatch("A", "[[:upper:]]")) 157 | assert(isMatch("A", "[[:digit:][:upper:][:space:]]")) 158 | assert(isMatch("1", "[[:digit:][:upper:][:space:]]")) 159 | assert(isMatch(" ", "[[:digit:][:upper:][:space:]]")) 160 | assert(isMatch("5", "[[:xdigit:]]")) 161 | assert(isMatch("f", "[[:xdigit:]]")) 162 | assert(isMatch("D", "[[:xdigit:]]")) 163 | assert( 164 | isMatch( 165 | "_", 166 | "[[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:graph:][:lower:][:print:][:punct:][:space:][:upper:][:xdigit:]]" 167 | ) 168 | ) 169 | assert( 170 | isMatch( 171 | "_", 172 | "[[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:graph:][:lower:][:print:][:punct:][:space:][:upper:][:xdigit:]]" 173 | ) 174 | ) 175 | assert( 176 | isMatch( 177 | ".", 178 | "[^[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:lower:][:space:][:upper:][:xdigit:]]" 179 | ) 180 | ) 181 | assert(isMatch("5", "[a-c[:digit:]x-z]")) 182 | assert(isMatch("b", "[a-c[:digit:]x-z]")) 183 | assert(isMatch("y", "[a-c[:digit:]x-z]")) 184 | end) 185 | 186 | itFIXME("should return `false` when the pattern does not match:", function() 187 | assert(not isMatch("A", "[[:lower:]]")) 188 | assert(isMatch("A", "[![:lower:]]")) 189 | assert(not isMatch("a", "[[:upper:]]")) 190 | assert(not isMatch("a", "[[:digit:][:upper:][:space:]]")) 191 | assert(not isMatch(".", "[[:digit:][:upper:][:space:]]")) 192 | assert( 193 | not isMatch( 194 | ".", 195 | "[[:alnum:][:alpha:][:blank:][:cntrl:][:digit:][:lower:][:space:][:upper:][:xdigit:]]" 196 | ) 197 | ) 198 | assert(not isMatch("q", "[a-c[:digit:]x-z]")) 199 | end) 200 | end) 201 | 202 | describe("literals", function() 203 | itFIXME("should match literal brackets when escaped", function() 204 | assert(isMatch("a [b]", "a [b]")) 205 | assert(isMatch("a b", "a [b]")) 206 | assert(isMatch("a [b] c", "a [b] c")) 207 | assert(isMatch("a b c", "a [b] c")) 208 | assert(isMatch("a [b]", "a \\[b\\]")) 209 | assert(not isMatch("a b", "a \\[b\\]")) 210 | assert(isMatch("a [b]", "a ([b])")) 211 | assert(isMatch("a b", "a ([b])")) 212 | assert(isMatch("a b", "a (\\[b\\]|[b])")) 213 | assert(isMatch("a [b]", "a (\\[b\\]|[b])")) 214 | end) 215 | end) 216 | 217 | describe(".makeRe()", function() 218 | itFIXME("should make a regular expression for the given pattern:", function() 219 | jestExpect(makeRe("[[:alpha:]123]", opts)).toEqual(RegExp("/^(?:(?=.)[a-zA-Z123])$/")) 220 | jestExpect(makeRe("[![:lower:]]", opts)).toEqual(RegExp("/^(?:(?=.)[^a-z])$/")) 221 | end) 222 | end) 223 | 224 | describe("POSIX: From the test suite for the POSIX.2 (BRE) pattern matching code:", function() 225 | itFIXME("First, test POSIX.2 character classes", function() 226 | assert(isMatch("e", "[[:xdigit:]]")) 227 | assert(isMatch("1", "[[:xdigit:]]")) 228 | assert(isMatch("a", "[[:alpha:]123]")) 229 | assert(isMatch("1", "[[:alpha:]123]")) 230 | end) 231 | 232 | itFIXME("should match using POSIX.2 negation patterns", function() 233 | assert(isMatch("9", "[![:alpha:]]")) 234 | assert(isMatch("9", "[^[:alpha:]]")) 235 | end) 236 | 237 | itFIXME("should match word characters", function() 238 | assert(isMatch("A", "[[:word:]]")) 239 | assert(isMatch("B", "[[:word:]]")) 240 | assert(isMatch("a", "[[:word:]]")) 241 | assert(isMatch("b", "[[:word:]]")) 242 | end) 243 | 244 | itFIXME("should match digits with word class", function() 245 | assert(isMatch("1", "[[:word:]]")) 246 | assert(isMatch("2", "[[:word:]]")) 247 | end) 248 | 249 | itFIXME("should not digits", function() 250 | assert(isMatch("1", "[[:digit:]]")) 251 | assert(isMatch("2", "[[:digit:]]")) 252 | end) 253 | 254 | it("should not match word characters with digit class", function() 255 | assert(not isMatch("a", "[[:digit:]]")) 256 | assert(not isMatch("A", "[[:digit:]]")) 257 | end) 258 | 259 | itFIXME("should match uppercase alpha characters", function() 260 | assert(isMatch("A", "[[:upper:]]")) 261 | assert(isMatch("B", "[[:upper:]]")) 262 | end) 263 | 264 | itFIXME("should not match lowercase alpha characters", function() 265 | assert(not isMatch("a", "[[:upper:]]")) 266 | assert(not isMatch("b", "[[:upper:]]")) 267 | end) 268 | 269 | it("should not match digits with upper class", function() 270 | assert(not isMatch("1", "[[:upper:]]")) 271 | assert(not isMatch("2", "[[:upper:]]")) 272 | end) 273 | 274 | itFIXME("should match lowercase alpha characters", function() 275 | assert(isMatch("a", "[[:lower:]]")) 276 | assert(isMatch("b", "[[:lower:]]")) 277 | end) 278 | 279 | it("should not match uppercase alpha characters", function() 280 | assert(not isMatch("A", "[[:lower:]]")) 281 | assert(not isMatch("B", "[[:lower:]]")) 282 | end) 283 | 284 | itFIXME("should match one lower and one upper character", function() 285 | assert(isMatch("aA", "[[:lower:]][[:upper:]]")) 286 | assert(not isMatch("AA", "[[:lower:]][[:upper:]]")) 287 | assert(not isMatch("Aa", "[[:lower:]][[:upper:]]")) 288 | end) 289 | 290 | itFIXME("should match hexadecimal digits", function() 291 | assert(isMatch("ababab", "[[:xdigit:]]*")) 292 | assert(isMatch("020202", "[[:xdigit:]]*")) 293 | assert(isMatch("900", "[[:xdigit:]]*")) 294 | end) 295 | 296 | itFIXME("should match punctuation characters (\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~)", function() 297 | assert(isMatch("!", "[[:punct:]]")) 298 | assert(isMatch("?", "[[:punct:]]")) 299 | assert(isMatch("#", "[[:punct:]]")) 300 | assert(isMatch("&", "[[:punct:]]")) 301 | assert(isMatch("@", "[[:punct:]]")) 302 | assert(isMatch("+", "[[:punct:]]")) 303 | assert(isMatch("*", "[[:punct:]]")) 304 | assert(isMatch(":", "[[:punct:]]")) 305 | assert(isMatch("=", "[[:punct:]]")) 306 | assert(isMatch("|", "[[:punct:]]")) 307 | assert(isMatch("|++", "[[:punct:]]*")) 308 | end) 309 | 310 | it("should only match one character", function() 311 | assert(not isMatch("?*+", "[[:punct:]]")) 312 | end) 313 | 314 | itFIXME("should only match zero or more punctuation characters", function() 315 | assert(isMatch("?*+", "[[:punct:]]*")) 316 | assert(isMatch("foo", "foo[[:punct:]]*")) 317 | assert(isMatch("foo?*+", "foo[[:punct:]]*")) 318 | end) 319 | 320 | itFIXME("invalid character class expressions are just characters to be matched", function() 321 | assert(isMatch("a", "[:al:]")) 322 | assert(isMatch("a", "[[:al:]")) 323 | assert(isMatch("!", "[abc[:punct:][0-9]")) 324 | end) 325 | 326 | itFIXME("should match the start of a valid sh identifier", function() 327 | assert(isMatch("PATH", "[_[:alpha:]]*")) 328 | end) 329 | 330 | itFIXME("should match the first two characters of a valid sh identifier", function() 331 | assert(isMatch("PATH", "[_[:alpha:]][_[:alnum:]]*")) 332 | end) 333 | 334 | itFIXME("should match multiple posix classses", function() 335 | assert(isMatch("a1B", "[[:alpha:]][[:digit:]][[:upper:]]")) 336 | assert(not isMatch("a1b", "[[:alpha:]][[:digit:]][[:upper:]]")) 337 | assert(isMatch(".", "[[:digit:][:punct:][:space:]]")) 338 | assert(not isMatch("a", "[[:digit:][:punct:][:space:]]")) 339 | assert(isMatch("!", "[[:digit:][:punct:][:space:]]")) 340 | assert(not isMatch("!", "[[:digit:]][[:punct:]][[:space:]]")) 341 | assert(isMatch("1! ", "[[:digit:]][[:punct:]][[:space:]]")) 342 | assert(not isMatch("1! ", "[[:digit:]][[:punct:]][[:space:]]")) 343 | end) 344 | 345 | --[[* 346 | * Some of these tests (and their descriptions) were ported directly 347 | * from the Bash 4.3 unit tests. 348 | ]] 349 | itFIXME("how about A?", function() 350 | assert(isMatch("9", "[[:digit:]]")) 351 | assert(not isMatch("X", "[[:digit:]]")) 352 | assert(isMatch("aB", "[[:lower:]][[:upper:]]")) 353 | assert(isMatch("a", "[[:alpha:][:digit:]]")) 354 | assert(isMatch("3", "[[:alpha:][:digit:]]")) 355 | assert(not isMatch("aa", "[[:alpha:][:digit:]]")) 356 | assert(not isMatch("a3", "[[:alpha:][:digit:]]")) 357 | assert(not isMatch("a", "[[:alpha:]\\]")) 358 | assert(not isMatch("b", "[[:alpha:]\\]")) 359 | end) 360 | 361 | itFIXME("OK, what's a tab? is it a blank? a space?", function() 362 | assert(isMatch("\t", "[[:blank:]]")) 363 | assert(isMatch("\t", "[[:space:]]")) 364 | assert(isMatch(" ", "[[:space:]]")) 365 | end) 366 | 367 | it("let's check out characters in the ASCII range", function() 368 | assert(not isMatch("\\377", "[[:ascii:]]")) 369 | assert(not isMatch("9", "[1[:alpha:]123]")) 370 | end) 371 | 372 | it("punctuation", function() 373 | assert(not isMatch(" ", "[[:punct:]]")) 374 | end) 375 | 376 | itFIXME("graph", function() 377 | assert(isMatch("A", "[[:graph:]]")) 378 | assert(not isMatch("\\b", "[[:graph:]]")) 379 | assert(not isMatch("\\n", "[[:graph:]]")) 380 | assert(not isMatch("\\s", "[[:graph:]]")) 381 | end) 382 | end) 383 | end) 384 | end 385 | -------------------------------------------------------------------------------- /src/__tests__/dotfiles.spec.lua: -------------------------------------------------------------------------------- 1 | -- ROBLOX upstream: https://github.com/micromatch/picomatch/tree/2.3.1/test/dotfiles.js 2 | 3 | return function() 4 | local CurrentModule = script.Parent 5 | local PicomatchModule = CurrentModule.Parent 6 | local Packages = PicomatchModule.Parent 7 | 8 | local jestExpect = require(Packages.Dev.JestGlobals).expect 9 | 10 | local Promise = require(Packages.Promise) 11 | 12 | local match = require(CurrentModule.support.match) 13 | local isMatch = require(PicomatchModule).isMatch 14 | describe("dotfiles", function() 15 | describe("normal", function() 16 | itFIXME("should not match dotfiles by default:", function() 17 | jestExpect(match({ ".dotfile" }, "*")).toEqual({}) 18 | jestExpect(match({ ".dotfile" }, "**")).toEqual({}) 19 | jestExpect(match({ "a/b/c/.dotfile.md" }, "*.md")).toEqual({}) 20 | jestExpect(match({ "a/b", "a/.b", ".a/b", ".a/.b" }, "**")).toEqual({ "a/b" }) 21 | jestExpect(match({ "a/b/c/.dotfile" }, "*.*")).toEqual({}) 22 | end) 23 | end) 24 | 25 | describe("leading dot", function() 26 | itFIXME("should match dotfiles when a leading dot is defined in the path:", function() 27 | jestExpect(match({ "a/b/c/.dotfile.md" }, "**/.*")).toEqual({ "a/b/c/.dotfile.md" }) 28 | jestExpect(match({ "a/b/c/.dotfile.md" }, "**/.*.md")).toEqual({ "a/b/c/.dotfile.md" }) 29 | end) 30 | 31 | itFIXME("should use negation patterns on dotfiles:", function() 32 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!.*")).toEqual({ "c", "c.md" }) 33 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!.b")).toEqual({ ".a", "c", "c.md" }) 34 | end) 35 | 36 | itFIXME("should match dotfiles when there is a leading dot:", function() 37 | local opts = { dot = true } 38 | jestExpect(match({ ".dotfile" }, "*", opts)).toEqual({ ".dotfile" }) 39 | jestExpect(match({ ".dotfile" }, "**", opts)).toEqual({ ".dotfile" }) 40 | jestExpect(match({ "a/b", "a/.b", ".a/b", ".a/.b" }, "**", opts)).toEqual({ 41 | "a/b", 42 | "a/.b", 43 | ".a/b", 44 | ".a/.b", 45 | }) 46 | jestExpect(match({ "a/b", "a/.b", "a/.b", ".a/.b" }, "a/{.*,**}", opts)).toEqual({ "a/b", "a/.b" }) 47 | jestExpect(match({ "a/b", "a/.b", "a/.b", ".a/.b" }, "{.*,**}", {})).toEqual({ "a/b" }) 48 | jestExpect(match({ "a/b", "a/.b", "a/.b", ".a/.b" }, "{.*,**}", opts)).toEqual({ 49 | "a/b", 50 | "a/.b", 51 | ".a/.b", 52 | }) 53 | jestExpect(match({ ".dotfile" }, ".dotfile", opts)).toEqual({ ".dotfile" }) 54 | jestExpect(match({ ".dotfile.md" }, ".*.md", opts)).toEqual({ ".dotfile.md" }) 55 | end) 56 | 57 | itFIXME("should match dotfiles when there is not a leading dot:", function() 58 | local opts = { dot = true } 59 | jestExpect(match({ ".dotfile" }, "*.*", opts)).toEqual({ ".dotfile" }) 60 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "*.*", opts)).toEqual({ ".a", ".b", "c.md" }) 61 | jestExpect(match({ ".dotfile" }, "*.md", opts)).toEqual({}) 62 | jestExpect(match({ ".verb.txt" }, "*.md", opts)).toEqual({}) 63 | jestExpect(match({ "a/b/c/.dotfile" }, "*.md", opts)).toEqual({}) 64 | jestExpect(match({ "a/b/c/.dotfile.md" }, "*.md", opts)).toEqual({}) 65 | jestExpect(match({ "a/b/c/.verb.md" }, "**/*.md", opts)).toEqual({ "a/b/c/.verb.md" }) 66 | jestExpect(match({ "foo.md" }, "*.md", opts)).toEqual({ "foo.md" }) 67 | end) 68 | 69 | itFIXME("should use negation patterns on dotfiles_:", function() 70 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!.*")).toEqual({ "c", "c.md" }) 71 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!(.*)")).toEqual({ "c", "c.md" }) 72 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!(.*)*")).toEqual({ "c", "c.md" }) 73 | jestExpect(match({ ".a", ".b", "c", "c.md" }, "!*.*")).toEqual({ ".a", ".b", "c" }) 74 | end) 75 | end) 76 | 77 | describe("options.dot", function() 78 | itFIXME("should match dotfiles when `options.dot` is true:", function() 79 | local fixtures = { "a/./b", "a/../b", "a/c/b", "a/.d/b" } 80 | jestExpect(match({ ".dotfile" }, "*.*", { dot = true })).toEqual({ ".dotfile" }) 81 | jestExpect(match({ ".dotfile" }, "*.md", { dot = true })).toEqual({}) 82 | jestExpect(match({ ".dotfile" }, ".dotfile", { dot = true })).toEqual({ ".dotfile" }) 83 | jestExpect(match({ ".dotfile.md" }, ".*.md", { dot = true })).toEqual({ ".dotfile.md" }) 84 | jestExpect(match({ ".verb.txt" }, "*.md", { dot = true })).toEqual({}) 85 | jestExpect(match({ ".verb.txt" }, "*.md", { dot = true })).toEqual({}) 86 | jestExpect(match({ "a/b/c/.dotfile" }, "*.md", { dot = true })).toEqual({}) 87 | jestExpect(match({ "a/b/c/.dotfile.md" }, "**/*.md", { dot = true })).toEqual({ "a/b/c/.dotfile.md" }) 88 | jestExpect(match({ "a/b/c/.dotfile.md" }, "**/.*", { dot = false })).toEqual({ "a/b/c/.dotfile.md" }) 89 | jestExpect(match({ "a/b/c/.dotfile.md" }, "**/.*.md", { dot = false })).toEqual({ 90 | "a/b/c/.dotfile.md", 91 | }) 92 | jestExpect(match({ "a/b/c/.dotfile.md" }, "*.md", { dot = false })).toEqual({}) 93 | jestExpect(match({ "a/b/c/.dotfile.md" }, "*.md", { dot = true })).toEqual({}) 94 | jestExpect(match({ "a/b/c/.verb.md" }, "**/*.md", { dot = true })).toEqual({ "a/b/c/.verb.md" }) 95 | jestExpect(match({ "d.md" }, "*.md", { dot = true })).toEqual({ "d.md" }) 96 | jestExpect(match(fixtures, "a/*/b", { dot = true })).toEqual({ "a/c/b", "a/.d/b" }) 97 | jestExpect(match(fixtures, "a/.*/b")).toEqual({ "a/.d/b" }) 98 | jestExpect(match(fixtures, "a/.*/b", { dot = true })).toEqual({ "a/.d/b" }) 99 | end) 100 | 101 | itFIXME("should match dotfiles when `options.dot` is true", function() 102 | assert(isMatch(".dot", "**/*dot", { dot = true })) 103 | assert(isMatch(".dot", "*dot", { dot = true })) 104 | assert(isMatch(".dot", "?dot", { dot = true })) 105 | assert(isMatch(".dotfile.js", ".*.js", { dot = true })) 106 | assert(isMatch("/a/b/.dot", "/**/*dot", { dot = true })) 107 | assert(isMatch("/a/b/.dot", "**/*dot", { dot = true })) 108 | assert(isMatch("/a/b/.dot", "**/.[d]ot", { dot = true })) 109 | assert(isMatch("/a/b/.dot", "**/?dot", { dot = true })) 110 | assert(isMatch("/a/b/.dot", "/**/.[d]ot", { dot = true })) 111 | assert(isMatch("/a/b/.dot", "/**/?dot", { dot = true })) 112 | assert(isMatch("a/b/.dot", "**/*dot", { dot = true })) 113 | assert(isMatch("a/b/.dot", "**/.[d]ot", { dot = true })) 114 | assert(isMatch("a/b/.dot", "**/?dot", { dot = true })) 115 | end) 116 | 117 | it("should not match dotfiles when `options.dot` is false", function() 118 | assert(not isMatch("a/b/.dot", "**/*dot", { dot = false })) 119 | assert(not isMatch("a/b/.dot", "**/?dot", { dot = false })) 120 | end) 121 | 122 | it("should not match dotfiles when `.dot` is not defined and a dot is not in the glob pattern", function() 123 | assert(not isMatch("a/b/.dot", "**/*dot")) 124 | assert(not isMatch("a/b/.dot", "**/?dot")) 125 | end) 126 | end) 127 | 128 | describe("valid dotfiles", function() 129 | it("micromatch issue#63 (dots)", function() 130 | assert(not isMatch("/aaa/.git/foo", "/aaa/**/*")) 131 | assert(not isMatch("/aaa/bbb/.git", "/aaa/bbb/*")) 132 | assert(not isMatch("/aaa/bbb/.git", "/aaa/bbb/**")) 133 | assert(not isMatch("/aaa/bbb/ccc/.git", "/aaa/bbb/**")) 134 | assert(not isMatch("aaa/bbb/.git", "aaa/bbb/**")) 135 | assert(isMatch("/aaa/bbb/", "/aaa/bbb/**")) 136 | assert(isMatch("/aaa/bbb/foo", "/aaa/bbb/**")) 137 | assert(isMatch("/aaa/.git/foo", "/aaa/**/*", { dot = true })) 138 | assert(isMatch("/aaa/bbb/.git", "/aaa/bbb/*", { dot = true })) 139 | assert(isMatch("/aaa/bbb/.git", "/aaa/bbb/**", { dot = true })) 140 | assert(isMatch("/aaa/bbb/ccc/.git", "/aaa/bbb/**", { dot = true })) 141 | assert(isMatch("aaa/bbb/.git", "aaa/bbb/**", { dot = true })) 142 | end) 143 | 144 | it("should not match dotfiles with single stars by default", function() 145 | assert(isMatch("foo", "*")) 146 | assert(isMatch("foo/bar", "*/*")) 147 | assert(not isMatch(".foo", "*")) 148 | assert(not isMatch(".foo/bar", "*/*")) 149 | assert(not isMatch(".foo/.bar", "*/*")) 150 | assert(not isMatch("foo/.bar", "*/*")) 151 | assert(not isMatch("foo/.bar/baz", "*/*/*")) 152 | end) 153 | 154 | it("should work with dots in the path", function() 155 | assert(isMatch("../test.js", "../*.js")) 156 | assert(isMatch("../.test.js", "../*.js", { dot = true })) 157 | assert(not isMatch("../.test.js", "../*.js")) 158 | end) 159 | 160 | it("should not match dotfiles with globstar by default", function() 161 | assert(not isMatch(".foo", "**/**")) 162 | assert(not isMatch(".foo", "**")) 163 | assert(not isMatch(".foo", "**/*")) 164 | assert(not isMatch("bar/.foo", "**/*")) 165 | assert(not isMatch(".bar", "**/*")) 166 | assert(not isMatch("foo/.bar", "**/*")) 167 | assert(not isMatch("foo/.bar", "**/*a*")) 168 | end) 169 | 170 | it("should match dotfiles when a leading dot is in the pattern", function() 171 | assert(not isMatch("foo", "**/.*a*")) 172 | assert(isMatch(".bar", "**/.*a*")) 173 | assert(isMatch("foo/.bar", "**/.*a*")) 174 | assert(isMatch(".foo", "**/.*")) 175 | assert(not isMatch("foo", ".*a*")) 176 | assert(isMatch(".bar", ".*a*")) 177 | assert(not isMatch("bar", ".*a*")) 178 | assert(not isMatch("foo", ".b*")) 179 | assert(isMatch(".bar", ".b*")) 180 | assert(not isMatch("bar", ".b*")) 181 | assert(not isMatch("foo", ".*r")) 182 | assert(isMatch(".bar", ".*r")) 183 | assert(not isMatch("bar", ".*r")) 184 | end) 185 | 186 | it("should not match a dot when the dot is not explicitly defined", function() 187 | assert(not isMatch(".dot", "**/*dot")) 188 | assert(not isMatch(".dot", "**/?dot")) 189 | assert(not isMatch(".dot", "*/*dot")) 190 | assert(not isMatch(".dot", "*/?dot")) 191 | assert(not isMatch(".dot", "*dot")) 192 | assert(not isMatch(".dot", "/*dot")) 193 | assert(not isMatch(".dot", "/?dot")) 194 | assert(not isMatch("/.dot", "**/*dot")) 195 | assert(not isMatch("/.dot", "**/?dot")) 196 | assert(not isMatch("/.dot", "*/*dot")) 197 | assert(not isMatch("/.dot", "*/?dot")) 198 | assert(not isMatch("/.dot", "/*dot")) 199 | assert(not isMatch("/.dot", "/?dot")) 200 | assert(not isMatch("abc/.dot", "*/*dot")) 201 | assert(not isMatch("abc/.dot", "*/?dot")) 202 | assert(not isMatch("abc/.dot", "abc/*dot")) 203 | assert(not isMatch("abc/abc/.dot", "**/*dot")) 204 | assert(not isMatch("abc/abc/.dot", "**/?dot")) 205 | end) 206 | 207 | itFIXME("should not match leading dots with question marks", function() 208 | assert(not isMatch(".dot", "?dot")) 209 | assert(not isMatch("/.dot", "/?dot")) 210 | assert(not isMatch("abc/.dot", "abc/?dot")) 211 | end) 212 | 213 | it("should match double dots when defined in pattern", function() 214 | assert(not isMatch("../../b", "**/../*")) 215 | assert(not isMatch("../../b", "*/../*")) 216 | assert(not isMatch("../../b", "../*")) 217 | assert(not isMatch("../abc", "*/../*")) 218 | assert(not isMatch("../abc", "*/../*")) 219 | assert(not isMatch("../c/d", "**/../*")) 220 | assert(not isMatch("../c/d", "*/../*")) 221 | assert(not isMatch("../c/d", "../*")) 222 | assert(not isMatch("abc", "**/../*")) 223 | assert(not isMatch("abc", "*/../*")) 224 | assert(not isMatch("abc", "../*")) 225 | assert(not isMatch("abc/../abc", "../*")) 226 | assert(not isMatch("abc/../abc", "../*")) 227 | assert(not isMatch("abc/../", "**/../*")) 228 | assert(isMatch("..", "..")) 229 | assert(isMatch("../b", "../*")) 230 | assert(isMatch("../../b", "../../*")) 231 | assert(isMatch("../../..", "../../..")) 232 | assert(isMatch("../abc", "**/../*")) 233 | assert(isMatch("../abc", "../*")) 234 | assert(isMatch("abc/../abc", "**/../*")) 235 | assert(isMatch("abc/../abc", "*/../*")) 236 | assert(isMatch("abc/../abc", "**/../*")) 237 | assert(isMatch("abc/../abc", "*/../*")) 238 | end) 239 | 240 | it("should not match double dots when not defined in pattern", function() 241 | return Promise.resolve():andThen(function() 242 | assert(not isMatch("../abc", "**/*")) 243 | assert(not isMatch("../abc", "**/**/**")) 244 | assert(not isMatch("../abc", "**/**/abc")) 245 | assert(not isMatch("../abc", "**/**/abc/**")) 246 | assert(not isMatch("../abc", "**/*/*")) 247 | assert(not isMatch("../abc", "**/abc/**")) 248 | assert(not isMatch("../abc", "*/*")) 249 | assert(not isMatch("../abc", "*/abc/**")) 250 | assert(not isMatch("abc/../abc", "**/*")) 251 | assert(not isMatch("abc/../abc", "**/*/*")) 252 | assert(not isMatch("abc/../abc", "**/*/abc")) 253 | assert(not isMatch("abc/../abc", "*/**/*")) 254 | assert(not isMatch("abc/../abc", "*/*/*")) 255 | assert(not isMatch("abc/../abc", "abc/**/*")) 256 | assert(not isMatch("abc/../abc", "**/**/*")) 257 | assert(not isMatch("abc/../abc", "**/*/*")) 258 | assert(not isMatch("abc/../abc", "*/**/*")) 259 | assert(not isMatch("abc/../abc", "*/*/*")) 260 | assert(not isMatch("../abc", "**/**/**", { dot = true })) 261 | assert(not isMatch("../abc", "**/**/abc", { dot = true })) 262 | assert(not isMatch("../abc", "**/**/abc/**", { dot = true })) 263 | assert(not isMatch("../abc", "**/abc/**", { dot = true })) 264 | assert(not isMatch("../abc", "*/abc/**", { dot = true })) 265 | assert(not isMatch("../abc", "**/*/*", { dot = true })) 266 | assert(not isMatch("../abc", "*/*", { dot = true })) 267 | assert(not isMatch("abc/../abc", "**/*/*", { dot = true })) 268 | assert(not isMatch("abc/../abc", "*/*/*", { dot = true })) 269 | assert(not isMatch("abc/../abc", "**/*/*", { dot = true })) 270 | assert(not isMatch("abc/../abc", "*/*/*", { dot = true })) 271 | assert(not isMatch("abc/..", "**/*", { dot = true })) 272 | assert(not isMatch("abc/..", "*/*", { dot = true })) 273 | assert(not isMatch("abc/abc/..", "*/**/*", { dot = true })) 274 | assert(not isMatch("abc/../abc", "abc/**/*")) 275 | assert(not isMatch("abc/../abc", "abc/**/*", { dot = true })) 276 | assert(not isMatch("abc/../abc", "abc/**/*/*", { dot = true })) 277 | assert(not isMatch("abc/../abc", "abc/*/*/*", { dot = true })) 278 | assert(not isMatch("abc/../abc", "abc/**/*/*", { dot = true })) 279 | assert(not isMatch("abc/../abc", "abc/*/*/*", { dot = true })) 280 | assert(not isMatch("abc/..", "abc/**/*", { dot = true })) 281 | assert(not isMatch("abc/..", "abc/*/*", { dot = true })) 282 | assert(not isMatch("abc/abc/..", "abc/*/**/*", { dot = true })) 283 | assert(not isMatch("../abc", "**/*/*", { dot = true })) 284 | assert(not isMatch("../abc", "*/*", { dot = true })) 285 | assert(not isMatch("abc/../abc", "**/*/*", { dot = true })) 286 | assert(not isMatch("abc/../abc", "*/*/*", { dot = true })) 287 | assert(not isMatch("abc/../abc", "**/*/*", { dot = true })) 288 | assert(not isMatch("abc/../abc", "*/*/*", { dot = true })) 289 | assert(not isMatch("abc/..", "**/*", { dot = true })) 290 | assert(not isMatch("abc/..", "*/*", { dot = true })) 291 | assert(not isMatch("abc/abc/..", "*/**/*", { dot = true })) 292 | assert(not isMatch("abc/../abc", "abc/**/*", { strictSlashes = true })) 293 | assert(not isMatch("abc/../abc", "abc/**/*/*", { strictSlashes = true })) 294 | assert(not isMatch("abc/../abc", "abc/**/*/*", { strictSlashes = true })) 295 | assert(not isMatch("abc/../abc", "abc/*/*/*", { strictSlashes = true })) 296 | assert(not isMatch("abc/../abc", "abc/**/*/*", { strictSlashes = true })) 297 | assert(not isMatch("abc/../abc", "abc/*/*/*", { strictSlashes = true })) 298 | assert(not isMatch("abc/..", "abc/**/*", { strictSlashes = true })) 299 | assert(not isMatch("abc/..", "abc/*/*", { strictSlashes = true })) 300 | assert(not isMatch("abc/abc/..", "abc/*/**/*", { strictSlashes = true })) 301 | end) 302 | end) 303 | 304 | it("should not match single exclusive dots when not defined in pattern", function() 305 | return Promise.resolve():andThen(function() 306 | assert(not isMatch(".", "**")) 307 | assert(not isMatch("abc/./abc", "**")) 308 | assert(not isMatch("abc/abc/.", "**")) 309 | assert(not isMatch("abc/abc/./abc", "**")) 310 | assert(not isMatch(".", "**", { dot = true })) 311 | assert(not isMatch("..", "**", { dot = true })) 312 | assert(not isMatch("../", "**", { dot = true })) 313 | assert(not isMatch("/../", "**", { dot = true })) 314 | assert(not isMatch("/..", "**", { dot = true })) 315 | assert(not isMatch("abc/./abc", "**", { dot = true })) 316 | assert(not isMatch("abc/abc/.", "**", { dot = true })) 317 | assert(not isMatch("abc/abc/./abc", "**", { dot = true })) 318 | end) 319 | end) 320 | 321 | it("should match leading dots in root path when glob is prefixed with **/", function() 322 | assert(not isMatch(".abc/.abc", "**/.abc/**")) 323 | assert(isMatch(".abc", "**/.abc/**")) 324 | assert(isMatch(".abc/", "**/.abc/**")) 325 | assert(isMatch(".abc/abc", "**/.abc/**")) 326 | assert(isMatch(".abc/abc/b", "**/.abc/**")) 327 | assert(isMatch("abc/.abc/b", "**/.abc/**")) 328 | assert(isMatch("abc/abc/.abc", "**/.abc")) 329 | assert(isMatch("abc/abc/.abc", "**/.abc/**")) 330 | assert(isMatch("abc/abc/.abc/", "**/.abc/**")) 331 | assert(isMatch("abc/abc/.abc/abc", "**/.abc/**")) 332 | assert(isMatch("abc/abc/.abc/c/d", "**/.abc/**")) 333 | assert(isMatch("abc/abc/.abc/c/d/e", "**/.abc/**")) 334 | end) 335 | 336 | it("should match a dot when the dot is explicitly defined", function() 337 | assert(isMatch("/.dot", "**/.dot*")) 338 | assert(isMatch("aaa/bbb/.dot", "**/.dot*")) 339 | assert(isMatch("aaa/.dot", "*/.dot*")) 340 | assert(isMatch(".aaa.bbb", ".*.*")) 341 | assert(isMatch(".aaa.bbb", ".*.*")) 342 | assert(not isMatch(".aaa.bbb/", ".*.*", { strictSlashes = true })) 343 | assert(not isMatch(".aaa.bbb", ".*.*/")) 344 | assert(isMatch(".aaa.bbb/", ".*.*/")) 345 | assert(isMatch(".aaa.bbb/", ".*.*{,/}")) 346 | assert(isMatch(".aaa.bbb", ".*.bbb")) 347 | assert(isMatch(".dotfile.js", ".*.js")) 348 | assert(isMatch(".dot", ".*ot")) 349 | assert(isMatch(".dot.bbb.ccc", ".*ot.*.*")) 350 | assert(isMatch(".dot", ".d?t")) 351 | assert(isMatch(".dot", ".dot*")) 352 | assert(isMatch("/.dot", "/.dot*")) 353 | end) 354 | 355 | itFIXME("should match dots defined in brackets", function() 356 | assert(isMatch("/.dot", "**/.[d]ot")) 357 | assert(isMatch("aaa/.dot", "**/.[d]ot")) 358 | assert(isMatch("aaa/bbb/.dot", "**/.[d]ot")) 359 | assert(isMatch("aaa/.dot", "*/.[d]ot")) 360 | assert(isMatch(".dot", ".[d]ot")) 361 | assert(isMatch(".dot", ".[d]ot")) 362 | assert(isMatch("/.dot", "/.[d]ot")) 363 | end) 364 | end) 365 | end) 366 | end 367 | --------------------------------------------------------------------------------