From 70d32f24531ef2cabd901780703023cf5d37a680 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:33:44 +0700 Subject: [PATCH] fix(util): ensure bare * and ? in non-** glob patterns do not match / (#1732) --- CHANGELOG.md | 1 + src/serena/util/text_utils.py | 42 ++++++++++++++++++++++++++++++++-- test/serena/test_text_utils.py | 6 +++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d794e1..054e1de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Add Grok Build support (context `grok`, setup CLI, hooks) - The `languages` key in project configurations was changed to `language_servers` to better reflect the actual semantics (configurations are automatically migrated) + - Fix: `glob_match` bare `*` and `?` in non-`**` patterns matched across `/`, contradicting its documented behavior #1732 * Language Servers: - Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index 4f8f10cc..dad80252 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -5,6 +5,7 @@ import re from collections.abc import Callable from dataclasses import dataclass, field from enum import StrEnum +from functools import lru_cache from typing import Any, Literal, Self from bs4 import BeautifulSoup @@ -240,6 +241,43 @@ def expand_braces(pattern: str) -> list[str]: return patterns +@lru_cache(maxsize=256) +def _translate_glob_no_double_star(pattern: str) -> str: + res = [] + i = 0 + n = len(pattern) + while i < n: + c = pattern[i] + i += 1 + if c == "*": + if not res or res[-1] != "[^/]*": + res.append("[^/]*") + elif c == "?": + res.append("[^/]") + elif c == "[": + j = i + if j < n and pattern[j] == "!": + j += 1 + if j < n and pattern[j] == "]": + j += 1 + while j < n and pattern[j] != "]": + j += 1 + if j >= n: + res.append(r"\[") + else: + stuff = pattern[i:j] + if stuff.startswith("!"): + stuff = "^" + stuff[1:] + elif stuff.startswith("^"): + stuff = r"\^" + stuff + res.append(f"[{stuff}]") + i = j + 1 + else: + res.append(re.escape(c)) + inner = "".join(res) + return f"(?s:{inner})\\Z" + + def glob_match(pattern: str, path: str) -> bool: """ Match a file path against a glob pattern. @@ -289,8 +327,8 @@ def glob_match(pattern: str, path: str) -> bool: return False else: - # Simple pattern without **, use fnmatch directly - return fnmatch.fnmatch(path, pattern) + # Simple pattern without **, translate bare * and ? to not match / + return bool(re.match(_translate_glob_no_double_star(pattern), path)) class GlobMatcher: diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 0eb86f3c..7d8074eb 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -593,10 +593,16 @@ class TestGlobMatch: [ # Basic wildcard patterns ("*.py", "file.py", True), + ("*.py", "src/file.py", False), ("*.py", "file.txt", False), ("*agent.py", "agent.py", True), ("*agent.py", "process_isolated_agent.py", True), + ("*agent.py", "src/agent.py", False), ("*agent.py", "agent_test.py", False), + ("a?b.py", "acb.py", True), + ("a?b.py", "a/b.py", False), + ("src/*.py", "src/file.py", True), + ("src/*.py", "src/sub/file.py", False), # Double asterisk patterns ("**agent.py", "agent.py", True), ("**agent.py", "src/agent.py", True),