Skip to content

Commit 2ab1986

Browse files
committed
refactor(lsp): extract pure text helpers from lsp_definition
Move the two stateless methods (find_symbol_in_text, extract_definition_block) into pure_auto_codeql/utils/_lsp_text_extraction.py as module functions. LSPDefinitionLookup keeps both as thin delegators, so the external find_symbol_in_text call and internal self.extract_definition_block calls are unchanged. Executable statements verified identical via AST (docstring indentation is the only whitespace delta). 97 passed / 2 skipped, ruff clean.
1 parent ba3cea3 commit 2ab1986

2 files changed

Lines changed: 135 additions & 82 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""LSP 定义查找的纯文本处理助手。
2+
3+
从 LSPDefinitionLookup 抽出的无状态函数(不依赖实例状态),
4+
便于独立测试与复用。
5+
"""
6+
7+
from pathlib import Path
8+
from typing import List, Optional, Tuple
9+
10+
11+
def find_symbol_in_text(text: str, symbol: str) -> Optional[Tuple[int, int]]:
12+
"""
13+
Find the first occurrence of a symbol in text.
14+
15+
Args:
16+
text: Source code text to search
17+
symbol: Symbol name to find
18+
19+
Returns:
20+
Tuple of (line_index, character_index) if found, None otherwise
21+
"""
22+
for line_index, line_text in enumerate(text.splitlines()):
23+
column = line_text.find(symbol)
24+
if column != -1:
25+
# Return position in middle of symbol for better LSP resolution
26+
return (line_index, column + len(symbol) // 2)
27+
return None
28+
29+
30+
def extract_definition_block(
31+
file_path: Path,
32+
start_line: int,
33+
start_char: int
34+
) -> Optional[List[Tuple[int, str]]]:
35+
"""
36+
Extract complete definition block with intelligent boundary detection.
37+
38+
This method handles:
39+
- Class definitions (with brace matching)
40+
- Method/predicate definitions (with brace matching)
41+
- Single-line definitions (no braces)
42+
- Multi-line definitions with proper indentation
43+
44+
Args:
45+
file_path: Path to source file
46+
start_line: 0-indexed starting line
47+
start_char: Starting character position
48+
49+
Returns:
50+
List of (line_number, line_text) tuples, or None if extraction fails
51+
"""
52+
try:
53+
file_text = file_path.read_text(encoding="utf-8", errors="replace")
54+
except Exception:
55+
return None
56+
57+
lines = file_text.splitlines()
58+
if start_line >= len(lines):
59+
return None
60+
61+
# First, try to find the start of the definition by looking backwards
62+
# for keywords like 'class', 'predicate', 'private', 'signature module', etc.
63+
def_start_line = start_line
64+
for i in range(start_line, max(0, start_line - 10), -1):
65+
line = lines[i].strip()
66+
# Look for definition keywords
67+
if any(keyword in line for keyword in ['signature module ', 'class ', 'predicate ', 'private ', 'override ', 'abstract ', 'final ']):
68+
def_start_line = i
69+
break
70+
# Stop if we hit a closing brace (previous definition)
71+
if line.startswith('}'):
72+
break
73+
74+
block_lines = []
75+
brace_balance = 0
76+
started = False
77+
found_opening_brace = False
78+
79+
line_idx = def_start_line
80+
search_char = start_char if line_idx == start_line else 0
81+
82+
# Get the initial indentation level
83+
initial_indent = len(lines[def_start_line]) - len(lines[def_start_line].lstrip())
84+
85+
while line_idx < len(lines):
86+
line_text = lines[line_idx]
87+
block_lines.append((line_idx + 1, line_text))
88+
89+
# Scan characters for brace matching
90+
for ch in line_text[search_char:]:
91+
if ch == '{':
92+
brace_balance += 1
93+
started = True
94+
found_opening_brace = True
95+
elif ch == '}':
96+
if started:
97+
brace_balance -= 1
98+
99+
if started and brace_balance == 0:
100+
# Found matching closing brace
101+
return block_lines
102+
103+
# If no braces found yet, check for single-line or indentation-based definitions
104+
if not found_opening_brace and line_idx > def_start_line:
105+
stripped = line_text.strip()
106+
current_indent = len(line_text) - len(line_text.lstrip())
107+
108+
# Stop if we hit another definition at the same or lower indentation
109+
if stripped and current_indent <= initial_indent:
110+
if any(keyword in stripped for keyword in ['signature module ', 'class ', 'predicate ', 'private ', 'override ']):
111+
# Remove the last line (it's a new definition)
112+
block_lines.pop()
113+
return block_lines if block_lines else None
114+
115+
# Stop if we've collected enough lines for a single-line definition
116+
if len(block_lines) > 1 and stripped.endswith(';'):
117+
return block_lines
118+
119+
line_idx += 1
120+
search_char = 0
121+
122+
# Safety limit: don't extract more than 200 lines
123+
if len(block_lines) > 200:
124+
break
125+
126+
# Return what we collected
127+
return block_lines if block_lines else None

pure_auto_codeql/utils/lsp_definition.py

Lines changed: 8 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
from pure_auto_codeql.paths import get_repo_root
1818
from pure_auto_codeql.tools.lsp_codeql import HotCodeQL, write_msg
19+
from pure_auto_codeql.utils._lsp_text_extraction import (
20+
extract_definition_block as _extract_definition_block,
21+
)
22+
from pure_auto_codeql.utils._lsp_text_extraction import (
23+
find_symbol_in_text as _find_symbol_in_text,
24+
)
1925

2026

2127
class LSPDefinitionLookup:
@@ -450,12 +456,7 @@ def find_symbol_in_text(self, text: str, symbol: str) -> Optional[Tuple[int, int
450456
Returns:
451457
Tuple of (line_index, character_index) if found, None otherwise
452458
"""
453-
for line_index, line_text in enumerate(text.splitlines()):
454-
column = line_text.find(symbol)
455-
if column != -1:
456-
# Return position in middle of symbol for better LSP resolution
457-
return (line_index, column + len(symbol) // 2)
458-
return None
459+
return _find_symbol_in_text(text, symbol)
459460

460461
def query_definition(
461462
self,
@@ -540,82 +541,7 @@ def extract_definition_block(
540541
Returns:
541542
List of (line_number, line_text) tuples, or None if extraction fails
542543
"""
543-
try:
544-
file_text = file_path.read_text(encoding="utf-8", errors="replace")
545-
except Exception:
546-
return None
547-
548-
lines = file_text.splitlines()
549-
if start_line >= len(lines):
550-
return None
551-
552-
# First, try to find the start of the definition by looking backwards
553-
# for keywords like 'class', 'predicate', 'private', 'signature module', etc.
554-
def_start_line = start_line
555-
for i in range(start_line, max(0, start_line - 10), -1):
556-
line = lines[i].strip()
557-
# Look for definition keywords
558-
if any(keyword in line for keyword in ['signature module ', 'class ', 'predicate ', 'private ', 'override ', 'abstract ', 'final ']):
559-
def_start_line = i
560-
break
561-
# Stop if we hit a closing brace (previous definition)
562-
if line.startswith('}'):
563-
break
564-
565-
block_lines = []
566-
brace_balance = 0
567-
started = False
568-
found_opening_brace = False
569-
570-
line_idx = def_start_line
571-
search_char = start_char if line_idx == start_line else 0
572-
573-
# Get the initial indentation level
574-
initial_indent = len(lines[def_start_line]) - len(lines[def_start_line].lstrip())
575-
576-
while line_idx < len(lines):
577-
line_text = lines[line_idx]
578-
block_lines.append((line_idx + 1, line_text))
579-
580-
# Scan characters for brace matching
581-
for ch in line_text[search_char:]:
582-
if ch == '{':
583-
brace_balance += 1
584-
started = True
585-
found_opening_brace = True
586-
elif ch == '}':
587-
if started:
588-
brace_balance -= 1
589-
590-
if started and brace_balance == 0:
591-
# Found matching closing brace
592-
return block_lines
593-
594-
# If no braces found yet, check for single-line or indentation-based definitions
595-
if not found_opening_brace and line_idx > def_start_line:
596-
stripped = line_text.strip()
597-
current_indent = len(line_text) - len(line_text.lstrip())
598-
599-
# Stop if we hit another definition at the same or lower indentation
600-
if stripped and current_indent <= initial_indent:
601-
if any(keyword in stripped for keyword in ['signature module ', 'class ', 'predicate ', 'private ', 'override ']):
602-
# Remove the last line (it's a new definition)
603-
block_lines.pop()
604-
return block_lines if block_lines else None
605-
606-
# Stop if we've collected enough lines for a single-line definition
607-
if len(block_lines) > 1 and stripped.endswith(';'):
608-
return block_lines
609-
610-
line_idx += 1
611-
search_char = 0
612-
613-
# Safety limit: don't extract more than 200 lines
614-
if len(block_lines) > 200:
615-
break
616-
617-
# Return what we collected
618-
return block_lines if block_lines else None
544+
return _extract_definition_block(file_path, start_line, start_char)
619545

620546
def get_function_definition(
621547
self,

0 commit comments

Comments
 (0)