match() treats paths wrongly
Done- Plan (done)
- Code (done)
- Test (done)
- Review (done)
- Finish (done)
Cost
$0.0150
Tests
Passed
Iterations
2
Re-plans
0
LLM calls
19
Prompt read from cache
75%
Usage by agent
- Cache read
- Cache write
- Input
- Output
View as table
| Agent | Cost | Cache read | Cache write | Input | Output |
|---|---|---|---|---|---|
| planner | $0.0017 | 4,441 | 2,654 | 8 | 2,659 |
| coder | $0.0061 | 35,793 | 14,548 | 18 | 7,870 |
| tester | $0.0072 | 66,497 | 17,428 | 22 | 8,657 |
Timeline
-
planner iteration 1 Finished 3 calls · 9,762 tokens · $0.0017 · 13.9s
Loading…
-
coder iteration 1 Finished 4 calls · 31.7K tokens · $0.0040 · 27.1s
Loading…
-
tester iteration 1 Finished 6 calls · 65.0K tokens · $0.0044 · 32.5s
Loading…
-
verify iteration 1 Failed 0.5s
.............F..... [100%] =================================== FAILURES =================================== ________________ test_trailing_globstar_matches_newline_in_name ________________ def test_trailing_globstar_matches_newline_in_name(): # '*' and '?' match any character except '/', including newline; # '/**' should be consistent with that. assert match("src/*", "src/a\nb") > assert match("src/**", "src/a\nb") E AssertionError: assert False E + where False = match('src/**', 'src/a\nb') test_pathmatch.py:78: AssertionError =========================== short test summary info ============================ FAILED test_pathmatch.py::test_trailing_globstar_matches_newline_in_name - As... 1 failed, 18 passed in 0.06s -
coder iteration 2 Finished 3 calls · 26.5K tokens · $0.0021 · 13.4s
Loading…
-
tester iteration 2 Finished 3 calls · 27.6K tokens · $0.0028 · 19.1s
Loading…
-
verify iteration 2 Passed 0.6s
................... [100%] 19 passed in 0.05s
Final diff
diff --git a/pathmatch.py b/pathmatch.py index 42f6c2a..e25414b 100644 --- a/pathmatch.py +++ b/pathmatch.py @@ -1,5 +1,58 @@ -import fnmatch +import functools +import re + + +def _bracket_class(pattern: str, i: int): + """Translate a bracket expression starting at pattern[i] == '['. + + Returns (regex_fragment, next_index). If the bracket is not closed, + the '[' is treated as a literal character. + """ + negated = pattern.startswith("!", i + 1) + start = i + 2 if negated else i + 1 + end = pattern.find("]", start + 1) + if end == -1: + return re.escape("["), i + 1 + content = "".join(re.escape(c) for c in pattern[start:end]) + if negated: + return "[^" + content + "/]", end + 1 + return "[" + content + "]", end + 1 + + +def _glob_to_regex(pattern: str) -> str: + parts = [] + i = 0 + n = len(pattern) + while i < n: + c = pattern[i] + if c == "*" and pattern.startswith("**/", i): + # zero or more whole directories + parts.append("(?:[^/]+/)*") + i += 3 + elif c == "/" and pattern.startswith("/**", i) and i + 3 == n: + # trailing /** : everything below the directory, not the directory + parts.append("/.+") + i += 3 + elif c == "*": + parts.append("[^/]*") + i += 1 + elif c == "?": + parts.append("[^/]") + i += 1 + elif c == "[": + fragment, i = _bracket_class(pattern, i) + parts.append(fragment) + else: + parts.append(re.escape(c)) + i += 1 + return "".join(parts) + + +@functools.lru_cache(maxsize=1024) +def _compile(pattern: str): + # DOTALL so that the '.' in '/.+' matches newlines, like '*' and '?' do. + return re.compile(_glob_to_regex(pattern), re.DOTALL) def match(pattern: str, path: str) -> bool: - return fnmatch.fnmatchcase(path, pattern) + return _compile(pattern).fullmatch(path) is not None diff --git a/test_pathmatch.py b/test_pathmatch.py index 1895e55..7867396 100644 --- a/test_pathmatch.py +++ b/test_pathmatch.py @@ -7,3 +7,106 @@ def test_star(): def test_question(): assert match("a?c", "abc") + + +def test_star_does_not_cross_slash(): + assert match("*.py", "a.py") + assert not match("*.py", "x/a.py") + + +def test_question_does_not_match_slash(): + assert not match("a?c", "a/c") + assert match("a?c", "abc") + + +def test_leading_globstar(): + assert match("**/a.py", "a.py") + assert match("**/a.py", "x/y/a.py") + assert not match("**/a.py", "x/b.py") + + +def test_middle_globstar(): + assert match("a/**/b", "a/b") + assert match("a/**/b", "a/x/y/b") + assert not match("a/**/b", "a/xb") + + +def test_trailing_globstar(): + assert match("src/**", "src/a") + assert match("src/**", "src/a/b") + assert not match("src/**", "src") + + +def test_bracket_set(): + assert match("[abc]x", "bx") + assert not match("[abc]x", "dx") + + +def test_bracket_negated(): + assert match("[!abc]x", "dx") + assert not match("[!abc]x", "ax") + assert not match("[!abc]x", "/x") + + +def test_unclosed_bracket_is_literal(): + assert match("[a", "[a") + assert not match("[a", "a") + + +def test_regex_metacharacters_are_literal(): + assert match("a.c", "a.c") + assert not match("a.c", "abc") + assert match("a+", "a+") + assert not match("a+", "aa") + assert match("(a)", "(a)") + assert not match("(a)", "a") + + +def test_full_path_anchoring(): + assert match("a.py", "a.py") + assert not match("a.py", "x/a.py") + + +def test_case_sensitive(): + assert not match("*.PY", "a.py") + + +def test_trailing_globstar_matches_newline_in_name(): + # '*' and '?' match any character except '/', including newline; + # '/**' should be consistent with that. + assert match("src/*", "src/a\nb") + assert match("src/**", "src/a\nb") + + +def test_double_star_without_slash_acts_as_star(): + assert match("a**b", "axb") + assert not match("a**b", "ax/b") + + +def test_bare_double_star_does_not_cross_slash(): + assert match("**", "ab") + assert not match("**", "a/b") + + +def test_empty_and_odd_brackets_do_not_break_regex(): + assert match("[]a]", "]") + assert match("[]a]", "a") + assert not match("[]a]", "b") + assert match("[!]x", "[!]x") + assert not match("[!]x", "zx") + assert not match("[!]x", "/x") + + +def test_bracket_special_characters_are_literal(): + assert match("[\\\\]", "\\") + assert match("[^a]", "^") + assert not match("[^a]", "b") + assert match("[a-c]", "-") + assert match("[a-c]", "a") + assert not match("[a-c]", "b") + + +def test_unclosed_bracket_with_later_text(): + assert match("a[", "a[") + assert match("[a]b", "ab") + assert not match("[a]b", "[a]b")