diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index 670bae2e..67053632 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -1,4 +1,3 @@ -import fnmatch import hashlib import logging import re @@ -214,7 +213,7 @@ def expand_braces(pattern: str) -> list[str]: @lru_cache(maxsize=256) -def _translate_glob_no_double_star(pattern: str) -> str: +def _translate_glob_to_regex(pattern: str) -> str: res = [] i = 0 n = len(pattern) @@ -222,8 +221,22 @@ def _translate_glob_no_double_star(pattern: str) -> str: c = pattern[i] i += 1 if c == "*": - if not res or res[-1] != "[^/]*": - res.append("[^/]*") + if i < n and pattern[i] == "*": # ** + i += 1 + if i < n and pattern[i] == "/": # **/ + i += 1 + if res and res[-1] == "/": # /**/ + res = res[:-1] + res.append("(?:/|/.*/)") + elif res: # c**/ (with c != '/') + res.append(".*/") + else: # **/ at the start of the pattern + res.append("(?:.*/)?") + else: # c**d (with c, d != '/') + res.append(".*") + else: # single * + if not res or res[-1] != "[^/]*": + res.append("[^/]*") elif c == "?": res.append("[^/]") elif c == "[": @@ -271,36 +284,11 @@ def glob_match(pattern: str, path: str) -> bool: :param path: File path to match against :return: True if path matches pattern """ - pattern = pattern.replace("\\", "/") # Normalize backslashes to forward slashes - path = path.replace("\\", "/") # Normalize path backslashes to forward slashes + # normalise backslashes to forward slashes in both path and pattern + pattern = pattern.replace("\\", "/") + path = path.replace("\\", "/") - # Handle ** patterns that should match zero or more directories - if "**" in pattern: - # Method 1: Standard fnmatch (matches one or more directories) - regex1 = fnmatch.translate(pattern) - if re.match(regex1, path): - return True - - # Method 2: Handle zero-directory case by removing /** entirely - # Convert "src/**/test.py" to "src/test.py" - if "/**/" in pattern: - zero_dir_pattern = pattern.replace("/**/", "/") - regex2 = fnmatch.translate(zero_dir_pattern) - if re.match(regex2, path): - return True - - # Method 3: Handle leading ** case by removing **/ - # Convert "**/test.py" to "test.py" - if pattern.startswith("**/"): - zero_dir_pattern = pattern[3:] # Remove "**/" - regex3 = fnmatch.translate(zero_dir_pattern) - if re.match(regex3, path): - return True - - return False - else: - # Simple pattern without **, translate bare * and ? to not match / - return bool(re.match(_translate_glob_no_double_star(pattern), path)) + return bool(re.match(_translate_glob_to_regex(pattern), path)) class GlobMatcher: diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index ce69ae55..3fdcfa12 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -517,17 +517,21 @@ class TestGlobMatch: ("src/*.py", "src/sub/file.py", False), # Double asterisk patterns ("**agent.py", "agent.py", True), + ("**/agent.py", "agent.py", True), + ("**/agent.py", "src/serena/agent.py", True), + ("src/**/agent.py", "src/agent.py", True), + ("src/**/agent.py", "src/serena/foo/agent.py", True), + ("src/s**a/agent.py", "src/serena/agent.py", True), + ("src/s**a/agent.py", "src/serena/a/agent.py", True), ("**agent.py", "src/agent.py", True), ("**agent.py", "src/serena/agent.py", True), ("**agent.py", "src/serena/process_isolated_agent.py", True), ("**agent.py", "agent_test.py", False), - # Prefix with double asterisk ("src/**agent.py", "src/agent.py", True), ("src/**agent.py", "src/serena/agent.py", True), ("src/**agent.py", "src/serena/process_isolated_agent.py", True), ("src/**agent.py", "other/agent.py", False), ("src/**agent.py", "src/agent_test.py", False), - # Directory patterns ("src/**", "src/file.py", True), ("src/**", "src/dir/file.py", True), ("src/**", "other/file.py", False), @@ -538,6 +542,15 @@ class TestGlobMatch: # Simple patterns without asterisks ("src/file.py", "src/file.py", True), ("src/file.py", "src/other.py", False), + # Patterns with backslash + ("src\\file.py", "src/file.py", True), + ("src\\file.py", "src\\file.py", True), + ("src\\file.py", "src/other.py", False), + # Patterns with [] + ("file[0-9].py", "file1.py", True), + ("file[0-9].py", "filea.py", False), + ("file[!0-9].py", "filea.py", True), + ("file[!0-9].py", "file1.py", False), ], ) def test_glob_match(self, pattern, path, expected):