Make GlobMatcher with cached regex patterns the only glob matching mechanism

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-07-23 22:48:27 +02:00
1 parent 7033e90306
commit 954d8554fd
5 files changed
+30 -39

No files matched your search

+1 -1
View File
@@ -6,7 +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
- Fix: glob matching bare `*` and `?` in non-`**` patterns matched across `/`, contradicting documented behaviour #1732
* Language Servers:
- Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during
+2 -2
View File
@@ -33,7 +33,7 @@ from serena.constants import (
SERENA_MANAGED_DIR_NAME,
)
from serena.util.inspection import compute_language_server_support_composition
from serena.util.text_utils import glob_match
from serena.util.text_utils import GlobMatcher
from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml_comments, save_yaml, transfer_yaml_comments
from solidlsp.ls_config import LanguageServerId
@@ -1437,7 +1437,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
"""
project_root_str = str(project_root)
for pattern in self.trusted_project_path_patterns:
if glob_match(pattern, project_root_str):
if GlobMatcher(pattern).matches(project_root_str):
return True
return False
+5 -6
View File
@@ -16,10 +16,9 @@ from serena.tools import SUCCESS_RESULT, EditedFileContext, EditingToolWithDiagn
from serena.util.file_system import scan_directory
from serena.util.text_utils import (
ContentReplacer,
GlobMatcher,
MultiFileContentReplacer,
ReplacementOccurrence,
expand_braces,
glob_match,
)
from solidlsp.ls_utils import TextUtils
@@ -350,13 +349,13 @@ class ReplaceInFilesTool(EditingToolWithDiagnostics):
is_ignored_file=self.project.is_ignored_path,
relative_to=self.get_project_root(),
)
include_patterns = expand_braces(paths_include_glob.strip()) if paths_include_glob.strip() else None
exclude_patterns = expand_braces(paths_exclude_glob.strip()) if paths_exclude_glob.strip() else None
include_glob_matcher = GlobMatcher(paths_include_glob.strip()) if paths_include_glob.strip() else None
exclude_glob_matcher = GlobMatcher(paths_exclude_glob.strip()) if paths_exclude_glob.strip() else None
files: list[tuple[str, str]] = []
for path in sorted(rel_paths):
if include_patterns and not any(glob_match(p, path) for p in include_patterns):
if include_glob_matcher and not include_glob_matcher.matches(path):
continue
if exclude_patterns and any(glob_match(p, path) for p in exclude_patterns):
if exclude_glob_matcher and exclude_glob_matcher.matches(path):
continue
try:
files.append((path, self.project.read_file(path)))
+16 -22
View File
@@ -4,11 +4,11 @@ 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
from joblib import Parallel, delayed
from sensai.util.string import ToStringMixin
from serena.util.file_proxy import FileCollection, FileProxy
from solidlsp.ls_utils import TextUtils
@@ -185,7 +185,7 @@ def search_text(
return matches
def expand_braces(pattern: str) -> list[str]:
def _expand_braces(pattern: str) -> list[str]:
"""
Expands brace patterns in a glob string.
For example, "**/*.{js,jsx,ts,tsx}" becomes ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"].
@@ -212,7 +212,6 @@ def expand_braces(pattern: str) -> list[str]:
return patterns
@lru_cache(maxsize=256)
def _translate_glob_to_regex(pattern: str) -> str:
res = []
i = 0
@@ -263,9 +262,9 @@ def _translate_glob_to_regex(pattern: str) -> str:
return f"(?s:{inner})\\Z"
def glob_match(pattern: str, path: str) -> bool:
class GlobMatcher(ToStringMixin):
"""
Match a file path against a glob pattern.
Supports matching a file path against a glob pattern.
Supports standard glob patterns:
- * matches any number of characters except /
@@ -275,28 +274,23 @@ def glob_match(pattern: str, path: str) -> bool:
Supports brace expansion:
- {a,b,c} expands to multiple patterns (including nesting)
Unsupported patterns:
- Bash extended glob features are unavailable in Python's fnmatch
- Extended globs like !(), ?(), +(), *(), @() are not supported
:param pattern: Glob pattern (e.g., 'src/**/*.py', '**agent.py')
:param path: File path to match against
:return: True if path matches pattern
"""
# normalise backslashes to forward slashes in both path and pattern
pattern = pattern.replace("\\", "/")
path = path.replace("\\", "/")
return bool(re.match(_translate_glob_to_regex(pattern), path))
class GlobMatcher:
def __init__(self, expr: str):
self._patterns = expand_braces(expr)
"""
:param expr: a glob pattern which may make use of brace expansion (e.g., "src/**/*.{js,jsx,ts,tsx}")
"""
expr = expr.replace("\\", "/") # normalise backslashes to forward slashes
self._glob_expr = expr
self._glob_patterns = _expand_braces(expr)
self._regex_patterns = [re.compile(_translate_glob_to_regex(p)) for p in self._glob_patterns]
def _tostring_includes(self) -> list[str]:
return ["_glob_expr"]
def matches(self, path: str) -> bool:
return any(glob_match(p, path) for p in self._patterns)
path = path.replace("\\", "/") # normalise backslashes to forward slashes
return any(regex.match(path) for regex in self._regex_patterns)
def search_files(
+6 -8
View File
@@ -3,7 +3,7 @@ from collections.abc import Callable
import pytest
from serena.util.file_proxy import FileCollection, FileProxy
from serena.util.text_utils import LineType, MultiFileContentReplacer, search_files, search_text
from serena.util.text_utils import GlobMatcher, LineType, MultiFileContentReplacer, search_files, search_text
class TestSearchText:
@@ -555,9 +555,7 @@ class TestGlobMatch:
)
def test_glob_match(self, pattern, path, expected):
"""Test glob_match function with various patterns."""
from serena.util.text_utils import glob_match
assert glob_match(pattern, path) == expected
assert GlobMatcher(pattern).matches(path) == expected
class TestExpandBraces:
@@ -582,9 +580,9 @@ class TestExpandBraces:
)
def test_expand_braces(self, pattern, expected):
"""Test brace expansion for glob patterns."""
from serena.util.text_utils import expand_braces
from serena.util.text_utils import _expand_braces
assert sorted(expand_braces(pattern)) == sorted(expected)
assert sorted(_expand_braces(pattern)) == sorted(expected)
@pytest.mark.parametrize(
"pattern",
@@ -596,10 +594,10 @@ class TestExpandBraces:
)
def test_expand_braces_rejects_malformed_braces(self, pattern):
"""Malformed brace globs should fail instead of looping forever."""
from serena.util.text_utils import expand_braces
from serena.util.text_utils import _expand_braces
with pytest.raises(ValueError, match="Invalid glob brace expression"):
expand_braces(pattern)
_expand_braces(pattern)
class TestMultiFileContentReplacer: