aurora-lsp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aurora_lsp/__init__.py +28 -0
- aurora_lsp/analysis.py +485 -0
- aurora_lsp/client.py +305 -0
- aurora_lsp/diagnostics.py +207 -0
- aurora_lsp/facade.py +402 -0
- aurora_lsp/filters.py +195 -0
- aurora_lsp-0.1.0.dist-info/METADATA +195 -0
- aurora_lsp-0.1.0.dist-info/RECORD +9 -0
- aurora_lsp-0.1.0.dist-info/WHEEL +4 -0
aurora_lsp/facade.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""AuroraLSP - High-level facade for LSP integration.
|
|
2
|
+
|
|
3
|
+
Provides a simple, synchronous API for Aurora CLI and MCP.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from aurora_lsp.analysis import CodeAnalyzer
|
|
14
|
+
from aurora_lsp.client import AuroraLSPClient
|
|
15
|
+
from aurora_lsp.diagnostics import DiagnosticsFormatter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuroraLSP:
|
|
22
|
+
"""High-level LSP interface for Aurora.
|
|
23
|
+
|
|
24
|
+
Provides synchronous methods that wrap async LSP operations.
|
|
25
|
+
Designed for use in CLI commands and MCP tools.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
lsp = AuroraLSP("/path/to/workspace")
|
|
29
|
+
|
|
30
|
+
# Find usages (excluding imports)
|
|
31
|
+
result = lsp.find_usages("src/main.py", 10, 5)
|
|
32
|
+
print(f"Found {result['total_usages']} usages")
|
|
33
|
+
|
|
34
|
+
# Find dead code
|
|
35
|
+
dead = lsp.find_dead_code()
|
|
36
|
+
for item in dead:
|
|
37
|
+
print(f"Unused: {item['name']} in {item['file']}")
|
|
38
|
+
|
|
39
|
+
# Get linting diagnostics
|
|
40
|
+
diags = lsp.lint("src/")
|
|
41
|
+
print(f"Found {diags['total_errors']} errors")
|
|
42
|
+
|
|
43
|
+
lsp.close()
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, workspace: str | Path | None = None):
|
|
47
|
+
"""Initialize AuroraLSP.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
workspace: Workspace root directory. Defaults to current directory.
|
|
51
|
+
"""
|
|
52
|
+
self.workspace = Path(workspace or Path.cwd()).resolve()
|
|
53
|
+
self._client: AuroraLSPClient | None = None
|
|
54
|
+
self._analyzer: CodeAnalyzer | None = None
|
|
55
|
+
self._diagnostics: DiagnosticsFormatter | None = None
|
|
56
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def client(self) -> AuroraLSPClient:
|
|
60
|
+
"""Get or create LSP client."""
|
|
61
|
+
if self._client is None:
|
|
62
|
+
self._client = AuroraLSPClient(self.workspace)
|
|
63
|
+
return self._client
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def analyzer(self) -> CodeAnalyzer:
|
|
67
|
+
"""Get or create code analyzer."""
|
|
68
|
+
if self._analyzer is None:
|
|
69
|
+
self._analyzer = CodeAnalyzer(self.client, self.workspace)
|
|
70
|
+
return self._analyzer
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def diagnostics(self) -> DiagnosticsFormatter:
|
|
74
|
+
"""Get or create diagnostics formatter."""
|
|
75
|
+
if self._diagnostics is None:
|
|
76
|
+
self._diagnostics = DiagnosticsFormatter(self.client, self.workspace)
|
|
77
|
+
return self._diagnostics
|
|
78
|
+
|
|
79
|
+
def _run_async(self, coro) -> Any:
|
|
80
|
+
"""Run an async coroutine synchronously."""
|
|
81
|
+
try:
|
|
82
|
+
loop = asyncio.get_event_loop()
|
|
83
|
+
except RuntimeError:
|
|
84
|
+
loop = asyncio.new_event_loop()
|
|
85
|
+
asyncio.set_event_loop(loop)
|
|
86
|
+
|
|
87
|
+
return loop.run_until_complete(coro)
|
|
88
|
+
|
|
89
|
+
# =========================================================================
|
|
90
|
+
# VITAL: Import vs Usage
|
|
91
|
+
# =========================================================================
|
|
92
|
+
|
|
93
|
+
def find_usages(
|
|
94
|
+
self,
|
|
95
|
+
file_path: str | Path,
|
|
96
|
+
line: int,
|
|
97
|
+
col: int = 0,
|
|
98
|
+
include_imports: bool = False,
|
|
99
|
+
) -> dict:
|
|
100
|
+
"""Find usages of a symbol (excluding imports by default).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
file_path: Path to file containing the symbol.
|
|
104
|
+
line: Line number (0-indexed).
|
|
105
|
+
col: Column number (0-indexed).
|
|
106
|
+
include_imports: Whether to include import statements.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dict with:
|
|
110
|
+
- usages: List of usage locations with context
|
|
111
|
+
- imports: List of import locations with context
|
|
112
|
+
- total_usages: Count of actual usages
|
|
113
|
+
- total_imports: Count of import statements
|
|
114
|
+
"""
|
|
115
|
+
return self._run_async(
|
|
116
|
+
self.analyzer.find_usages(file_path, line, col, include_imports)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_usage_summary(
|
|
120
|
+
self,
|
|
121
|
+
file_path: str | Path,
|
|
122
|
+
line: int,
|
|
123
|
+
col: int = 0,
|
|
124
|
+
symbol_name: str | None = None,
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Get comprehensive usage summary for a symbol.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
file_path: Path to file containing the symbol.
|
|
130
|
+
line: Line number (0-indexed).
|
|
131
|
+
col: Column number (0-indexed).
|
|
132
|
+
symbol_name: Optional symbol name for display.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dict with:
|
|
136
|
+
- symbol: Symbol name
|
|
137
|
+
- total_usages: Count of actual usages
|
|
138
|
+
- total_imports: Count of imports
|
|
139
|
+
- impact: 'low', 'medium', or 'high'
|
|
140
|
+
- files_affected: Number of files with usages
|
|
141
|
+
- usages_by_file: Usages grouped by file
|
|
142
|
+
- usages: Top 20 usage locations
|
|
143
|
+
- imports: All import locations
|
|
144
|
+
"""
|
|
145
|
+
return self._run_async(
|
|
146
|
+
self.analyzer.get_usage_summary(file_path, line, col, symbol_name)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# =========================================================================
|
|
150
|
+
# IMPORTANT: Dead Code Detection
|
|
151
|
+
# =========================================================================
|
|
152
|
+
|
|
153
|
+
def find_dead_code(
|
|
154
|
+
self,
|
|
155
|
+
path: str | Path | None = None,
|
|
156
|
+
include_private: bool = False,
|
|
157
|
+
) -> list[dict]:
|
|
158
|
+
"""Find functions/classes with 0 usages.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
path: Directory or file to analyze. Defaults to workspace.
|
|
162
|
+
include_private: Whether to include private symbols (_name).
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of dead code items, each with:
|
|
166
|
+
- file: File path
|
|
167
|
+
- line: Line number
|
|
168
|
+
- name: Symbol name
|
|
169
|
+
- kind: 'function', 'class', or 'method'
|
|
170
|
+
- imports: Number of times imported (but never used)
|
|
171
|
+
"""
|
|
172
|
+
return self._run_async(
|
|
173
|
+
self.analyzer.find_dead_code(path, include_private)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# =========================================================================
|
|
177
|
+
# IMPORTANT: Linting
|
|
178
|
+
# =========================================================================
|
|
179
|
+
|
|
180
|
+
def lint(
|
|
181
|
+
self,
|
|
182
|
+
path: str | Path | None = None,
|
|
183
|
+
severity_filter: int | None = None,
|
|
184
|
+
) -> dict:
|
|
185
|
+
"""Get linting diagnostics for file(s).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
path: File or directory to lint. Defaults to workspace.
|
|
189
|
+
severity_filter: Minimum severity (1=error, 2=warning, etc.).
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dict with:
|
|
193
|
+
- errors: List of errors
|
|
194
|
+
- warnings: List of warnings
|
|
195
|
+
- hints: List of hints
|
|
196
|
+
- total_errors: Error count
|
|
197
|
+
- total_warnings: Warning count
|
|
198
|
+
- total_hints: Hint count
|
|
199
|
+
"""
|
|
200
|
+
return self._run_async(
|
|
201
|
+
self.diagnostics.get_all_diagnostics(path, severity_filter)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def lint_file(self, file_path: str | Path) -> dict:
|
|
205
|
+
"""Get linting diagnostics for a single file.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
file_path: Path to file.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dict with errors, warnings, hints lists.
|
|
212
|
+
"""
|
|
213
|
+
return self._run_async(
|
|
214
|
+
self.diagnostics.get_file_diagnostics(file_path)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# =========================================================================
|
|
218
|
+
# OPTIONAL: Call Hierarchy (callers/callees)
|
|
219
|
+
# =========================================================================
|
|
220
|
+
|
|
221
|
+
def get_callers(
|
|
222
|
+
self,
|
|
223
|
+
file_path: str | Path,
|
|
224
|
+
line: int,
|
|
225
|
+
col: int = 0,
|
|
226
|
+
) -> list[dict]:
|
|
227
|
+
"""Find functions that call this symbol.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
file_path: Path to file containing the symbol.
|
|
231
|
+
line: Line number (0-indexed).
|
|
232
|
+
col: Column number (0-indexed).
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of caller functions, each with:
|
|
236
|
+
- file: File path
|
|
237
|
+
- line: Line number
|
|
238
|
+
- name: Function name
|
|
239
|
+
- kind: 'function' or 'method'
|
|
240
|
+
"""
|
|
241
|
+
return self._run_async(
|
|
242
|
+
self.analyzer.get_callers(file_path, line, col)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def get_callees(
|
|
246
|
+
self,
|
|
247
|
+
file_path: str | Path,
|
|
248
|
+
line: int,
|
|
249
|
+
col: int = 0,
|
|
250
|
+
) -> list[dict]:
|
|
251
|
+
"""Find functions called by this symbol.
|
|
252
|
+
|
|
253
|
+
Note: Limited support. May return empty list.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
file_path: Path to file containing the symbol.
|
|
257
|
+
line: Line number (0-indexed).
|
|
258
|
+
col: Column number (0-indexed).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of called functions (may be empty).
|
|
262
|
+
"""
|
|
263
|
+
return self._run_async(
|
|
264
|
+
self.analyzer.get_callees(file_path, line, col)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# =========================================================================
|
|
268
|
+
# Utility Methods
|
|
269
|
+
# =========================================================================
|
|
270
|
+
|
|
271
|
+
def find_symbol(self, name: str, path: str | Path | None = None) -> dict | None:
|
|
272
|
+
"""Find a symbol by name in the workspace.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
name: Symbol name to find.
|
|
276
|
+
path: Limit search to this path.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict with file, line, col if found, or None.
|
|
280
|
+
"""
|
|
281
|
+
# Get all source files
|
|
282
|
+
target = Path(path) if path else self.workspace
|
|
283
|
+
if target.is_file():
|
|
284
|
+
files = [target]
|
|
285
|
+
else:
|
|
286
|
+
extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java"}
|
|
287
|
+
files = []
|
|
288
|
+
for ext in extensions:
|
|
289
|
+
files.extend(target.rglob(f"*{ext}"))
|
|
290
|
+
|
|
291
|
+
# Search for symbol
|
|
292
|
+
for file_path in files:
|
|
293
|
+
try:
|
|
294
|
+
symbols = self._run_async(
|
|
295
|
+
self.client.request_document_symbols(file_path)
|
|
296
|
+
)
|
|
297
|
+
for symbol in self._flatten_symbols(symbols or []):
|
|
298
|
+
if symbol.get("name") == name:
|
|
299
|
+
range_info = symbol.get("range", {})
|
|
300
|
+
start = range_info.get("start", {})
|
|
301
|
+
return {
|
|
302
|
+
"file": str(file_path),
|
|
303
|
+
"line": start.get("line", 0),
|
|
304
|
+
"col": start.get("character", 0),
|
|
305
|
+
"kind": symbol.get("kind"),
|
|
306
|
+
}
|
|
307
|
+
except Exception:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _flatten_symbols(self, symbols: list) -> list[dict]:
|
|
313
|
+
"""Flatten nested symbol tree."""
|
|
314
|
+
result = []
|
|
315
|
+
for s in symbols:
|
|
316
|
+
result.append(s)
|
|
317
|
+
children = s.get("children", [])
|
|
318
|
+
if children:
|
|
319
|
+
result.extend(self._flatten_symbols(children))
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
def close(self) -> None:
|
|
323
|
+
"""Close all language server connections."""
|
|
324
|
+
if self._client:
|
|
325
|
+
self._run_async(self._client.close())
|
|
326
|
+
self._client = None
|
|
327
|
+
self._analyzer = None
|
|
328
|
+
self._diagnostics = None
|
|
329
|
+
|
|
330
|
+
def __enter__(self):
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
334
|
+
self.close()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =========================================================================
|
|
338
|
+
# Convenience Functions
|
|
339
|
+
# =========================================================================
|
|
340
|
+
|
|
341
|
+
def find_usages(
|
|
342
|
+
file_path: str | Path,
|
|
343
|
+
line: int,
|
|
344
|
+
col: int = 0,
|
|
345
|
+
workspace: str | Path | None = None,
|
|
346
|
+
) -> dict:
|
|
347
|
+
"""Find usages of a symbol (excluding imports).
|
|
348
|
+
|
|
349
|
+
Convenience function that creates a temporary AuroraLSP instance.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
file_path: Path to file containing the symbol.
|
|
353
|
+
line: Line number (0-indexed).
|
|
354
|
+
col: Column number (0-indexed).
|
|
355
|
+
workspace: Workspace root. Defaults to file's parent.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Dict with usages, imports, and counts.
|
|
359
|
+
"""
|
|
360
|
+
ws = workspace or Path(file_path).parent
|
|
361
|
+
with AuroraLSP(ws) as lsp:
|
|
362
|
+
return lsp.find_usages(file_path, line, col)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def find_dead_code(
|
|
366
|
+
path: str | Path | None = None,
|
|
367
|
+
workspace: str | Path | None = None,
|
|
368
|
+
) -> list[dict]:
|
|
369
|
+
"""Find dead code in a directory.
|
|
370
|
+
|
|
371
|
+
Convenience function that creates a temporary AuroraLSP instance.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
path: Directory to analyze.
|
|
375
|
+
workspace: Workspace root. Defaults to path.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of dead code items.
|
|
379
|
+
"""
|
|
380
|
+
ws = workspace or path or Path.cwd()
|
|
381
|
+
with AuroraLSP(ws) as lsp:
|
|
382
|
+
return lsp.find_dead_code(path)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def lint(
|
|
386
|
+
path: str | Path | None = None,
|
|
387
|
+
workspace: str | Path | None = None,
|
|
388
|
+
) -> dict:
|
|
389
|
+
"""Lint a directory.
|
|
390
|
+
|
|
391
|
+
Convenience function that creates a temporary AuroraLSP instance.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
path: Directory to lint.
|
|
395
|
+
workspace: Workspace root. Defaults to path.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dict with errors, warnings, hints.
|
|
399
|
+
"""
|
|
400
|
+
ws = workspace or path or Path.cwd()
|
|
401
|
+
with AuroraLSP(ws) as lsp:
|
|
402
|
+
return lsp.lint(path)
|
aurora_lsp/filters.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Import filtering for LSP references.
|
|
2
|
+
|
|
3
|
+
Distinguishes actual usages from import statements across multiple languages.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Awaitable, Callable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImportFilter:
|
|
14
|
+
"""Filter import statements from LSP references.
|
|
15
|
+
|
|
16
|
+
LSP returns ALL references including imports. This class distinguishes
|
|
17
|
+
actual usages from import statements using language-specific patterns.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Language-specific import statement patterns
|
|
21
|
+
IMPORT_PATTERNS: dict[str, list[str]] = {
|
|
22
|
+
"python": [
|
|
23
|
+
r"^\s*import\s+",
|
|
24
|
+
r"^\s*from\s+[\w.]+\s+import\s+",
|
|
25
|
+
],
|
|
26
|
+
"javascript": [
|
|
27
|
+
r"^\s*import\s+",
|
|
28
|
+
r"^\s*import\s*\{",
|
|
29
|
+
r"^\s*import\s+\*\s+as\s+",
|
|
30
|
+
r"^\s*(const|let|var)\s+.*\s*=\s*require\s*\(",
|
|
31
|
+
],
|
|
32
|
+
"typescript": [
|
|
33
|
+
r"^\s*import\s+",
|
|
34
|
+
r"^\s*import\s*\{",
|
|
35
|
+
r"^\s*import\s+\*\s+as\s+",
|
|
36
|
+
r"^\s*import\s+type\s+",
|
|
37
|
+
r"^\s*(const|let|var)\s+.*\s*=\s*require\s*\(",
|
|
38
|
+
],
|
|
39
|
+
"go": [
|
|
40
|
+
r"^\s*import\s+",
|
|
41
|
+
r'^\s*import\s*\(',
|
|
42
|
+
r'^\s*"[\w/.-]+"', # Inside import block
|
|
43
|
+
],
|
|
44
|
+
"rust": [
|
|
45
|
+
r"^\s*use\s+",
|
|
46
|
+
r"^\s*extern\s+crate\s+",
|
|
47
|
+
],
|
|
48
|
+
"java": [
|
|
49
|
+
r"^\s*import\s+",
|
|
50
|
+
r"^\s*import\s+static\s+",
|
|
51
|
+
],
|
|
52
|
+
"ruby": [
|
|
53
|
+
r"^\s*require\s+",
|
|
54
|
+
r"^\s*require_relative\s+",
|
|
55
|
+
r"^\s*load\s+",
|
|
56
|
+
r"^\s*autoload\s+",
|
|
57
|
+
],
|
|
58
|
+
"csharp": [
|
|
59
|
+
r"^\s*using\s+",
|
|
60
|
+
r"^\s*using\s+static\s+",
|
|
61
|
+
],
|
|
62
|
+
"dart": [
|
|
63
|
+
r"^\s*import\s+",
|
|
64
|
+
r"^\s*export\s+",
|
|
65
|
+
r"^\s*part\s+",
|
|
66
|
+
],
|
|
67
|
+
"kotlin": [
|
|
68
|
+
r"^\s*import\s+",
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def __init__(self, language: str):
|
|
73
|
+
"""Initialize filter for a specific language.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
language: Language identifier (e.g., 'python', 'typescript').
|
|
77
|
+
"""
|
|
78
|
+
self.language = language
|
|
79
|
+
patterns = self.IMPORT_PATTERNS.get(language, [])
|
|
80
|
+
self.patterns = [re.compile(p) for p in patterns]
|
|
81
|
+
|
|
82
|
+
def is_import_line(self, line_content: str) -> bool:
|
|
83
|
+
"""Check if a line is an import statement.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
line_content: The source code line to check.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if the line is an import statement.
|
|
90
|
+
"""
|
|
91
|
+
for pattern in self.patterns:
|
|
92
|
+
if pattern.match(line_content):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
async def filter_references(
|
|
97
|
+
self,
|
|
98
|
+
refs: list[dict],
|
|
99
|
+
file_reader: Callable[[str, int], Awaitable[str]],
|
|
100
|
+
) -> tuple[list[dict], list[dict]]:
|
|
101
|
+
"""Split references into usages and imports.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
refs: List of reference locations from LSP.
|
|
105
|
+
file_reader: Async function to read a line from a file.
|
|
106
|
+
Signature: (file_path, line_number) -> line_content
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (usages, imports) where each is a list of references.
|
|
110
|
+
"""
|
|
111
|
+
usages = []
|
|
112
|
+
imports = []
|
|
113
|
+
|
|
114
|
+
for ref in refs:
|
|
115
|
+
file_path = ref.get("file", "")
|
|
116
|
+
line_num = ref.get("line", 0)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
line_content = await file_reader(file_path, line_num)
|
|
120
|
+
ref_with_context = {**ref, "context": line_content.strip()}
|
|
121
|
+
|
|
122
|
+
if self.is_import_line(line_content):
|
|
123
|
+
imports.append(ref_with_context)
|
|
124
|
+
else:
|
|
125
|
+
usages.append(ref_with_context)
|
|
126
|
+
except Exception:
|
|
127
|
+
# If we can't read the line, assume it's a usage
|
|
128
|
+
usages.append(ref)
|
|
129
|
+
|
|
130
|
+
return usages, imports
|
|
131
|
+
|
|
132
|
+
def filter_references_sync(
|
|
133
|
+
self,
|
|
134
|
+
refs: list[dict],
|
|
135
|
+
file_reader: Callable[[str, int], str],
|
|
136
|
+
) -> tuple[list[dict], list[dict]]:
|
|
137
|
+
"""Synchronous version of filter_references.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
refs: List of reference locations from LSP.
|
|
141
|
+
file_reader: Sync function to read a line from a file.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Tuple of (usages, imports).
|
|
145
|
+
"""
|
|
146
|
+
usages = []
|
|
147
|
+
imports = []
|
|
148
|
+
|
|
149
|
+
for ref in refs:
|
|
150
|
+
file_path = ref.get("file", "")
|
|
151
|
+
line_num = ref.get("line", 0)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
line_content = file_reader(file_path, line_num)
|
|
155
|
+
ref_with_context = {**ref, "context": line_content.strip()}
|
|
156
|
+
|
|
157
|
+
if self.is_import_line(line_content):
|
|
158
|
+
imports.append(ref_with_context)
|
|
159
|
+
else:
|
|
160
|
+
usages.append(ref_with_context)
|
|
161
|
+
except Exception:
|
|
162
|
+
usages.append(ref)
|
|
163
|
+
|
|
164
|
+
return usages, imports
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_filter_for_file(file_path: str | Path) -> ImportFilter:
|
|
168
|
+
"""Get appropriate ImportFilter for a file based on extension.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
file_path: Path to file.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ImportFilter for the file's language.
|
|
175
|
+
"""
|
|
176
|
+
ext_to_lang = {
|
|
177
|
+
".py": "python",
|
|
178
|
+
".pyi": "python",
|
|
179
|
+
".js": "javascript",
|
|
180
|
+
".jsx": "javascript",
|
|
181
|
+
".ts": "typescript",
|
|
182
|
+
".tsx": "typescript",
|
|
183
|
+
".go": "go",
|
|
184
|
+
".rs": "rust",
|
|
185
|
+
".java": "java",
|
|
186
|
+
".rb": "ruby",
|
|
187
|
+
".cs": "csharp",
|
|
188
|
+
".dart": "dart",
|
|
189
|
+
".kt": "kotlin",
|
|
190
|
+
".kts": "kotlin",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ext = Path(file_path).suffix.lower()
|
|
194
|
+
lang = ext_to_lang.get(ext, "python") # Default to Python patterns
|
|
195
|
+
return ImportFilter(lang)
|