diff --git a/include/Support/GlobPattern.h b/include/Support/GlobPattern.h new file mode 100644 index 00000000..10abb49d --- /dev/null +++ b/include/Support/GlobPattern.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace clice { + +using GlobCharSet = std::bitset<256>; + +/// This class implements a glob pattern matcher to parse patterns to +/// watch relative to the base path. +/// +/// Glob patterns can have the following syntax: +/// - `*` to match one or more characters in a path segment +/// - `?` to match on one character in a path segment +/// - `**` to match any number of path segments, including none +/// - `{}` to group conditions (e.g. `***.{ts,js}` matches all TypeScript +/// and JavaScript files) +/// - `[]` to declare a range of characters to match in a path segment +/// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +/// - `[!...]` to negate a range of characters to match in a path segment +/// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, +/// but not `example.0`) +/// +/// Note: Use only `/` for path segment seperator +class GlobPattern { +public: + /// \param Pat the pattern to match against + /// \param MaxSubPatterns if provided limit the number of allowed subpatterns + /// created from expanding braces otherwise disable + /// brace expansion + static std::expected + create(llvm::StringRef s, std::optional max_subpattern_num = {}); + + // Returns true for glob pattern "*" or "**". Can be used to avoid expensive + // preparation/acquisition of the input for match(). + bool isTrivialMatchAll() const { + if(!prefix.empty()) { + // have no prefix + return false; + } + if((sub_globs.size() == 1 && sub_globs[0].getPat() == "*") || + (sub_globs.size() == 2 && sub_globs[0].getPat() == "**")) { + // not "*" or "**" + return true; + } + // default return false + return false; + } + + /// \returns \p true if \p str matches this glob pattern + bool match(llvm::StringRef s); + +private: + /// GlobPattern is seperated into `Prefix + SubGlobPattern` + std::string prefix; + + /// if prefix contains full path: + /// + /// xxx/yyy/*.c + /// ~~~~~~^ + /// + /// prefix contains incomplete path: + /// xxx/yyy*.c + /// ~~~~~~^ + /// + bool prefix_at_seg_end = false; + + /// SubGlobPattern: + /// Pattern `foo.{c,cpp,cppm}` + /// -> extend to 3 SubGlobPatterns: `foo.c`, `foo.cpp`, `foo.cppm` + struct SubGlobPattern { + /// \param Pat the pattern to match against + static std::expected create(llvm::StringRef s); + /// \returns \p true if \p S matches this glob pattern + bool match(llvm::StringRef str) const; + + llvm::StringRef getPat() const { + return llvm::StringRef{pat.data(), pat.size()}; + } + + struct Bracket { + size_t next_offset; + GlobCharSet bytes; + }; + + llvm::SmallVector brackets; + + // GlobSegment devide patterm into segments by '/' + // SubPattern: + // **include/aaa/bbb/test[0-9].cc + // ^~~~~~~~~^~~~^~~~^~~~~~~~~~~~~^ + // 1 2 3 4 + // Devided into 4 segments + // + // SubPattern: + // **/include/aaa/bbb/test[0-9].h + // ^~^~~~~~~~^~~~^~~~^~~~~~~~~~~~^ + // 1 2 3 4 5 + // Devided into 5 segments + struct GlobSegment { + size_t start; + size_t end; + }; + + llvm::SmallVector glob_segments; + + llvm::SmallVector pat; + }; + + llvm::SmallVector sub_globs; +}; + +} // namespace clice diff --git a/src/Support/GlobPattern.cpp b/src/Support/GlobPattern.cpp new file mode 100644 index 00000000..6ba8d9ba --- /dev/null +++ b/src/Support/GlobPattern.cpp @@ -0,0 +1,562 @@ +#include + +#include "Support/GlobPattern.h" + +namespace clice { + +// Expands character ranges and returns a bitmap. +// For example, "a-cf-hz" is expanded to "abcfghz". +static std::expected expand(llvm::StringRef s, llvm::StringRef original) { + GlobCharSet bv{false}; + + for(size_t i = 0, e = s.size(); i < e; ++i) { + switch(s[i]) { + case '\\': { + ++i; + if(i == e) { + return std::unexpected{"Invalid expansions: stary `\\`"}; + } + if(s[i] != '/') { + bv.set(s[i], true); + } + break; + } + + case '-': { + if(i == 0 || i + 1 == e) { + bv.set('-', true); + break; + } + char c_begin = s[i - 1]; + char c_end = s[i + 1]; + ++i; + if(c_end == '\\') { + ++i; + if(s[i] == e) { + return std::unexpected{"Invalid expansions: stary `\\`"}; + } + c_end = s[i]; + } + if(c_begin > c_end) { + return std::unexpected{ + std::format("Invalid expansion: `{}` is larger than `{}`", c_begin, c_end)}; + } + for(char c = c_begin; c <= c_end; ++c) { + if(c != '/') { + bv.set(c, true); + } + } + break; + } + + default: { + if(s[i] != '/') { + bv.set(s[i], true); + } + } + } + } + + return bv; +} + +// Identify brace expansions in S and return the list of patterns they expand +// into. +static std::expected, std::string> + parseBraceExpansions(llvm::StringRef s, std::optional max_subpattern_num) { + llvm::SmallVector subpatterns = {s.str()}; + if(!max_subpattern_num || !s.contains('{')) { + return subpatterns; + } + + struct BraceExpansion { + size_t start; + size_t length; + llvm::SmallVector terms; + }; + + llvm::SmallVector brace_expansions; + + BraceExpansion* current_be = nullptr; + size_t term_begin = 0; + for(size_t i = 0, e = s.size(); i != e; ++i) { + if(s[i] == '[') { + ++i; + if(s[i] == ']') { + ++i; + } + while(i != e && s[i] != ']') { + if(s[i] == '\\') { + if(i == e) { + return std::unexpected{ + "Invalid glob pattern, unmatched '[', with stray " "'\\' inside"}; + } + ++i; + } + ++i; + } + if(i == e) { + return std::unexpected{"Invalid glob pattern, unmatched '['"}; + } + } else if(s[i] == '{') { + if(current_be) { + return std::unexpected{"Nested brace expansions are not supported"}; + } + current_be = &brace_expansions.emplace_back(); + current_be->start = i; + term_begin = i + 1; + } else if(s[i] == ',') { + if(!current_be) { + continue; + } + current_be->terms.push_back(s.substr(term_begin, i - term_begin)); + term_begin = i + 1; + } else if(s[i] == '}') { + if(!current_be) { + continue; + } + if(current_be->terms.empty() && i - term_begin == 0) { + return std::unexpected{"Empty brace expression is not supported"}; + } + current_be->terms.push_back(s.substr(term_begin, i - term_begin)); + current_be->length = i - current_be->start + 1; + current_be = nullptr; + } else if(s[i] == '\\') { + ++i; + if(i == e) { + return std::unexpected{"Invalid glob pattern, stray '\\'"}; + } + } + } + + if(current_be) { + return std::unexpected{"Incomplete brace expansion"}; + } + + size_t subpattern_num = 1; + for(auto& be: brace_expansions) { + if(subpattern_num > std::numeric_limits::max() / be.terms.size()) { + subpattern_num = std::numeric_limits::max(); + break; + } + subpattern_num *= be.terms.size(); + } + + if(subpattern_num > *max_subpattern_num) { + return std::unexpected{"Too many brace expansions"}; + } + + for(auto& be: reverse(brace_expansions)) { + llvm::SmallVector orig_sub_patterns; + std::swap(subpatterns, orig_sub_patterns); + for(llvm::StringRef term: be.terms) { + for(llvm::StringRef orig: orig_sub_patterns) { + subpatterns.emplace_back(orig).replace(be.start, be.length, term); + } + } + } + + return subpatterns; +} + +std::expected + GlobPattern::create(llvm::StringRef s, std::optional max_subpattern_num) { + // Store the prefix that does not contain any metacharacter. + GlobPattern pat; + size_t prefix_size = s.find_first_of("?*[{\\"); + if(prefix_size == std::string::npos) { + pat.prefix = s.substr(0, prefix_size).str(); + // check if there is multiple `/` in prefix + size_t last_slash = 0; + size_t size = pat.prefix.size(); + for(size_t i = 0; i < size; ++i) { + if(s[i] == '/' && i - last_slash == 1) { + return std::unexpected{"Multiple `/` is not allowed"}; + } + last_slash = i; + } + return pat; + } + if(prefix_size != 0 && s[prefix_size - 1] == '/') { + pat.prefix_at_seg_end = true; + --prefix_size; + } + pat.prefix = s.substr(0, prefix_size).str(); + // check if there is multiple `/` in prefix + size_t last_slash = 0; + size_t size = pat.prefix.size(); + for(size_t i = 0; i < size; ++i) { + if(s[i] == '/' && i - last_slash == 1) { + return std::unexpected{"Multiple `/` is not allowed"}; + } + last_slash = i; + } + s = s.substr(pat.prefix_at_seg_end ? prefix_size + 1 : prefix_size); + + llvm::SmallVector sub_pats; + if(auto res = parseBraceExpansions(s, max_subpattern_num); res.has_value()) { + sub_pats = res.value(); + } else { + return std::unexpected{res.error()}; + } + + for(auto sub_pat: sub_pats) { + if(auto res = SubGlobPattern::create(sub_pat); res.has_value()) { + pat.sub_globs.push_back(res.value()); + } else { + return std::unexpected{res.error()}; + } + } + + return pat; +} + +std::expected + GlobPattern::SubGlobPattern::create(llvm::StringRef s) { + SubGlobPattern pat; + llvm::SmallVector glob_segments; + GlobSegment* current_gs = &glob_segments.emplace_back(); + current_gs->start = 0; + pat.pat.assign(s.begin(), s.end()); + // Parse brackets. + size_t e = s.size(); + for(size_t i = 0; i < e; ++i) { + if(!current_gs) { + current_gs = &glob_segments.emplace_back(); + current_gs->start = i; + } + if(s[i] == '[') { + ++i; + size_t j = i; + if(s[j] == ']') { + // ']' is allowed as the first character of a character class. '[]' + // is invalid. So, just skip the first character. + ++j; + } + while(j != e && s[j] != ']') { + // Check if there is invalid escape char + ++j; + if(s[j - 1] == '\\') { + if(j == e) { + // Reach here in such case: + // [a-zA-Z\] + // ~~~~~~~~^ + return std::unexpected{ + "Invalid glob pattern, unmatched '[' with stray '\\' inside"}; + } + ++j; + } + } + if(j == e) { + return std::unexpected{"Invalid glob pattern, unmatched '['"}; + } + llvm::StringRef chars = s.substr(i, j - i); + bool invert = s[i] == '^' || s[i] == '!'; + auto bv = invert ? expand(chars.substr(1), s) : expand(chars, s); + if(!bv.has_value()) { + return std::unexpected{bv.error()}; + } + if(invert) { + bv->flip(); + } + pat.brackets.push_back(Bracket{j + 1, bv.value()}); + i = j; + } else if(s[i] == '\\') { + if(++i == e) { + return std::unexpected{"Invalid glob pattern, stray '\\'"}; + } + } else if(s[i] == '/') { + if(i - current_gs->start == 1) { + return std::unexpected{"Multiple `/` is not allowed"}; + } + current_gs->end = i; + current_gs = nullptr; + } else if(s[i] == '*') { + // check if there is multiple `*` + if(i + 1 < e - 1 && s[i + 1] == '*' && s[i + 2] == '*') { + return std::unexpected{"Multiple `*` is not allowed"}; + } + } + } + + if(current_gs) { + current_gs->end = e; + current_gs = nullptr; + } + + std::swap(pat.glob_segments, glob_segments); + return pat; +} + +bool GlobPattern::match(llvm::StringRef str) { + if(!str.consume_front(prefix)) { + return false; + } + + if(str.empty() && sub_globs.empty()) { + return true; + } + + if(!str.empty() && prefix_at_seg_end) { + if(str[0] != '/') { + return false; + } + str = str.substr(1); + } + + for(auto& Glob: sub_globs) { + if(Glob.match(str)) { + return true; + } + } + return false; +} + +bool GlobPattern::SubGlobPattern::match(llvm::StringRef str) const { + // p: Current position in the pattern + // s: Current position in the String + // b: Current processed bracket num + // s_start: String start + // s_end: String end + // p_start: Pattern start + // p_end: Pattern end + // seg_start: Position of the start of current segment + // s_end: Position of the end of current segment + // b: Current matched bracket + // current_glob_seg: Index of glob patterns + // wild_mode: Whether wildcards are allowed to cross segments + const char* s = str.data(); + const char* const s_start = s; + const char* const s_end = s + str.size(); + const char* p = pat.data(); + const char* seg_start = p; + const char* const p_start = p; + const char* const p_end = p + pat.size(); + const char* seg_end = p + glob_segments[0].end; + size_t b = 0; + size_t current_glob_seg = 0; + bool wild_mode = false; + + // Status for backtracing + struct BacktraceStat { + size_t b; + size_t glob_seg; + bool wild_mode; + const char* p; + const char* s; + const char* seg_end; + const char* seg_start; + }; + + llvm::SmallVector backtrace_stack; + const size_t seg_num = glob_segments.size(); + + auto save_stat = + [&backtrace_stack, &b, ¤t_glob_seg, &wild_mode, &p, &s, &seg_end, &seg_start]() { + backtrace_stack.push_back({.b = b, + .glob_seg = current_glob_seg, + .wild_mode = wild_mode, + .p = p, + .s = s, + .seg_end = seg_end, + .seg_start = seg_start}); + }; + + while(current_glob_seg < seg_num) { + + if(s == s_end) { + // Return true if all pattern characters are processed or only + // '*' or '/' characters remain + return getPat().find_first_not_of("*/", p - pat.data()) == std::string::npos; + } + + if(p != seg_end) { + switch(*p) { + case '*': { + if(p + 1 != p_end && *(p + 1) == '*') { + // Met '**' + p += 2; + wild_mode = true; + // Consume extra '*' + while(p != p_end && (*p == '*' || *p == '/')) { + if(*p == '/') { + if(current_glob_seg + 1 == seg_num) { + return true; + } + ++current_glob_seg; + seg_start = p_start + glob_segments[current_glob_seg].start; + seg_end = p_start + glob_segments[current_glob_seg].end; + } + ++p; + } + if(p == seg_end) { + // '**' at segment end + if(current_glob_seg + 1 == seg_num) { + // '**' at pattern end + return true; + } + + // Goto next segment + ++current_glob_seg; + while(s != s_end && *s == '/') { + ++s; + } + p = p_start + glob_segments[current_glob_seg].start; + seg_start = p; + seg_end = p_start + glob_segments[current_glob_seg].end; + } + save_stat(); + } else { + // Met '*' + ++p; + wild_mode = false; + if(p == seg_end) { + while(s != s_end && *s != '/') { + ++s; + } + if(s == s_end) { + continue; + } + if(s + 1 != s_end) { + ++s; + } + if(current_glob_seg + 1 == seg_num) { + return true; + } + ++current_glob_seg; + p = p_start + glob_segments[current_glob_seg].start; + seg_start = p; + seg_end = p_start + glob_segments[current_glob_seg].end; + } + save_stat(); + } + continue; + } + + case '?': { + if(p + 1 != seg_end && *(p + 1) == '*') { + // Handle '?*' + unsigned offset = *s == '\\' ? 2 : 1; + s += offset; + save_stat(); + p += 2; + continue; + } + if(s != s_end && *s != '/') { + ++p; + s = *s == '\\' ? s + 2 : s + 1; + continue; + } + break; + } + + case '[': { + if(b < brackets.size() && brackets[b].bytes[uint8_t(*s)]) { + if(p == seg_start && !(s == s_start || *(s - 1) == '/')) { + // Segment start dismatch + break; + } + p = pat.data() + brackets[b].next_offset; + ++b; + ++s; + continue; + } + break; + } + + case '\\': { + if(p + 1 != seg_end && *(p + 1) == *s) { + if(p == seg_start && !(s == s_start || *(s - 1) == '/')) { + // Segment start dismatch + break; + } + p += 2; + ++s; + continue; + } + break; + } + + default: { + if(*p == *s) { + if(p == seg_start && !(s == s_start || *(s - 1) == '/')) { + // Segment start dismatch + break; + } + ++p; + ++s; + continue; + } + break; + } + } + + } else { + // P comes to a segment end + if(seg_end != p_end) { + if(wild_mode) { + // Step to next segment + ++current_glob_seg; + while(s != s_end && *s != '/') { + ++s; + } + if(s != s_end && *s == '/') { + ++s; + } + if(current_glob_seg >= seg_num) { + return s == s_end; + } + p = p_start + glob_segments[current_glob_seg].start; + seg_start = p; + seg_end = p_start + glob_segments[current_glob_seg].end; + continue; + } else { + if(*seg_end != *s) { + return false; + } + // *S and *SegEnd should be '/' + // Escape repeat '/' + while(s != s_end && *s == '/') { + ++s; + } + // Step to next segment + ++current_glob_seg; + p = p_start + glob_segments[current_glob_seg].start; + seg_start = p; + seg_end = p_start + glob_segments[current_glob_seg].end; + continue; + } + } + } + + if(backtrace_stack.empty()) { + return false; + } + + // Backstrace + auto& state = backtrace_stack.back(); + + p = state.p; + s = ++state.s; + b = state.b; + current_glob_seg = state.glob_seg; + wild_mode = state.wild_mode; + seg_start = state.seg_start; + seg_end = state.seg_end; + + // In non-WildMode, pop back invalid stat in the stack + if(!wild_mode && (s == s_end || *s == '/')) { + backtrace_stack.pop_back(); + continue; + } + + // In non-WildMode, '/' is a barrier + if(!wild_mode && *s == '/') { + return false; + } + } + + return s == s_end; +} + +} // namespace clice diff --git a/unittests/Compiler/Preamble.cpp b/unittests/Compiler/Preamble.cpp index 8fb30925..62e75d05 100644 --- a/unittests/Compiler/Preamble.cpp +++ b/unittests/Compiler/Preamble.cpp @@ -148,7 +148,6 @@ int main() { params.command = "clang++ -std=c++20 main.cpp"; params.content = content; params.outPath = path::join(".", "header2.pch"); - params.bound.reset(); { auto AST = compile(params); diff --git a/unittests/Support/GlobPattern.cpp b/unittests/Support/GlobPattern.cpp new file mode 100644 index 00000000..0e19fa61 --- /dev/null +++ b/unittests/Support/GlobPattern.cpp @@ -0,0 +1,484 @@ +#include + +#include "Test/Test.h" +#include "Support/GlobPattern.h" + +namespace clice::testing { + +#define PATDEF(NAME, PAT) \ + const char* PatString_##NAME = PAT; \ + auto Res##NAME = clice::GlobPattern::create(PatString_##NAME, 100); \ + if(!Res##NAME.has_value()) { \ + std::cout << Res##NAME.error() << '\n'; \ + } \ + assert(Res##NAME.has_value()); \ + auto NAME = Res##NAME.value(); + + +TEST(GlobPattern, PattermSema) { + auto Pat1 = clice::GlobPattern::create("**/****.{c,cc}", 100); + EXPECT_EQ(Pat1.has_value(), false); + + auto Pat2 = clice::GlobPattern::create("/foo/bar/baz////aaa.{c,cc}", 100); + EXPECT_EQ(Pat2.has_value(), false); + + auto Pat3 = clice::GlobPattern::create("/foo/bar/baz/**////*.{c,cc}", 100); + EXPECT_EQ(Pat3.has_value(), false); +} + +TEST(GlobPattern, Simple) { + PATDEF(Pat1, "node_modules") + EXPECT_EQ(Pat1.match("node_modules"), true); + EXPECT_EQ(Pat1.match("node_module"), false); + EXPECT_EQ(Pat1.match("/node_modules"), false); + EXPECT_EQ(Pat1.match("test/node_modules"), false); + + PATDEF(Pat2, "test.txt") + EXPECT_EQ(Pat2.match("test.txt"), true); + EXPECT_EQ(Pat2.match("test?txt"), false); + EXPECT_EQ(Pat2.match("/text.txt"), false); + EXPECT_EQ(Pat2.match("test/test.txt"), false); + + PATDEF(Pat3, "test(.txt") + EXPECT_EQ(Pat3.match("test(.txt"), true); + EXPECT_EQ(Pat3.match("test?txt"), false); + + PATDEF(Pat4, "qunit") + EXPECT_EQ(Pat4.match("qunit"), true); + EXPECT_EQ(Pat4.match("qunit.css"), false); + EXPECT_EQ(Pat4.match("test/qunit"), false); + + PATDEF(Pat5, "/DNXConsoleApp/**/*.cs") + EXPECT_EQ(Pat5.match("/DNXConsoleApp/Program.cs"), true); + EXPECT_EQ(Pat5.match("/DNXConsoleApp/foo/Program.cs"), true); +} + +TEST(GlobPattern, DotHidden) { + PATDEF(Pat1, ".*"); + EXPECT_EQ(Pat1.match(".git"), true); + EXPECT_EQ(Pat1.match(".hidden.txt"), true); + EXPECT_EQ(Pat1.match("git"), false); + EXPECT_EQ(Pat1.match("hidden.txt"), false); + EXPECT_EQ(Pat1.match("path/.git"), false); + EXPECT_EQ(Pat1.match("path/.hidden.txt"), false); + + PATDEF(Pat2, "**/.*"); + EXPECT_EQ(Pat2.match(".git"), true); + EXPECT_EQ(Pat2.match("/.git"), true); + EXPECT_EQ(Pat2.match(".hidden.txt"), true); + EXPECT_EQ(Pat2.match("git"), false); + EXPECT_EQ(Pat2.match("hidden.txt"), false); + EXPECT_EQ(Pat2.match("path/.git"), true); + EXPECT_EQ(Pat2.match("path/.hidden.txt"), true); + EXPECT_EQ(Pat2.match("/path/.git"), true); + EXPECT_EQ(Pat2.match("/path/.hidden.txt"), true); + EXPECT_EQ(Pat2.match("path/git"), false); + EXPECT_EQ(Pat2.match("pat.h/hidden.txt"), false); + + PATDEF(Pat3, "._*"); + EXPECT_EQ(Pat3.match("._git"), true); + EXPECT_EQ(Pat3.match("._hidden.txt"), true); + EXPECT_EQ(Pat3.match("git"), false); + EXPECT_EQ(Pat3.match("hidden.txt"), false); + EXPECT_EQ(Pat3.match("path/._git"), false); + EXPECT_EQ(Pat3.match("path/._hidden.txt"), false); + + PATDEF(Pat4, "**/._*"); + EXPECT_EQ(Pat4.match("._git"), true); + EXPECT_EQ(Pat4.match("._hidden.txt"), true); + EXPECT_EQ(Pat4.match("git"), false); + EXPECT_EQ(Pat4.match("hidden._txt"), false); + EXPECT_EQ(Pat4.match("path/._git"), true); + EXPECT_EQ(Pat4.match("path/._hidden.txt"), true); + EXPECT_EQ(Pat4.match("/path/._git"), true); + EXPECT_EQ(Pat4.match("/path/._hidden.txt"), true); + EXPECT_EQ(Pat4.match("path/git"), false); + EXPECT_EQ(Pat4.match("pat.h/hidden._txt"), false); +} + +TEST(GlobPattern, EscapeCharacter) { + PATDEF(Pat1, R"(\*star)") + EXPECT_EQ(Pat1.match("*star"), true); + + PATDEF(Pat2, R"(\{\*\})") + EXPECT_EQ(Pat2.match("{*}"), true); +} + +TEST(GlobPattern, BracketExpr) { + PATDEF(Pat1, R"([a-zA-Z\]])") + EXPECT_EQ(Pat1.match(R"(])"), true); + EXPECT_EQ(Pat1.match(R"([)"), false); + EXPECT_EQ(Pat1.match(R"(s)"), true); + EXPECT_EQ(Pat1.match(R"(S)"), true); + EXPECT_EQ(Pat1.match(R"(0)"), false); + + PATDEF(Pat2, R"([\^a-zA-Z""\\])") + EXPECT_EQ(Pat2.match(R"(")"), true); + EXPECT_EQ(Pat2.match(R"(^)"), true); + EXPECT_EQ(Pat2.match(R"(\)"), true); + EXPECT_EQ(Pat2.match(R"(")"), true); + EXPECT_EQ(Pat2.match(R"(x)"), true); + EXPECT_EQ(Pat2.match(R"(X)"), true); + EXPECT_EQ(Pat2.match(R"(0)"), false); + + PATDEF(Pat3, R"([!0-9a-fA-F\-+\*])") + EXPECT_EQ(Pat3.match("1"), false); + EXPECT_EQ(Pat3.match("*"), false); + EXPECT_EQ(Pat3.match("s"), true); + EXPECT_EQ(Pat3.match("S"), true); + EXPECT_EQ(Pat3.match("H"), true); + EXPECT_EQ(Pat3.match("]"), true); + + PATDEF(Pat4, R"([^\^0-9a-fA-F\-+\*])") + EXPECT_EQ(Pat4.match("1"), false); + EXPECT_EQ(Pat4.match("*"), false); + EXPECT_EQ(Pat4.match("^"), false); + EXPECT_EQ(Pat4.match("s"), true); + EXPECT_EQ(Pat4.match("S"), true); + EXPECT_EQ(Pat4.match("H"), true); + EXPECT_EQ(Pat4.match("]"), true); + + PATDEF(Pat5, R"([\*-\^])") + EXPECT_EQ(Pat5.match("*"), true); + EXPECT_EQ(Pat5.match("a"), false); + EXPECT_EQ(Pat5.match("z"), false); + EXPECT_EQ(Pat5.match("A"), true); + EXPECT_EQ(Pat5.match("Z"), true); + EXPECT_EQ(Pat5.match("\\"), true); + EXPECT_EQ(Pat5.match("^"), true); + EXPECT_EQ(Pat5.match("-"), true); + + PATDEF(Pat6, "foo.[^0-9]") + EXPECT_EQ(Pat6.match("foo.5"), false); + EXPECT_EQ(Pat6.match("foo.8"), false); + EXPECT_EQ(Pat6.match("bar.5"), false); + EXPECT_EQ(Pat6.match("foo.f"), true); + + PATDEF(Pat7, "foo.[!0-9]") + EXPECT_EQ(Pat7.match("foo.5"), false); + EXPECT_EQ(Pat7.match("foo.8"), false); + EXPECT_EQ(Pat7.match("bar.5"), false); + EXPECT_EQ(Pat7.match("foo.f"), true); + + PATDEF(Pat8, "foo.[0!^*?]") + EXPECT_EQ(Pat8.match("foo.5"), false); + EXPECT_EQ(Pat8.match("foo.8"), false); + EXPECT_EQ(Pat8.match("foo.0"), true); + EXPECT_EQ(Pat8.match("foo.!"), true); + EXPECT_EQ(Pat8.match("foo.^"), true); + EXPECT_EQ(Pat8.match("foo.*"), true); + EXPECT_EQ(Pat8.match("foo.?"), true); + + PATDEF(Pat9, "foo[/]bar") + EXPECT_EQ(Pat9.match("foo/bar"), false); + + PATDEF(Pat10, "foo.[[]") + EXPECT_EQ(Pat10.match("foo.["), true); + + PATDEF(Pat11, "foo.[]]") + EXPECT_EQ(Pat11.match("foo.]"), true); + + PATDEF(Pat12, "foo.[][!]") + EXPECT_EQ(Pat12.match("foo.]"), true); + EXPECT_EQ(Pat12.match("foo.["), true); + EXPECT_EQ(Pat12.match("foo.!"), true); + + PATDEF(Pat13, "foo.[]-]") + EXPECT_EQ(Pat13.match("foo.]"), true); + EXPECT_EQ(Pat13.match("foo.-"), true); + + PATDEF(Pat14, "foo.[0-9]") + EXPECT_EQ(Pat14.match("foo.5"), true); + EXPECT_EQ(Pat14.match("foo.8"), true); + EXPECT_EQ(Pat14.match("bar.5"), false); + EXPECT_EQ(Pat14.match("foo.f"), false); + +} + +TEST(GlobPattern, BraceExpr) { + PATDEF(Pat1, "*foo[0-9a-z].{c,cpp,cppm,?pp}") + EXPECT_EQ(Pat1.match("foo1.cc"), false); + EXPECT_EQ(Pat1.match("foo2.cpp"), true); + EXPECT_EQ(Pat1.match("foo3.cppm"), true); + EXPECT_EQ(Pat1.match("foot.cppm"), true); + EXPECT_EQ(Pat1.match("foot.hpp"), true); + EXPECT_EQ(Pat1.match("foot.app"), true); + EXPECT_EQ(Pat1.match("fooD.cppm"), false); + EXPECT_EQ(Pat1.match("BarfooD.cppm"), false); + EXPECT_EQ(Pat1.match("foofooD.cppm"), false); + + PATDEF(Pat2, "proj/{build*,include,src}/*.{cc,cpp,h,hpp}") + EXPECT_EQ(Pat2.match("proj/include/foo.cc"), true); + EXPECT_EQ(Pat2.match("proj/include/bar.cpp"), true); + EXPECT_EQ(Pat2.match("proj/include/xxx/yyy/zzz/foo.cc"), false); + EXPECT_EQ(Pat2.match("proj/build-yyy/foo.h"), true); + EXPECT_EQ(Pat2.match("proj/build-xxx/foo.cpp"), true); + EXPECT_EQ(Pat2.match("proj/build/foo.cpp"), true); + EXPECT_EQ(Pat2.match("proj/build-xxx/xxx/yyy/zzz/foo.cpp"), false); + + PATDEF(Pat3, "*.{html,js}") + EXPECT_EQ(Pat3.match("foo.js"), true); + EXPECT_EQ(Pat3.match("foo.html"), true); + EXPECT_EQ(Pat3.match("folder/foo.js"), false); + EXPECT_EQ(Pat3.match("/node_modules/foo.js"), false); + EXPECT_EQ(Pat3.match("foo.jss"), false); + EXPECT_EQ(Pat3.match("some.js/test"), false); + + PATDEF(Pat4, "*.{html}") + EXPECT_EQ(Pat4.match("foo.html"), true); + EXPECT_EQ(Pat4.match("foo.js"), false); + EXPECT_EQ(Pat4.match("folder/foo.js"), false); + EXPECT_EQ(Pat4.match("/node_modules/foo.js"), false); + EXPECT_EQ(Pat4.match("foo.jss"), false); + EXPECT_EQ(Pat4.match("some.js/test"), false); + + PATDEF(Pat5, "{node_modules,testing}") + EXPECT_EQ(Pat5.match("node_modules"), true); + EXPECT_EQ(Pat5.match("testing"), true); + EXPECT_EQ(Pat5.match("node_module"), false); + EXPECT_EQ(Pat5.match("dtesting"), false); + + PATDEF(Pat6, "**/{foo,bar}") + EXPECT_EQ(Pat6.match("foo"), true); + EXPECT_EQ(Pat6.match("bar"), true); + EXPECT_EQ(Pat6.match("test/foo"), true); + EXPECT_EQ(Pat6.match("test/bar"), true); + EXPECT_EQ(Pat6.match("other/more/foo"), true); + EXPECT_EQ(Pat6.match("other/more/bar"), true); + EXPECT_EQ(Pat6.match("/foo"), true); + EXPECT_EQ(Pat6.match("/bar"), true); + EXPECT_EQ(Pat6.match("/test/foo"), true); + EXPECT_EQ(Pat6.match("/test/bar"), true); + EXPECT_EQ(Pat6.match("/other/more/foo"), true); + EXPECT_EQ(Pat6.match("/other/more/bar"), true); + + PATDEF(Pat7, "{foo,bar}/**") + EXPECT_EQ(Pat7.match("foo"), true); + EXPECT_EQ(Pat7.match("bar"), true); + EXPECT_EQ(Pat7.match("bar/"), true); + EXPECT_EQ(Pat7.match("foo/test"), true); + EXPECT_EQ(Pat7.match("bar/test"), true); + EXPECT_EQ(Pat7.match("bar/test/"), true); + EXPECT_EQ(Pat7.match("foo/other/more"), true); + EXPECT_EQ(Pat7.match("bar/other/more"), true); + EXPECT_EQ(Pat7.match("bar/other/more/"), true); + + PATDEF(Pat8, "{**/*.d.ts,**/*.js}") + EXPECT_EQ(Pat8.match("foo.js"), true); + EXPECT_EQ(Pat8.match("testing/foo.js"), true); + EXPECT_EQ(Pat8.match("/testing/foo.js"), true); + EXPECT_EQ(Pat8.match("foo.d.ts"), true); + EXPECT_EQ(Pat8.match("testing/foo.d.ts"), true); + EXPECT_EQ(Pat8.match("/testing/foo.d.ts"), true); + EXPECT_EQ(Pat8.match("foo.d"), false); + EXPECT_EQ(Pat8.match("testing/foo.d"), false); + EXPECT_EQ(Pat8.match("/testing/foo.d"), false); + + PATDEF(Pat9, "{**/*.d.ts,**/*.js,path/simple.jgs}") + EXPECT_EQ(Pat9.match("foo.js"), true); + EXPECT_EQ(Pat9.match("testing/foo.js"), true); + EXPECT_EQ(Pat9.match("/testing/foo.js"), true); + EXPECT_EQ(Pat9.match("path/simple.jgs"), true); + EXPECT_EQ(Pat9.match("/path/simple.jgs"), false); + + PATDEF(Pat10, "{**/*.d.ts,**/*.js,foo.[0-9]}") + EXPECT_EQ(Pat10.match("foo.5"), true); + EXPECT_EQ(Pat10.match("foo.8"), true); + EXPECT_EQ(Pat10.match("bar.5"), false); + EXPECT_EQ(Pat10.match("foo.f"), false); + EXPECT_EQ(Pat10.match("foo.js"), true); + + PATDEF(Pat11, "prefix/{**/*.d.ts,**/*.js,foo.[0-9]}") + EXPECT_EQ(Pat11.match("prefix/foo.5"), true); + EXPECT_EQ(Pat11.match("prefix/foo.8"), true); + EXPECT_EQ(Pat11.match("prefix/bar.5"), false); + EXPECT_EQ(Pat11.match("prefix/foo.f"), false); + EXPECT_EQ(Pat11.match("prefix/foo.js"), true); +} + +TEST(GlobPattern, WildGlob) { + PATDEF(Pat1, "**/*") + EXPECT_EQ(Pat1.match("foo"), true); + EXPECT_EQ(Pat1.match("foo/bar/baz"), true); + + PATDEF(Pat2, "**/[0-9]*") + EXPECT_EQ(Pat2.match("114514foo"), true); + EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/zzz"), false); + EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/zzz114514"), false); + EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/114514"), true); + EXPECT_EQ(Pat2.match("foo/bar/baz/xxx/yyy/114514zzz"), true); + + PATDEF(Pat3, "**/*[0-9]") + EXPECT_EQ(Pat3.match("foo5"), true); + EXPECT_EQ(Pat3.match("foo/bar/baz/xxx/yyy/zzz"), false); + EXPECT_EQ(Pat3.match("foo/bar/baz/xxx/yyy/zzz114514"), true); + + PATDEF(Pat4, "**/include/test/*.{cc,hh,c,h,cpp,hpp}") + EXPECT_EQ(Pat4.match("include/test/aaa.cc"), true); + EXPECT_EQ(Pat4.match("/include/test/aaa.cc"), true); + EXPECT_EQ(Pat4.match("xxx/yyy/include/test/aaa.cc"), true); + EXPECT_EQ(Pat4.match("include/foo/bar/baz/include/test/bbb.hh"), true); + EXPECT_EQ(Pat4.match("include/include/include/include/include/test/bbb.hpp"), true); + + PATDEF(Pat5, "**include/test/*.{cc,hh,c,h,cpp,hpp}") + EXPECT_EQ(Pat5.match("include/test/fff.hpp"), true); + EXPECT_EQ(Pat5.match("xxx-yyy-include/test/fff.hpp"), true); + EXPECT_EQ(Pat5.match("xxx-yyy-include/test/.hpp"), true); + EXPECT_EQ(Pat5.match("/include/test/aaa.cc"), true); + EXPECT_EQ(Pat5.match("include/foo/bar/baz/include/test/bbb.hh"), true); + + PATDEF(Pat6, "**/*foo.{c,cpp}") + EXPECT_EQ(Pat6.match("bar/foo.cpp"), true); + EXPECT_EQ(Pat6.match("bar/barfoo.cpp"), true); + EXPECT_EQ(Pat6.match("/foofoo.cpp"), true); + EXPECT_EQ(Pat6.match("foo/foo/foo/foo/foofoo.cpp"), true); + EXPECT_EQ(Pat6.match("foofoo.cpp"), true); + EXPECT_EQ(Pat6.match("barfoo.cpp"), true); + EXPECT_EQ(Pat6.match("foo.cpp"), true); + + // Boundary test of `**` + PATDEF(Pat7, "**") + EXPECT_EQ(Pat7.match("foo"), true); + EXPECT_EQ(Pat7.match("foo/bar/baz"), true); + + PATDEF(Pat8, "x/**") + EXPECT_EQ(Pat8.match("x/"), true); + EXPECT_EQ(Pat8.match("x/foo/bar/baz"), true); + EXPECT_EQ(Pat8.match("x"), true); + + PATDEF(Pat9, "**/x") + EXPECT_EQ(Pat9.match("x"), true); + EXPECT_EQ(Pat9.match("/x"), true); + EXPECT_EQ(Pat9.match("/x/x/x/x/x"), true); + + PATDEF(Pat10, "**/*") + EXPECT_EQ(Pat10.match("foo"), true); + EXPECT_EQ(Pat10.match("foo/bar"), true); + EXPECT_EQ(Pat10.match("foo/bar/baz"), true); + + PATDEF(Pat11, "**/*.{cc,cpp}") + EXPECT_EQ(Pat11.match("foo/bar/baz.cc"), true); + EXPECT_EQ(Pat11.match("foo/foo/foo.cpp"), true); + EXPECT_EQ(Pat11.match("foo/bar/.cc"), true); + + PATDEF(Pat12, "**/*?.{cc,cpp}") + EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/aaa.cc"), true); + EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/a.cc"), true); + EXPECT_EQ(Pat12.match("foo/bar/baz/xxx/yyy/zzz/.cc"), false); + + PATDEF(Pat13, "**/?*.{cc,cpp}") + EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/aaa.cc"), true); + EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/a.cc"), true); + EXPECT_EQ(Pat13.match("foo/bar/baz/xxx/yyy/zzz/.cc"), false); + + PATDEF(Pat14, "**/*[0-9]") + EXPECT_EQ(Pat14.match("foo5"), true); + EXPECT_EQ(Pat14.match("foo/bar/baz/xxx/yyy/zzz"), false); + EXPECT_EQ(Pat14.match("foo/bar/baz/xxx/yyy/zzz114514"), true); + + PATDEF(Pat15, "**/*") + EXPECT_EQ(Pat15.match("foo"), true); + EXPECT_EQ(Pat15.match("foo/bar/baz"), true); + + PATDEF(Pat16, "**/*.js") + EXPECT_EQ(Pat16.match("foo.js"), true); + EXPECT_EQ(Pat16.match("/foo.js"), true); + EXPECT_EQ(Pat16.match("folder/foo.js"), true); + EXPECT_EQ(Pat16.match("/node_modules/foo.js"), true); + EXPECT_EQ(Pat16.match("foo.jss"), false); + EXPECT_EQ(Pat16.match("some.js/test"), false); + EXPECT_EQ(Pat16.match("/some.js/test"), false); + + PATDEF(Pat17, "**/project.json") + EXPECT_EQ(Pat17.match("project.json"), true); + EXPECT_EQ(Pat17.match("/project.json"), true); + EXPECT_EQ(Pat17.match("some/folder/project.json"), true); + EXPECT_EQ(Pat17.match("/some/folder/project.json"), true); + EXPECT_EQ(Pat17.match("some/folder/file_project.json"), false); + EXPECT_EQ(Pat17.match("some/folder/fileproject.json"), false); + EXPECT_EQ(Pat17.match("some/rrproject.json"), false); + + PATDEF(Pat18, "test/**") + EXPECT_EQ(Pat18.match("test"), true); + EXPECT_EQ(Pat18.match("test/foo"), true); + EXPECT_EQ(Pat18.match("test/foo/"), true); + EXPECT_EQ(Pat18.match("test/foo.js"), true); + EXPECT_EQ(Pat18.match("test/other/foo.js"), true); + EXPECT_EQ(Pat18.match("est/other/foo.js"), false); + + PATDEF(Pat19, "**") + EXPECT_EQ(Pat19.match("/"), true); + EXPECT_EQ(Pat19.match("foo.js"), true); + EXPECT_EQ(Pat19.match("folder/foo.js"), true); + EXPECT_EQ(Pat19.match("folder/foo/"), true); + EXPECT_EQ(Pat19.match("/node_modules/foo.js"), true); + EXPECT_EQ(Pat19.match("foo.jss"), true); + EXPECT_EQ(Pat19.match("some.js/test"), true); + + PATDEF(Pat20, "test/**/*.js") + EXPECT_EQ(Pat20.match("test/foo.js"), true); + EXPECT_EQ(Pat20.match("test/other/foo.js"), true); + EXPECT_EQ(Pat20.match("test/other/more/foo.js"), true); + EXPECT_EQ(Pat20.match("test/foo.ts"), false); + EXPECT_EQ(Pat20.match("test/other/foo.ts"), false); + EXPECT_EQ(Pat20.match("test/other/more/foo.ts"), false); + + PATDEF(Pat21, "**/**/*.js") + EXPECT_EQ(Pat21.match("foo.js"), true); + EXPECT_EQ(Pat21.match("/foo.js"), true); + EXPECT_EQ(Pat21.match("folder/foo.js"), true); + EXPECT_EQ(Pat21.match("/node_modules/foo.js"), true); + EXPECT_EQ(Pat21.match("foo.jss"), false); + EXPECT_EQ(Pat21.match("some.js/test"), false); + + PATDEF(Pat22, "**/node_modules/**/*.js") + EXPECT_EQ(Pat22.match("foo.js"), false); + EXPECT_EQ(Pat22.match("folder/foo.js"), false); + EXPECT_EQ(Pat22.match("node_modules/foo.js"), true); + EXPECT_EQ(Pat22.match("/node_modules/foo.js"), true); + EXPECT_EQ(Pat22.match("node_modules/some/folder/foo.js"), true); + EXPECT_EQ(Pat22.match("/node_modules/some/folder/foo.js"), true); + EXPECT_EQ(Pat22.match("node_modules/some/folder/foo.ts"), false); + EXPECT_EQ(Pat22.match("foo.jss"), false); + EXPECT_EQ(Pat22.match("some.js/test"), false); + + PATDEF(Pat23, "{**/node_modules/**,**/.git/**,**/bower_components/**}") + EXPECT_EQ(Pat23.match("node_modules"), true); + EXPECT_EQ(Pat23.match("/node_modules"), true); + EXPECT_EQ(Pat23.match("/node_modules/more"), true); + EXPECT_EQ(Pat23.match("some/test/node_modules"), true); + EXPECT_EQ(Pat23.match("/some/test/node_modules"), true); + EXPECT_EQ(Pat23.match("bower_components"), true); + EXPECT_EQ(Pat23.match("bower_components/more"), true); + EXPECT_EQ(Pat23.match("/bower_components"), true); + EXPECT_EQ(Pat23.match("some/test/bower_components"), true); + EXPECT_EQ(Pat23.match("/some/test/bower_components"), true); + EXPECT_EQ(Pat23.match(".git"), true); + EXPECT_EQ(Pat23.match("/.git"), true); + EXPECT_EQ(Pat23.match("some/test/.git"), true); + EXPECT_EQ(Pat23.match("/some/test/.git"), true); + EXPECT_EQ(Pat23.match("tempting"), false); + EXPECT_EQ(Pat23.match("/tempting"), false); + EXPECT_EQ(Pat23.match("some/test/tempting"), false); + EXPECT_EQ(Pat23.match("/some/test/tempting"), false); + + PATDEF(Pat24, "{**/package.json,**/project.json}") + EXPECT_EQ(Pat24.match("package.json"), true); + EXPECT_EQ(Pat24.match("/package.json"), true); + EXPECT_EQ(Pat24.match("xpackage.json"), false); + EXPECT_EQ(Pat24.match("/xpackage.json"), false); + + PATDEF(Pat25, "some/**/*.js") + EXPECT_EQ(Pat25.match("some/foo.js"), true); + EXPECT_EQ(Pat25.match("some/folder/foo.js"), true); + EXPECT_EQ(Pat25.match("something/foo.js"), false); + EXPECT_EQ(Pat25.match("something/folder/foo.js"), false); + + PATDEF(Pat26, "some/**/*") + EXPECT_EQ(Pat26.match("some/foo.js"), true); + EXPECT_EQ(Pat26.match("some/folder/foo.js"), true); + EXPECT_EQ(Pat26.match("something/foo.js"), false); + EXPECT_EQ(Pat26.match("something/folder/foo.js"), false); +} + + } // namespace clice::testing