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/client.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Low-level multilspy wrapper for Aurora.
|
|
2
|
+
|
|
3
|
+
Manages language server instances and provides async LSP operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from multilspy.language_server import LanguageServer
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Import multilspy components
|
|
20
|
+
try:
|
|
21
|
+
from multilspy.language_server import LanguageServer
|
|
22
|
+
from multilspy.multilspy_config import Language, MultilspyConfig
|
|
23
|
+
from multilspy.multilspy_logger import MultilspyLogger
|
|
24
|
+
MULTILSPY_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
MULTILSPY_AVAILABLE = False
|
|
27
|
+
Language = None # type: ignore
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuroraLSPClient:
|
|
31
|
+
"""Low-level multilspy wrapper.
|
|
32
|
+
|
|
33
|
+
Manages language server instances per language, lazily initialized.
|
|
34
|
+
Provides async methods for common LSP operations.
|
|
35
|
+
|
|
36
|
+
Must be used within the server context:
|
|
37
|
+
|
|
38
|
+
client = AuroraLSPClient(workspace)
|
|
39
|
+
async with client.start():
|
|
40
|
+
refs = await client.request_references(file, line, col)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Map file extensions to Language enum values
|
|
44
|
+
LANGUAGE_MAP: dict[str, Any] = {}
|
|
45
|
+
|
|
46
|
+
def __init__(self, workspace: Path | str):
|
|
47
|
+
"""Initialize LSP client for a workspace.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
workspace: Root directory of the project to analyze.
|
|
51
|
+
"""
|
|
52
|
+
self.workspace = Path(workspace).resolve()
|
|
53
|
+
self._servers: dict[str, Any] = {} # Language -> server
|
|
54
|
+
self._contexts: dict[str, Any] = {} # Language -> context manager
|
|
55
|
+
self._open_files: set[str] = set() # Tracks opened files
|
|
56
|
+
self._lock = asyncio.Lock()
|
|
57
|
+
self._logger: Any = None
|
|
58
|
+
self._started = False
|
|
59
|
+
|
|
60
|
+
# Initialize language map if multilspy is available
|
|
61
|
+
if MULTILSPY_AVAILABLE and Language is not None:
|
|
62
|
+
self.LANGUAGE_MAP = {
|
|
63
|
+
".py": Language.PYTHON,
|
|
64
|
+
".pyi": Language.PYTHON,
|
|
65
|
+
".rs": Language.RUST,
|
|
66
|
+
".go": Language.GO,
|
|
67
|
+
".js": Language.JAVASCRIPT,
|
|
68
|
+
".jsx": Language.JAVASCRIPT,
|
|
69
|
+
".ts": Language.TYPESCRIPT,
|
|
70
|
+
".tsx": Language.TYPESCRIPT,
|
|
71
|
+
".java": Language.JAVA,
|
|
72
|
+
".rb": Language.RUBY,
|
|
73
|
+
".cs": Language.CSHARP,
|
|
74
|
+
".dart": Language.DART,
|
|
75
|
+
".kt": Language.KOTLIN,
|
|
76
|
+
".kts": Language.KOTLIN,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def get_language(self, file_path: str | Path) -> Any:
|
|
80
|
+
"""Get language enum value for a file."""
|
|
81
|
+
ext = Path(file_path).suffix.lower()
|
|
82
|
+
return self.LANGUAGE_MAP.get(ext)
|
|
83
|
+
|
|
84
|
+
async def _ensure_server(self, file_path: str | Path) -> Any:
|
|
85
|
+
"""Ensure server is started for file's language.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_path: Path to file (used to determine language).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Started LanguageServer instance.
|
|
92
|
+
"""
|
|
93
|
+
if not MULTILSPY_AVAILABLE:
|
|
94
|
+
raise ImportError("multilspy not installed. Install with: pip install multilspy")
|
|
95
|
+
|
|
96
|
+
lang = self.get_language(file_path)
|
|
97
|
+
if not lang:
|
|
98
|
+
raise ValueError(f"Unsupported file type: {Path(file_path).suffix}")
|
|
99
|
+
|
|
100
|
+
lang_key = lang.name if hasattr(lang, 'name') else str(lang)
|
|
101
|
+
|
|
102
|
+
async with self._lock:
|
|
103
|
+
if lang_key not in self._servers:
|
|
104
|
+
# Create logger if not exists
|
|
105
|
+
if self._logger is None:
|
|
106
|
+
self._logger = MultilspyLogger()
|
|
107
|
+
|
|
108
|
+
# Create config
|
|
109
|
+
config = MultilspyConfig(code_language=lang)
|
|
110
|
+
|
|
111
|
+
logger.info(f"Starting {lang_key} language server for {self.workspace}")
|
|
112
|
+
|
|
113
|
+
# Create server (sync)
|
|
114
|
+
server = LanguageServer.create(config, self._logger, str(self.workspace))
|
|
115
|
+
|
|
116
|
+
# Start server (async context manager)
|
|
117
|
+
ctx = server.start_server()
|
|
118
|
+
await ctx.__aenter__()
|
|
119
|
+
|
|
120
|
+
self._servers[lang_key] = server
|
|
121
|
+
self._contexts[lang_key] = ctx
|
|
122
|
+
|
|
123
|
+
return self._servers[lang_key]
|
|
124
|
+
|
|
125
|
+
def _ensure_file_open(self, server: Any, file_path: str) -> None:
|
|
126
|
+
"""Ensure file is opened in the server."""
|
|
127
|
+
rel_path = self._to_relative(file_path)
|
|
128
|
+
if rel_path not in self._open_files:
|
|
129
|
+
server.open_file(rel_path)
|
|
130
|
+
self._open_files.add(rel_path)
|
|
131
|
+
|
|
132
|
+
async def request_references(
|
|
133
|
+
self,
|
|
134
|
+
file_path: str | Path,
|
|
135
|
+
line: int,
|
|
136
|
+
col: int,
|
|
137
|
+
) -> list[dict]:
|
|
138
|
+
"""Find all references to a symbol.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
file_path: Path to file containing the symbol.
|
|
142
|
+
line: Line number (0-indexed).
|
|
143
|
+
col: Column number (0-indexed).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of reference locations, each with 'file', 'line', 'col' keys.
|
|
147
|
+
"""
|
|
148
|
+
server = await self._ensure_server(file_path)
|
|
149
|
+
rel_path = self._to_relative(file_path)
|
|
150
|
+
self._ensure_file_open(server, file_path)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
refs = await server.request_references(rel_path, line, col)
|
|
154
|
+
return self._normalize_locations(refs)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.warning(f"request_references failed: {e}")
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
async def request_definition(
|
|
160
|
+
self,
|
|
161
|
+
file_path: str | Path,
|
|
162
|
+
line: int,
|
|
163
|
+
col: int,
|
|
164
|
+
) -> list[dict]:
|
|
165
|
+
"""Find definition of a symbol."""
|
|
166
|
+
server = await self._ensure_server(file_path)
|
|
167
|
+
rel_path = self._to_relative(file_path)
|
|
168
|
+
self._ensure_file_open(server, file_path)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
defs = await server.request_definition(rel_path, line, col)
|
|
172
|
+
return self._normalize_locations(defs)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.warning(f"request_definition failed: {e}")
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
async def request_document_symbols(
|
|
178
|
+
self,
|
|
179
|
+
file_path: str | Path,
|
|
180
|
+
) -> list[dict]:
|
|
181
|
+
"""Get all symbols defined in a file."""
|
|
182
|
+
server = await self._ensure_server(file_path)
|
|
183
|
+
rel_path = self._to_relative(file_path)
|
|
184
|
+
self._ensure_file_open(server, file_path)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
result = await server.request_document_symbols(rel_path)
|
|
188
|
+
# multilspy returns tuple (symbols_list, extra_info) - extract first element
|
|
189
|
+
if isinstance(result, tuple):
|
|
190
|
+
symbols = result[0]
|
|
191
|
+
else:
|
|
192
|
+
symbols = result
|
|
193
|
+
return symbols or []
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning(f"request_document_symbols failed: {e}")
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
async def request_hover(
|
|
199
|
+
self,
|
|
200
|
+
file_path: str | Path,
|
|
201
|
+
line: int,
|
|
202
|
+
col: int,
|
|
203
|
+
) -> dict | None:
|
|
204
|
+
"""Get hover information for a symbol."""
|
|
205
|
+
server = await self._ensure_server(file_path)
|
|
206
|
+
rel_path = self._to_relative(file_path)
|
|
207
|
+
self._ensure_file_open(server, file_path)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
return await server.request_hover(rel_path, line, col)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"request_hover failed: {e}")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
async def request_diagnostics(
|
|
216
|
+
self,
|
|
217
|
+
file_path: str | Path,
|
|
218
|
+
) -> list[dict]:
|
|
219
|
+
"""Get diagnostics (errors, warnings) for a file.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
file_path: Path to file to check.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of diagnostic dicts with severity, message, range.
|
|
226
|
+
"""
|
|
227
|
+
server = await self._ensure_server(file_path)
|
|
228
|
+
rel_path = self._to_relative(file_path)
|
|
229
|
+
self._ensure_file_open(server, file_path)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# multilspy may return diagnostics via different methods
|
|
233
|
+
# Try the standard approach first
|
|
234
|
+
if hasattr(server, 'request_diagnostics'):
|
|
235
|
+
return await server.request_diagnostics(rel_path) or []
|
|
236
|
+
# Some servers publish diagnostics automatically after open_file
|
|
237
|
+
# Check if server has cached diagnostics
|
|
238
|
+
if hasattr(server, 'get_diagnostics'):
|
|
239
|
+
return server.get_diagnostics(rel_path) or []
|
|
240
|
+
# Fallback: diagnostics may be in server state
|
|
241
|
+
logger.debug(f"Diagnostics not directly supported for {file_path}")
|
|
242
|
+
return []
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning(f"request_diagnostics failed: {e}")
|
|
245
|
+
return []
|
|
246
|
+
|
|
247
|
+
async def close(self) -> None:
|
|
248
|
+
"""Close all language server connections."""
|
|
249
|
+
async with self._lock:
|
|
250
|
+
# Exit context managers
|
|
251
|
+
for lang_key, ctx in self._contexts.items():
|
|
252
|
+
try:
|
|
253
|
+
logger.info(f"Stopping {lang_key} language server")
|
|
254
|
+
await ctx.__aexit__(None, None, None)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning(f"Error stopping {lang_key} server: {e}")
|
|
257
|
+
|
|
258
|
+
self._servers.clear()
|
|
259
|
+
self._contexts.clear()
|
|
260
|
+
self._open_files.clear()
|
|
261
|
+
|
|
262
|
+
def _to_relative(self, file_path: str | Path) -> str:
|
|
263
|
+
"""Convert absolute path to workspace-relative path."""
|
|
264
|
+
path = Path(file_path)
|
|
265
|
+
if path.is_absolute():
|
|
266
|
+
try:
|
|
267
|
+
return str(path.relative_to(self.workspace))
|
|
268
|
+
except ValueError:
|
|
269
|
+
return str(path)
|
|
270
|
+
return str(path)
|
|
271
|
+
|
|
272
|
+
def _normalize_locations(self, locations: list | None) -> list[dict]:
|
|
273
|
+
"""Normalize LSP location responses to consistent format."""
|
|
274
|
+
if not locations:
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
result = []
|
|
278
|
+
for loc in locations:
|
|
279
|
+
if isinstance(loc, dict):
|
|
280
|
+
# Handle different LSP location formats
|
|
281
|
+
if "absolutePath" in loc:
|
|
282
|
+
file_path = loc["absolutePath"]
|
|
283
|
+
elif "uri" in loc:
|
|
284
|
+
file_path = loc["uri"].replace("file://", "")
|
|
285
|
+
elif "targetUri" in loc:
|
|
286
|
+
file_path = loc["targetUri"].replace("file://", "")
|
|
287
|
+
else:
|
|
288
|
+
file_path = loc.get("file", loc.get("relativePath", ""))
|
|
289
|
+
|
|
290
|
+
range_info = loc.get("range") or loc.get("targetRange", {})
|
|
291
|
+
start = range_info.get("start", {})
|
|
292
|
+
|
|
293
|
+
result.append({
|
|
294
|
+
"file": file_path,
|
|
295
|
+
"line": start.get("line", 0),
|
|
296
|
+
"col": start.get("character", 0),
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
async def __aenter__(self):
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
305
|
+
await self.close()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Diagnostics (linting) wrapper for LSP.
|
|
2
|
+
|
|
3
|
+
Formats LSP diagnostics into Aurora-friendly output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import IntEnum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from aurora_lsp.client import AuroraLSPClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DiagnosticSeverity(IntEnum):
|
|
18
|
+
"""LSP DiagnosticSeverity values."""
|
|
19
|
+
|
|
20
|
+
ERROR = 1
|
|
21
|
+
WARNING = 2
|
|
22
|
+
INFORMATION = 3
|
|
23
|
+
HINT = 4
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DiagnosticsFormatter:
|
|
27
|
+
"""Format and filter LSP diagnostics."""
|
|
28
|
+
|
|
29
|
+
SEVERITY_NAMES = {
|
|
30
|
+
DiagnosticSeverity.ERROR: "error",
|
|
31
|
+
DiagnosticSeverity.WARNING: "warning",
|
|
32
|
+
DiagnosticSeverity.INFORMATION: "info",
|
|
33
|
+
DiagnosticSeverity.HINT: "hint",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def __init__(self, client: AuroraLSPClient, workspace: Path | str):
|
|
37
|
+
"""Initialize diagnostics formatter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
client: LSP client for making requests.
|
|
41
|
+
workspace: Workspace root directory.
|
|
42
|
+
"""
|
|
43
|
+
self.client = client
|
|
44
|
+
self.workspace = Path(workspace).resolve()
|
|
45
|
+
|
|
46
|
+
async def get_file_diagnostics(self, file_path: str | Path) -> dict:
|
|
47
|
+
"""Get diagnostics for a single file.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
file_path: Path to file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dict with errors, warnings, hints lists.
|
|
54
|
+
"""
|
|
55
|
+
diags = await self.client.request_diagnostics(file_path)
|
|
56
|
+
return self._format_diagnostics(diags, file_path)
|
|
57
|
+
|
|
58
|
+
async def get_all_diagnostics(
|
|
59
|
+
self,
|
|
60
|
+
path: str | Path | None = None,
|
|
61
|
+
severity_filter: int | None = None,
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""Get diagnostics for all files in a directory.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: Directory to scan. Defaults to workspace.
|
|
67
|
+
severity_filter: Minimum severity (1=error, 2=warning, etc.).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Dict with errors, warnings, hints lists and summary.
|
|
71
|
+
"""
|
|
72
|
+
target = Path(path) if path else self.workspace
|
|
73
|
+
|
|
74
|
+
if target.is_file():
|
|
75
|
+
files = [target]
|
|
76
|
+
else:
|
|
77
|
+
# Get all source files
|
|
78
|
+
extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java"}
|
|
79
|
+
files = []
|
|
80
|
+
for ext in extensions:
|
|
81
|
+
files.extend(target.rglob(f"*{ext}"))
|
|
82
|
+
|
|
83
|
+
# Filter out common non-source directories
|
|
84
|
+
exclude_dirs = {"node_modules", ".git", "__pycache__", ".venv", "venv"}
|
|
85
|
+
files = [f for f in files if not any(d in f.parts for d in exclude_dirs)]
|
|
86
|
+
|
|
87
|
+
all_errors = []
|
|
88
|
+
all_warnings = []
|
|
89
|
+
all_hints = []
|
|
90
|
+
|
|
91
|
+
for file_path in sorted(files):
|
|
92
|
+
try:
|
|
93
|
+
diags = await self.client.request_diagnostics(file_path)
|
|
94
|
+
formatted = self._format_diagnostics(diags, file_path)
|
|
95
|
+
|
|
96
|
+
all_errors.extend(formatted["errors"])
|
|
97
|
+
all_warnings.extend(formatted["warnings"])
|
|
98
|
+
all_hints.extend(formatted["hints"])
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Apply severity filter
|
|
103
|
+
if severity_filter:
|
|
104
|
+
if severity_filter > DiagnosticSeverity.ERROR:
|
|
105
|
+
all_errors = []
|
|
106
|
+
if severity_filter > DiagnosticSeverity.WARNING:
|
|
107
|
+
all_warnings = []
|
|
108
|
+
if severity_filter > DiagnosticSeverity.INFORMATION:
|
|
109
|
+
all_hints = []
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"errors": all_errors,
|
|
113
|
+
"warnings": all_warnings,
|
|
114
|
+
"hints": all_hints,
|
|
115
|
+
"total_errors": len(all_errors),
|
|
116
|
+
"total_warnings": len(all_warnings),
|
|
117
|
+
"total_hints": len(all_hints),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def _format_diagnostics(
|
|
121
|
+
self,
|
|
122
|
+
diags: list[dict],
|
|
123
|
+
file_path: str | Path,
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""Format raw LSP diagnostics.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
diags: Raw diagnostics from LSP.
|
|
129
|
+
file_path: Source file path.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dict with errors, warnings, hints lists.
|
|
133
|
+
"""
|
|
134
|
+
errors = []
|
|
135
|
+
warnings = []
|
|
136
|
+
hints = []
|
|
137
|
+
|
|
138
|
+
# Make path relative for display
|
|
139
|
+
try:
|
|
140
|
+
rel_path = Path(file_path).relative_to(self.workspace)
|
|
141
|
+
except ValueError:
|
|
142
|
+
rel_path = Path(file_path)
|
|
143
|
+
|
|
144
|
+
for d in diags:
|
|
145
|
+
severity = d.get("severity", DiagnosticSeverity.HINT)
|
|
146
|
+
range_info = d.get("range", {})
|
|
147
|
+
start = range_info.get("start", {})
|
|
148
|
+
|
|
149
|
+
entry = {
|
|
150
|
+
"file": str(rel_path),
|
|
151
|
+
"line": start.get("line", 0) + 1, # Convert to 1-indexed
|
|
152
|
+
"col": start.get("character", 0) + 1,
|
|
153
|
+
"message": d.get("message", ""),
|
|
154
|
+
"code": d.get("code", ""),
|
|
155
|
+
"source": d.get("source", ""),
|
|
156
|
+
"severity": self.SEVERITY_NAMES.get(severity, "unknown"),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if severity == DiagnosticSeverity.ERROR:
|
|
160
|
+
errors.append(entry)
|
|
161
|
+
elif severity == DiagnosticSeverity.WARNING:
|
|
162
|
+
warnings.append(entry)
|
|
163
|
+
else:
|
|
164
|
+
hints.append(entry)
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"errors": errors,
|
|
168
|
+
"warnings": warnings,
|
|
169
|
+
"hints": hints,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def format_for_display(self, diagnostics: dict, max_items: int = 10) -> str:
|
|
173
|
+
"""Format diagnostics for CLI display.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
diagnostics: Diagnostics dict from get_all_diagnostics.
|
|
177
|
+
max_items: Maximum items per category to show.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Formatted string for display.
|
|
181
|
+
"""
|
|
182
|
+
lines = []
|
|
183
|
+
|
|
184
|
+
total_errors = diagnostics.get("total_errors", len(diagnostics.get("errors", [])))
|
|
185
|
+
total_warnings = diagnostics.get("total_warnings", len(diagnostics.get("warnings", [])))
|
|
186
|
+
|
|
187
|
+
lines.append(f"{total_errors} errors, {total_warnings} warnings")
|
|
188
|
+
lines.append("")
|
|
189
|
+
|
|
190
|
+
errors = diagnostics.get("errors", [])
|
|
191
|
+
if errors:
|
|
192
|
+
lines.append("Errors:")
|
|
193
|
+
for e in errors[:max_items]:
|
|
194
|
+
lines.append(f" {e['file']}:{e['line']} {e['message']}")
|
|
195
|
+
if len(errors) > max_items:
|
|
196
|
+
lines.append(f" ... ({len(errors) - max_items} more)")
|
|
197
|
+
lines.append("")
|
|
198
|
+
|
|
199
|
+
warnings = diagnostics.get("warnings", [])
|
|
200
|
+
if warnings:
|
|
201
|
+
lines.append("Warnings:")
|
|
202
|
+
for w in warnings[:max_items]:
|
|
203
|
+
lines.append(f" {w['file']}:{w['line']} {w['message']}")
|
|
204
|
+
if len(warnings) > max_items:
|
|
205
|
+
lines.append(f" ... ({len(warnings) - max_items} more)")
|
|
206
|
+
|
|
207
|
+
return "\n".join(lines)
|