From f39906728da524fd0d1669466ad1c0de9685682c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 1 Jul 2026 13:56:41 +0200 Subject: [PATCH] search_text: Remove obsolete option allow_multiline_match The obsolete option was only used in tests, where adjusting the `multiline` parameter restored the tested functionality. --- src/serena/util/text_utils.py | 76 +++++++++++----------------------- test/serena/test_text_utils.py | 17 ++++---- 2 files changed, 32 insertions(+), 61 deletions(-) diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index b8020e12..67110f70 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -141,7 +141,6 @@ def search_text( pattern: str, content: str | None = None, source_file_path: str | None = None, - allow_multiline_match: bool = False, context_lines_before: int = 0, context_lines_after: int = 0, is_glob: bool = False, @@ -154,8 +153,6 @@ def search_text( :param content: The text content to search. May be None if source_file_path is provided. :param source_file_path: Optional path to the source file. If content is None, this has to be passed and the file will be read. - :param allow_multiline_match: Whether to search across multiple lines. Currently, the default - option (False) is very inefficient, so it is recommended to set this to True. :param context_lines_before: Number of context lines to include before matches :param context_lines_after: Number of context lines to include after matches :param is_glob: If True, pattern is treated as a glob-like pattern (e.g., "*.py", "test_??.py") @@ -178,60 +175,36 @@ def search_text( # Convert pattern to a compiled regex if it's a string if is_glob: pattern = glob_to_regex(pattern) - if allow_multiline_match: - # For multiline matches, optionally use DOTALL so '.' matches newlines - flags = (re.MULTILINE | re.DOTALL) if multiline else 0 - compiled_pattern = re.compile(pattern, flags) - # Search across the entire content as a single string - for match in compiled_pattern.finditer(content): - start_pos = match.start() - end_pos = match.end() - # Find the line numbers for the start and end positions - start_line_num = content[:start_pos].count("\n") - end_line_num = content[:end_pos].count("\n") + # For multiline matches, optionally use DOTALL so '.' matches newlines + flags = (re.MULTILINE | re.DOTALL) if multiline else 0 + compiled_pattern = re.compile(pattern, flags) + # Search across the entire content as a single string + for match in compiled_pattern.finditer(content): + start_pos = match.start() + end_pos = match.end() - # Calculate the range of lines to include in the context - context_start = max(0, start_line_num - context_lines_before) - context_end = min(total_lines - 1, end_line_num + context_lines_after) + # Find the line numbers for the start and end positions + start_line_num = content[:start_pos].count("\n") + end_line_num = content[:end_pos].count("\n") - # Create TextLine objects for the context - context_lines = [] - for line_num in range(context_start, context_end + 1): - if context_start <= line_num < start_line_num: - match_type = LineType.BEFORE_MATCH - elif end_line_num < line_num <= context_end: - match_type = LineType.AFTER_MATCH - else: - match_type = LineType.MATCH + # Calculate the range of lines to include in the context + context_start = max(0, start_line_num - context_lines_before) + context_end = min(total_lines - 1, end_line_num + context_lines_after) - context_lines.append(TextLine(line_number=line_num, line_content=lines[line_num], match_type=match_type)) + # Create TextLine objects for the context + context_lines = [] + for line_num in range(context_start, context_end + 1): + if context_start <= line_num < start_line_num: + match_type = LineType.BEFORE_MATCH + elif end_line_num < line_num <= context_end: + match_type = LineType.AFTER_MATCH + else: + match_type = LineType.MATCH - matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path)) - else: - # TODO: extremely inefficient! Since we currently don't use this option in SerenaAgent or LanguageServer, - # it is not urgent to fix, but should be either improved or the option should be removed. - # Search line by line, normal compile without DOTALL - compiled_pattern = re.compile(pattern) - for i, line in enumerate(lines): - if compiled_pattern.search(line): - # Calculate the range of lines to include in the context - context_start = max(0, i - context_lines_before) - context_end = min(total_lines - 1, i + context_lines_after) + context_lines.append(TextLine(line_number=line_num, line_content=lines[line_num], match_type=match_type)) - # Create TextLine objects for the context - context_lines = [] - for j in range(context_start, context_end + 1): - if j < i: - match_type = LineType.BEFORE_MATCH - elif j > i: - match_type = LineType.AFTER_MATCH - else: - match_type = LineType.MATCH - - context_lines.append(TextLine(line_number=j, line_content=lines[j], match_type=match_type)) - - matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path)) + matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path)) return matches @@ -354,7 +327,6 @@ def search_files( pattern, content=file_content, source_file_path=relative_path, - allow_multiline_match=True, context_lines_before=context_lines_before, context_lines_after=context_lines_after, multiline=multiline, diff --git a/test/serena/test_text_utils.py b/test/serena/test_text_utils.py index 408aadac..bdfa6e76 100644 --- a/test/serena/test_text_utils.py +++ b/test/serena/test_text_utils.py @@ -1,4 +1,3 @@ -import re from collections.abc import Callable import pytest @@ -49,7 +48,7 @@ class TestSearchText: assert "def process" in matches[1].lines[0].line_content assert "def filter" in matches[2].lines[0].line_content - def test_search_text_with_compiled_regex(self): + def test_search_text_with_regex_pattern2(self): """Test searching with a pre-compiled regex pattern.""" content = """ import os @@ -66,8 +65,8 @@ class TestSearchText: """ # Search for variable assignments with a compiled regex - pattern = re.compile(r"^\s*[A-Z_]+ = .+$") - matches = search_text(pattern, content=content) + pattern = r"^\s*[A-Z_]+ = .*?$" + matches = search_text(pattern, content=content, multiline=True) assert len(matches) == 2 assert "DEBUG = True" in matches[0].lines[0].line_content @@ -117,7 +116,7 @@ class TestSearchText: # Search for a pattern that spans multiple lines (if-else block) pattern = r"if.*?else.*?return" - matches = search_text(pattern, content=content, allow_multiline_match=True) + matches = search_text(pattern, content=content) assert len(matches) == 1 multiline_match = matches[0] @@ -145,7 +144,7 @@ class TestSearchText: """ # Search with a glob pattern for all user methods - matches = search_text("*_user*", content=content, is_glob=True) + matches = search_text("*_user*", content=content, is_glob=True, multiline=False) assert len(matches) == 3 assert "get_user" in matches[0].lines[0].line_content @@ -170,7 +169,7 @@ class TestSearchText: """ # Search with a simplified glob pattern to find all isinstance occurrences - matches = search_text("*isinstance*", content=content, is_glob=True) + matches = search_text("*isinstance*", content=content, is_glob=True, multiline=False) # Should match lines with isinstance(item, dict) and isinstance(item, list) assert len(matches) >= 2 @@ -194,11 +193,11 @@ class TestSearchText: print("value{bar}") """ - matches_square = search_text(r"*\[42\]*", content=content, is_glob=True) + matches_square = search_text(r"*\[42\]*", content=content, is_glob=True, multiline=False) assert len(matches_square) == 1 assert "[42]" in matches_square[0].lines[0].line_content - matches_curly = search_text("*{bar}*", content=content, is_glob=True) + matches_curly = search_text("*{bar}*", content=content, is_glob=True, multiline=False) assert len(matches_curly) == 1 assert "{bar}" in matches_curly[0].lines[0].line_content