Files
neovim/test/functional/lua/glob_spec.lua
Tristan Knight d2ca90d87e fix(glob): handle numeric literals in pattern matching (#37257)
Problem:
vim.glob.to_lpeg() errors when patterns contain numeric literals
(like the '1' in '.ps*1') because LPeg interprets numeric strings
as indexed grammar rule references. For example:
  vim.glob.to_lpeg('.ps*1')
  E5108: Lua: rule '1' undefined in given grammar

Solution:
Prefix all rule names with '_' in the end_seg() function to prevent
literal numbers from being interpreted as LPeg indexed rules. This
ensures pattern components like '1', '2', etc. are treated as
regular rule names rather than special references.
2026-01-12 10:58:01 -08:00

273 lines
11 KiB
Lua

local t = require('test.testutil')
local eq = t.eq
describe('glob', function()
local match = function(pattern, str)
return require('vim.glob').to_lpeg(pattern):match(str) ~= nil
end
describe('glob matching', function()
it('should match literal strings', function()
eq(true, match('', ''))
eq(false, match('', 'a'))
eq(true, match('a', 'a'))
eq(true, match('.', '.'))
eq(true, match('/', '/'))
eq(true, match('abc', 'abc'))
eq(false, match('abc', 'abcdef'))
eq(false, match('abc', 'a'))
eq(false, match('abc', 'bc'))
eq(false, match('a', 'b'))
eq(false, match('.', 'a'))
eq(true, match('$', '$'))
eq(true, match('a,b', 'a,b'))
eq(true, match('/dir', '/dir'))
eq(true, match('dir/', 'dir/'))
eq(true, match('dir/subdir', 'dir/subdir'))
eq(false, match('dir/subdir', 'subdir'))
eq(false, match('dir/subdir', 'dir/subdir/file'))
eq(true, match('🤠', '🤠'))
end)
it('should match * wildcards', function()
eq(true, match('*', ''))
eq(true, match('*', ' '))
eq(true, match('*', 'a'))
eq(false, match('*', '/'))
eq(false, match('*', '/a'))
eq(false, match('*', 'a/'))
eq(true, match('*', 'aaa'))
eq(true, match('*a', 'aa'))
eq(true, match('*a', 'abca'))
eq(true, match('*.ts', '.ts'))
eq(true, match('*.txt', 'file.txt'))
eq(false, match('*.txt', 'file.txtxt'))
eq(false, match('*.txt', 'dir/file.txt'))
eq(false, match('*.txt', '/dir/file.txt'))
eq(false, match('*.txt', 'C:/dir/file.txt'))
eq(false, match('*.dir', 'test.dir/file'))
eq(true, match('file.*', 'file.txt'))
eq(false, match('file.*', 'not-file.txt'))
eq(true, match('*/file.txt', 'dir/file.txt'))
eq(false, match('*/file.txt', 'dir/subdir/file.txt'))
eq(false, match('*/file.txt', '/dir/file.txt'))
eq(true, match('dir/*', 'dir/file.txt'))
eq(false, match('dir/*', 'dir'))
eq(false, match('dir/*.txt', 'file.txt'))
eq(true, match('dir/*.txt', 'dir/file.txt'))
eq(false, match('dir/*.txt', 'dir/subdir/file.txt'))
eq(false, match('dir/*/file.txt', 'dir/file.txt'))
eq(true, match('dir/*/file.txt', 'dir/subdir/file.txt'))
eq(false, match('dir/*/file.txt', 'dir/subdir/subdir/file.txt'))
eq(true, match('a*b*c*d*e*', 'axbxcxdxe'))
eq(true, match('a*b*c*d*e*', 'axbxcxdxexxx'))
eq(true, match('a*b?c*x', 'abxbbxdbxebxczzx'))
eq(false, match('a*b?c*x', 'abxbbxdbxebxczzy'))
eq(true, match('a*b*[cy]*d*e*', 'axbxcxdxexxx'))
eq(true, match('a*b*[cy]*d*e*', 'axbxyxdxexxx'))
eq(true, match('a*b*[cy]*d*e*', 'axbxxxyxdxexxx'))
eq(true, match('.ps*1', '.ps1'))
eq(true, match('.ps*1', '.psaa1'))
eq(false, match('.ps*1', '.ps1a'))
end)
it('should match ? wildcards', function()
eq(false, match('?', ''))
eq(true, match('?', 'a'))
eq(false, match('??', 'a'))
eq(false, match('?', 'ab'))
eq(true, match('??', 'ab'))
eq(true, match('a?c', 'abc'))
eq(false, match('a?c', 'a/c'))
eq(false, match('a/', 'a/.b'))
eq(true, match('?/?', 'a/b'))
eq(true, match('/??', '/ab'))
eq(true, match('/?b', '/ab'))
eq(false, match('foo?bar', 'foo/bar'))
end)
it('should match ** wildcards', function()
eq(true, match('**', ''))
eq(true, match('**', 'a'))
eq(true, match('**', '/'))
eq(true, match('**', 'a/'))
eq(true, match('**', '/a'))
eq(true, match('**', 'C:/a'))
eq(true, match('**', 'a/a'))
eq(true, match('**', 'a/a/a'))
eq(false, match('/**', '')) -- /** matches leading / literally
eq(true, match('/**', '/'))
eq(true, match('/**', '/a/b/c'))
eq(true, match('**/', '')) -- **/ absorbs trailing /
eq(false, match('**/', '/a/b/c'))
eq(true, match('**/**', ''))
eq(true, match('**/**', 'a'))
eq(false, match('a/**', ''))
eq(false, match('a/**', 'a'))
eq(true, match('a/**', 'a/b'))
eq(true, match('a/**', 'a/b/c'))
eq(false, match('a/**', 'b/a'))
eq(false, match('a/**', '/a'))
eq(false, match('**/a', ''))
eq(true, match('**/a', 'a'))
eq(false, match('**/a', 'a/b'))
eq(true, match('**/a', '/a'))
eq(true, match('**/a', '/b/a'))
eq(true, match('**/a', '/c/b/a'))
eq(true, match('**/a', '/a/a'))
eq(true, match('**/a', '/abc/a'))
eq(false, match('a/**/c', 'a'))
eq(false, match('a/**/c', 'c'))
eq(true, match('a/**/c', 'a/c'))
eq(true, match('a/**/c', 'a/b/c'))
eq(true, match('a/**/c', 'a/b/b/c'))
eq(false, match('**/a/**', 'a'))
eq(true, match('**/a/**', 'a/'))
eq(false, match('**/a/**', '/dir/a'))
eq(false, match('**/a/**', 'dir/a'))
eq(true, match('**/a/**', 'dir/a/'))
eq(true, match('**/a/**', 'a/dir'))
eq(true, match('**/a/**', 'dir/a/dir'))
eq(true, match('**/a/**', '/a/dir'))
eq(true, match('**/a/**', 'C:/a/dir'))
eq(false, match('**/a/**', 'a.txt'))
end)
it('should match {} groups', function()
eq(true, match('{,}', ''))
eq(true, match('{a,}', ''))
eq(true, match('{a,}', 'a'))
eq(true, match('{a,b}', 'a'))
eq(true, match('{a,b}', 'b'))
eq(false, match('{a,b}', 'ab'))
eq(true, match('{ab,cd}', 'ab'))
eq(false, match('{ab,cd}', 'a'))
eq(true, match('{ab,cd}', 'cd'))
eq(true, match('{a,b,c}', 'c'))
eq(true, match('{a,{b,c}}', 'c'))
eq(true, match('a{,/}*.txt', 'a.txt'))
eq(true, match('a{,/}*.txt', 'ab.txt'))
eq(true, match('a{,/}*.txt', 'a/b.txt'))
eq(true, match('a{,/}*.txt', 'a/ab.txt'))
eq(true, match('a/{a{a,b},b}', 'a/aa'))
eq(true, match('a/{a{a,b},b}', 'a/ab'))
eq(false, match('a/{a{a,b},b}', 'a/ac'))
eq(true, match('a/{a{a,b},b}', 'a/b'))
eq(false, match('a/{a{a,b},b}', 'a/c'))
eq(true, match('foo{bar,b*z}', 'foobar'))
eq(true, match('foo{bar,b*z}', 'foobuzz'))
eq(true, match('foo{bar,b*z}', 'foobarz'))
eq(true, match('{a,b}/c/{d,e}/**/*est.ts', 'a/c/d/one/two/three.test.ts'))
eq(true, match('{a,{d,e}b}/c', 'a/c'))
eq(true, match('{**/a,**/b}', 'b'))
end)
it('should match [] groups', function()
eq(true, match('[]', '[]')) -- empty [] is a literal
eq(false, match('[a-z]', ''))
eq(true, match('[a-z]', 'a'))
eq(false, match('[a-z]', 'ab'))
eq(true, match('[a-z]', 'z'))
eq(true, match('[a-z]', 'j'))
eq(false, match('[a-f]', 'j'))
eq(false, match('[a-z]', '`')) -- 'a' - 1
eq(false, match('[a-z]', '{')) -- 'z' + 1
eq(false, match('[a-z]', 'A'))
eq(false, match('[a-z]', '5'))
eq(true, match('[A-Z]', 'A'))
eq(true, match('[A-Z]', 'Z'))
eq(true, match('[A-Z]', 'J'))
eq(false, match('[A-Z]', '@')) -- 'A' - 1
eq(false, match('[A-Z]', '[')) -- 'Z' + 1
eq(false, match('[A-Z]', 'a'))
eq(false, match('[A-Z]', '5'))
eq(true, match('[a-zA-Z0-9]', 'z'))
eq(true, match('[a-zA-Z0-9]', 'Z'))
eq(true, match('[a-zA-Z0-9]', '9'))
eq(false, match('[a-zA-Z0-9]', '&'))
eq(true, match('[?]', '?'))
eq(false, match('[?]', 'a'))
eq(true, match('[*]', '*'))
eq(false, match('[*]', 'a'))
eq(true, match('[\\!]', '!'))
eq(true, match('a\\*b', 'a*b'))
eq(false, match('a\\*b', 'axb'))
end)
it('should match [!...] groups', function()
eq(true, match('[!]', '[!]')) -- [!] is a literal
eq(false, match('[!a-z]', ''))
eq(false, match('[!a-z]', 'a'))
eq(false, match('[!a-z]', 'z'))
eq(false, match('[!a-z]', 'j'))
eq(true, match('[!a-f]', 'j'))
eq(false, match('[!a-f]', 'jj'))
eq(true, match('[!a-z]', '`')) -- 'a' - 1
eq(true, match('[!a-z]', '{')) -- 'z' + 1
eq(false, match('[!a-zA-Z0-9]', 'a'))
eq(false, match('[!a-zA-Z0-9]', 'A'))
eq(false, match('[!a-zA-Z0-9]', '0'))
eq(true, match('[!a-zA-Z0-9]', '!'))
end)
it('should handle long patterns', function()
-- lpeg has a recursion limit of 200 by default, make sure the grammar does trigger it on
-- strings longer than that
local fill_200 = ('a'):rep(200)
eq(200, fill_200:len())
local long_lit = fill_200 .. 'a'
eq(false, match(long_lit, 'b'))
eq(true, match(long_lit, long_lit))
local long_pat = fill_200 .. 'a/**/*.c'
eq(true, match(long_pat, fill_200 .. 'a/b/c/d.c'))
end)
-- New test for unicode patterns from assets
it('should match unicode patterns', function()
eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.ts'))
eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.tsx'))
eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.js'))
eq(true, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.jsx'))
eq(false, match('😎/¢£.{ts,tsx,js,jsx}', '😎/¢£.jsxxxxxxxx'))
eq(true, match('*é*', 'café noir'))
eq(true, match('caf*noir', 'café noir'))
eq(true, match('caf*noir', 'cafeenoir'))
eq(true, match('F[ë£a]', ''))
eq(true, match('F[ë£a]', ''))
eq(true, match('F[ë£a]', 'Fa'))
end)
it('should match complex patterns', function()
eq(false, match('**/*.{c,h}', ''))
eq(false, match('**/*.{c,h}', 'c'))
eq(false, match('**/*.{c,h}', 'file.m'))
eq(true, match('**/*.{c,h}', 'file.c'))
eq(true, match('**/*.{c,h}', 'file.h'))
eq(true, match('**/*.{c,h}', '/file.c'))
eq(true, match('**/*.{c,h}', 'dir/subdir/file.c'))
eq(true, match('**/*.{c,h}', 'dir/subdir/file.h'))
eq(true, match('**/*.{c,h}', '/dir/subdir/file.c'))
eq(true, match('**/*.{c,h}', 'C:/dir/subdir/file.c'))
eq(true, match('/dir/**/*.{c,h}', '/dir/file.c'))
eq(false, match('/dir/**/*.{c,h}', 'dir/file.c'))
eq(true, match('/dir/**/*.{c,h}', '/dir/subdir/subdir/file.c'))
eq(true, match('{[0-9],[a-z]}', '0'))
eq(true, match('{[0-9],[a-z]}', 'a'))
eq(false, match('{[0-9],[a-z]}', 'A'))
-- glob is from willRename filter in typescript-language-server
-- https://github.com/typescript-language-server/typescript-language-server/blob/b224b878652438bcdd639137a6b1d1a6630129e4/src/lsp-server.ts#L266
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.js'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.ts'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.mts'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.mjs'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.cjs'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.cts'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.jsx'))
eq(true, match('**/*.{ts,js,jsx,tsx,mjs,mts,cjs,cts}', 'test.tsx'))
end)
end)
end)