lean-lsp-mcp 0.12.1__tar.gz → 0.13.0__tar.gz
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.
- {lean_lsp_mcp-0.12.1/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.13.0}/PKG-INFO +2 -2
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/pyproject.toml +2 -2
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/instructions.py +2 -2
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/server.py +14 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/utils.py +75 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0/src/lean_lsp_mcp.egg-info}/PKG-INFO +2 -2
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/requires.txt +1 -1
- lean_lsp_mcp-0.13.0/tests/test_diagnostic_line_range.py +319 -0
- lean_lsp_mcp-0.12.1/tests/test_diagnostic_line_range.py +0 -154
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/LICENSE +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/README.md +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/setup.cfg +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/client_utils.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_file_caching.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_project_tools.py +0 -0
- {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_search_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.5.
|
|
11
|
+
Requires-Dist: leanclient==0.5.2
|
|
12
12
|
Requires-Dist: mcp[cli]==1.21.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "Lean Theorem Prover MCP"
|
|
5
5
|
authors = [{name="Oliver Dressler", email="hey@oli.show"}]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"leanclient==0.5.
|
|
10
|
+
"leanclient==0.5.2",
|
|
11
11
|
"mcp[cli]==1.21.0",
|
|
12
12
|
"orjson>=3.11.1",
|
|
13
13
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
INSTRUCTIONS = """## General Rules
|
|
2
|
-
- All line and column numbers are 1-indexed
|
|
2
|
+
- All line and column numbers are 1-indexed.
|
|
3
3
|
- Always analyze/search context before each file edit.
|
|
4
4
|
- This MCP does NOT make permanent file changes. Use other tools for editing.
|
|
5
5
|
- Work iteratively: Small steps, intermediate sorries, frequent checks.
|
|
@@ -7,7 +7,7 @@ INSTRUCTIONS = """## General Rules
|
|
|
7
7
|
## Key Tools
|
|
8
8
|
- lean_local_search: Confirm declarations (theorems/lemmas/defs/etc.) exist. VERY USEFUL AND FAST!
|
|
9
9
|
- lean_goal: Check proof state. USE OFTEN!
|
|
10
|
-
- lean_diagnostic_messages: Understand
|
|
10
|
+
- lean_diagnostic_messages: Understand current proof situation (use declaration_name to focus on one proof).
|
|
11
11
|
- lean_hover_info: Documentation about terms and lean syntax.
|
|
12
12
|
- lean_leansearch: Search theorems using natural language or Lean terms.
|
|
13
13
|
- lean_loogle: Search definitions and theorems by name, type, or subexpression.
|
|
@@ -34,6 +34,7 @@ from lean_lsp_mcp.utils import (
|
|
|
34
34
|
format_diagnostics,
|
|
35
35
|
format_goal,
|
|
36
36
|
format_line,
|
|
37
|
+
get_declaration_range,
|
|
37
38
|
OptionalTokenVerifier,
|
|
38
39
|
)
|
|
39
40
|
|
|
@@ -238,6 +239,8 @@ async def lsp_build(
|
|
|
238
239
|
def file_contents(ctx: Context, file_path: str, annotate_lines: bool = True) -> str:
|
|
239
240
|
"""Get the text contents of a Lean file, optionally with line numbers.
|
|
240
241
|
|
|
242
|
+
Use sparingly (bloats context). Mainly when unsure about line numbers.
|
|
243
|
+
|
|
241
244
|
Args:
|
|
242
245
|
file_path (str): Abs path to Lean file
|
|
243
246
|
annotate_lines (bool, optional): Annotate lines with line numbers. Defaults to True.
|
|
@@ -273,6 +276,7 @@ def diagnostic_messages(
|
|
|
273
276
|
file_path: str,
|
|
274
277
|
start_line: Optional[int] = None,
|
|
275
278
|
end_line: Optional[int] = None,
|
|
279
|
+
declaration_name: Optional[str] = None,
|
|
276
280
|
) -> List[str] | str:
|
|
277
281
|
"""Get all diagnostic msgs (errors, warnings, infos) for a Lean file.
|
|
278
282
|
|
|
@@ -282,6 +286,9 @@ def diagnostic_messages(
|
|
|
282
286
|
file_path (str): Abs path to Lean file
|
|
283
287
|
start_line (int, optional): Start line (1-indexed). Filters from this line.
|
|
284
288
|
end_line (int, optional): End line (1-indexed). Filters to this line.
|
|
289
|
+
declaration_name (str, optional): Name of a specific theorem/lemma/definition.
|
|
290
|
+
If provided, only returns diagnostics within that declaration.
|
|
291
|
+
Takes precedence over start_line/end_line.
|
|
285
292
|
|
|
286
293
|
Returns:
|
|
287
294
|
List[str] | str: Diagnostic msgs or error msg
|
|
@@ -292,6 +299,13 @@ def diagnostic_messages(
|
|
|
292
299
|
|
|
293
300
|
client: LeanLSPClient = ctx.request_context.lifespan_context.client
|
|
294
301
|
|
|
302
|
+
# If declaration_name is provided, get its range and use that for filtering
|
|
303
|
+
if declaration_name:
|
|
304
|
+
decl_range = get_declaration_range(client, rel_path, declaration_name)
|
|
305
|
+
if decl_range is None:
|
|
306
|
+
return f"Declaration '{declaration_name}' not found in file. Check the name (case-sensitive) and try again."
|
|
307
|
+
start_line, end_line = decl_range
|
|
308
|
+
|
|
295
309
|
# Convert 1-indexed to 0-indexed for leanclient
|
|
296
310
|
start_line_0 = (start_line - 1) if start_line is not None else None
|
|
297
311
|
end_line_0 = (end_line - 1) if end_line is not None else None
|
|
@@ -259,3 +259,78 @@ def filter_diagnostics_by_position(
|
|
|
259
259
|
matches.append(diagnostic)
|
|
260
260
|
|
|
261
261
|
return matches
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def search_symbols(symbols: List[Dict], target_name: str) -> Dict | None:
|
|
265
|
+
"""Recursively search through symbols and their children.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
symbols: List of LSP document symbols
|
|
269
|
+
target_name: Name of the symbol to find (case-sensitive)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The matching symbol dict, or None if not found
|
|
273
|
+
"""
|
|
274
|
+
for symbol in symbols:
|
|
275
|
+
if symbol.get("name") == target_name:
|
|
276
|
+
return symbol
|
|
277
|
+
# Search nested declarations (children)
|
|
278
|
+
children = symbol.get("children", [])
|
|
279
|
+
if children:
|
|
280
|
+
result = search_symbols(children, target_name)
|
|
281
|
+
if result:
|
|
282
|
+
return result
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def get_declaration_range(
|
|
287
|
+
client, file_path: str, declaration_name: str
|
|
288
|
+
) -> tuple[int, int] | None:
|
|
289
|
+
"""Get the line range (1-indexed) of a declaration by name using LSP document symbols.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
client: The Lean LSP client instance (LeanLSPClient)
|
|
293
|
+
file_path: Relative path to the Lean file
|
|
294
|
+
declaration_name: Name of the declaration to find (case-sensitive)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Tuple of (start_line, end_line) as 1-indexed integers, or None if not found
|
|
298
|
+
"""
|
|
299
|
+
from lean_lsp_mcp.server import logger
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# Ensure file is opened (LSP needs this to analyze the file)
|
|
303
|
+
client.open_file(file_path)
|
|
304
|
+
|
|
305
|
+
# Get document symbols from LSP
|
|
306
|
+
symbols = client.get_document_symbols(file_path)
|
|
307
|
+
|
|
308
|
+
if not symbols:
|
|
309
|
+
logger.debug(
|
|
310
|
+
"No document symbols returned for '%s' - file may not be processed yet",
|
|
311
|
+
file_path,
|
|
312
|
+
)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
matching_symbol = search_symbols(symbols, declaration_name)
|
|
316
|
+
if not matching_symbol:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
# Extract range - LSP returns 0-indexed, convert to 1-indexed
|
|
320
|
+
range_info = matching_symbol.get("range")
|
|
321
|
+
if not range_info:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
start_line = range_info["start"]["line"] + 1
|
|
325
|
+
end_line = range_info["end"]["line"] + 1
|
|
326
|
+
|
|
327
|
+
return (start_line, end_line)
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.warning(
|
|
331
|
+
"Failed to get declaration range for '%s' in '%s': %s",
|
|
332
|
+
declaration_name,
|
|
333
|
+
file_path,
|
|
334
|
+
e,
|
|
335
|
+
)
|
|
336
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.5.
|
|
11
|
+
Requires-Dist: leanclient==0.5.2
|
|
12
12
|
Requires-Dist: mcp[cli]==1.21.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import AsyncContextManager
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from tests.helpers.mcp_client import MCPClient, result_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture()
|
|
14
|
+
def diagnostic_file(test_project_path: Path) -> Path:
|
|
15
|
+
path = test_project_path / "DiagnosticTest.lean"
|
|
16
|
+
content = textwrap.dedent(
|
|
17
|
+
"""
|
|
18
|
+
import Mathlib
|
|
19
|
+
|
|
20
|
+
-- Line 3: Valid definition
|
|
21
|
+
def validDef : Nat := 42
|
|
22
|
+
|
|
23
|
+
-- Line 6: Error on this line
|
|
24
|
+
def errorDef : Nat := "string"
|
|
25
|
+
|
|
26
|
+
-- Line 9: Another valid definition
|
|
27
|
+
def anotherValidDef : Nat := 100
|
|
28
|
+
|
|
29
|
+
-- Line 12: Another error
|
|
30
|
+
def anotherError : String := 123
|
|
31
|
+
|
|
32
|
+
-- Line 15: Valid theorem
|
|
33
|
+
theorem validTheorem : True := by
|
|
34
|
+
trivial
|
|
35
|
+
"""
|
|
36
|
+
).strip()
|
|
37
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
38
|
+
return path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_diagnostic_messages_without_line_range(
|
|
43
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
44
|
+
diagnostic_file: Path,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Test getting all diagnostic messages without line range filtering."""
|
|
47
|
+
async with mcp_client_factory() as client:
|
|
48
|
+
diagnostics = await client.call_tool(
|
|
49
|
+
"lean_diagnostic_messages",
|
|
50
|
+
{"file_path": str(diagnostic_file)},
|
|
51
|
+
)
|
|
52
|
+
diag_text = result_text(diagnostics)
|
|
53
|
+
|
|
54
|
+
# Should contain both errors
|
|
55
|
+
assert "string" in diag_text.lower() or "error" in diag_text.lower()
|
|
56
|
+
# Check that multiple diagnostics are returned (at least the two errors we created)
|
|
57
|
+
assert diag_text.count("severity") >= 2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_diagnostic_messages_with_start_line(
|
|
62
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
63
|
+
diagnostic_file: Path,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Test getting diagnostic messages starting from a specific line."""
|
|
66
|
+
async with mcp_client_factory() as client:
|
|
67
|
+
# First get all diagnostics to see what we have
|
|
68
|
+
all_diagnostics = await client.call_tool(
|
|
69
|
+
"lean_diagnostic_messages",
|
|
70
|
+
{
|
|
71
|
+
"file_path": str(diagnostic_file),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
all_diag_text = result_text(all_diagnostics)
|
|
75
|
+
|
|
76
|
+
# Get diagnostics starting from line 10 (should only include the second error)
|
|
77
|
+
diagnostics = await client.call_tool(
|
|
78
|
+
"lean_diagnostic_messages",
|
|
79
|
+
{
|
|
80
|
+
"file_path": str(diagnostic_file),
|
|
81
|
+
"start_line": 10,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
diag_text = result_text(diagnostics)
|
|
85
|
+
|
|
86
|
+
# Should contain the second error (line 13: anotherError)
|
|
87
|
+
assert "123" in diag_text or "error" in diag_text.lower()
|
|
88
|
+
# Should have fewer diagnostics than all_diagnostics
|
|
89
|
+
assert len(diag_text) < len(all_diag_text)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_diagnostic_messages_with_line_range(
|
|
94
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
95
|
+
diagnostic_file: Path,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Test getting diagnostic messages for a specific line range."""
|
|
98
|
+
async with mcp_client_factory() as client:
|
|
99
|
+
# First, get all diagnostics to see what lines they're actually on
|
|
100
|
+
all_diagnostics = await client.call_tool(
|
|
101
|
+
"lean_diagnostic_messages",
|
|
102
|
+
{"file_path": str(diagnostic_file)},
|
|
103
|
+
)
|
|
104
|
+
all_diag_text = result_text(all_diagnostics)
|
|
105
|
+
|
|
106
|
+
# Extract line numbers from the diagnostics (format: "l7c23-l7c31")
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
line_matches = re.findall(r"l(\d+)c", all_diag_text)
|
|
110
|
+
if line_matches:
|
|
111
|
+
first_error_line = int(line_matches[0])
|
|
112
|
+
|
|
113
|
+
# Get diagnostics only up to that error line
|
|
114
|
+
diagnostics = await client.call_tool(
|
|
115
|
+
"lean_diagnostic_messages",
|
|
116
|
+
{
|
|
117
|
+
"file_path": str(diagnostic_file),
|
|
118
|
+
"start_line": 1,
|
|
119
|
+
"end_line": first_error_line,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
diag_text = result_text(diagnostics)
|
|
123
|
+
|
|
124
|
+
# Should contain the first error
|
|
125
|
+
assert "string" in diag_text.lower() or len(diag_text) > 0
|
|
126
|
+
# Should be fewer diagnostics than all
|
|
127
|
+
assert len(diag_text) < len(all_diag_text)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.asyncio
|
|
131
|
+
async def test_diagnostic_messages_with_no_errors_in_range(
|
|
132
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
133
|
+
diagnostic_file: Path,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Test getting diagnostic messages for a range with no errors."""
|
|
136
|
+
async with mcp_client_factory() as client:
|
|
137
|
+
# Get diagnostics only for lines 14-17 (valid theorem, should have no errors)
|
|
138
|
+
diagnostics = await client.call_tool(
|
|
139
|
+
"lean_diagnostic_messages",
|
|
140
|
+
{
|
|
141
|
+
"file_path": str(diagnostic_file),
|
|
142
|
+
"start_line": 14,
|
|
143
|
+
"end_line": 17,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
diag_text = result_text(diagnostics)
|
|
147
|
+
|
|
148
|
+
# Should indicate no errors or be empty
|
|
149
|
+
# The exact format depends on how the tool formats an empty result
|
|
150
|
+
assert (
|
|
151
|
+
"no" in diag_text.lower()
|
|
152
|
+
or len(diag_text.strip()) == 0
|
|
153
|
+
or diag_text == "[]"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.fixture()
|
|
158
|
+
def declaration_diagnostic_file(test_project_path: Path) -> Path:
|
|
159
|
+
"""Create a test file with multiple declarations, some with errors."""
|
|
160
|
+
path = test_project_path / "DeclarationDiagnosticTest.lean"
|
|
161
|
+
content = textwrap.dedent(
|
|
162
|
+
"""
|
|
163
|
+
import Mathlib
|
|
164
|
+
|
|
165
|
+
-- First theorem with a clear type error
|
|
166
|
+
theorem firstTheorem : 1 + 1 = 2 := "string instead of proof"
|
|
167
|
+
|
|
168
|
+
-- Valid definition
|
|
169
|
+
def validFunction : Nat := 42
|
|
170
|
+
|
|
171
|
+
-- Second theorem with an error in the statement type mismatch
|
|
172
|
+
theorem secondTheorem : Nat := True
|
|
173
|
+
|
|
174
|
+
-- Another valid definition
|
|
175
|
+
def anotherValidFunction : String := "hello"
|
|
176
|
+
"""
|
|
177
|
+
).strip()
|
|
178
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.asyncio
|
|
183
|
+
async def test_diagnostic_messages_with_declaration_name_valid(
|
|
184
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
185
|
+
declaration_diagnostic_file: Path,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Test filtering diagnostics by a specific declaration name."""
|
|
188
|
+
async with mcp_client_factory() as client:
|
|
189
|
+
# Get all diagnostics first to verify file has errors
|
|
190
|
+
all_diagnostics = await client.call_tool(
|
|
191
|
+
"lean_diagnostic_messages",
|
|
192
|
+
{"file_path": str(declaration_diagnostic_file)},
|
|
193
|
+
)
|
|
194
|
+
all_diag_text = result_text(all_diagnostics)
|
|
195
|
+
|
|
196
|
+
# File should have diagnostics (contains intentional errors)
|
|
197
|
+
assert len(all_diag_text) > 0
|
|
198
|
+
assert "string" in all_diag_text.lower() or "type" in all_diag_text.lower()
|
|
199
|
+
|
|
200
|
+
# Get diagnostics for firstTheorem only
|
|
201
|
+
diagnostics = await client.call_tool(
|
|
202
|
+
"lean_diagnostic_messages",
|
|
203
|
+
{
|
|
204
|
+
"file_path": str(declaration_diagnostic_file),
|
|
205
|
+
"declaration_name": "firstTheorem",
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
diag_text = result_text(diagnostics)
|
|
209
|
+
|
|
210
|
+
# Should contain error from firstTheorem
|
|
211
|
+
# The exact error message may vary, but should reference the theorem
|
|
212
|
+
assert len(diag_text) > 0
|
|
213
|
+
|
|
214
|
+
# Filtered diagnostics should be shorter than or equal to all diagnostics
|
|
215
|
+
assert len(diag_text) <= len(all_diag_text)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@pytest.mark.asyncio
|
|
219
|
+
async def test_diagnostic_messages_with_declaration_name_with_errors(
|
|
220
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
221
|
+
declaration_diagnostic_file: Path,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Test filtering by declaration that has type errors."""
|
|
224
|
+
async with mcp_client_factory() as client:
|
|
225
|
+
# Get all diagnostics first to verify file has errors
|
|
226
|
+
all_diagnostics = await client.call_tool(
|
|
227
|
+
"lean_diagnostic_messages",
|
|
228
|
+
{"file_path": str(declaration_diagnostic_file)},
|
|
229
|
+
)
|
|
230
|
+
all_diag_text = result_text(all_diagnostics)
|
|
231
|
+
|
|
232
|
+
# File should have diagnostics (contains intentional errors)
|
|
233
|
+
assert len(all_diag_text) > 0
|
|
234
|
+
|
|
235
|
+
# Get diagnostics for secondTheorem (has type error in statement)
|
|
236
|
+
diagnostics = await client.call_tool(
|
|
237
|
+
"lean_diagnostic_messages",
|
|
238
|
+
{
|
|
239
|
+
"file_path": str(declaration_diagnostic_file),
|
|
240
|
+
"declaration_name": "secondTheorem",
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
diag_text = result_text(diagnostics)
|
|
244
|
+
|
|
245
|
+
# secondTheorem has type errors, should have diagnostics
|
|
246
|
+
assert len(diag_text) > 0
|
|
247
|
+
assert isinstance(diag_text, str)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
async def test_diagnostic_messages_with_declaration_name_no_errors(
|
|
252
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
253
|
+
declaration_diagnostic_file: Path,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Test filtering by declaration that has no errors."""
|
|
256
|
+
async with mcp_client_factory() as client:
|
|
257
|
+
# Get diagnostics for validFunction (no errors)
|
|
258
|
+
diagnostics = await client.call_tool(
|
|
259
|
+
"lean_diagnostic_messages",
|
|
260
|
+
{
|
|
261
|
+
"file_path": str(declaration_diagnostic_file),
|
|
262
|
+
"declaration_name": "validFunction",
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
diag_text = result_text(diagnostics)
|
|
266
|
+
|
|
267
|
+
# Should indicate no errors or be empty
|
|
268
|
+
assert (
|
|
269
|
+
"no" in diag_text.lower()
|
|
270
|
+
or len(diag_text.strip()) == 0
|
|
271
|
+
or diag_text == "[]"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.asyncio
|
|
276
|
+
async def test_diagnostic_messages_with_nonexistent_declaration(
|
|
277
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
278
|
+
declaration_diagnostic_file: Path,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Test error handling when declaration name doesn't exist."""
|
|
281
|
+
async with mcp_client_factory() as client:
|
|
282
|
+
# Try to get diagnostics for non-existent declaration
|
|
283
|
+
result = await client.call_tool(
|
|
284
|
+
"lean_diagnostic_messages",
|
|
285
|
+
{
|
|
286
|
+
"file_path": str(declaration_diagnostic_file),
|
|
287
|
+
"declaration_name": "nonExistentTheorem",
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
result_str = result_text(result)
|
|
291
|
+
|
|
292
|
+
# Should return error message about declaration not found
|
|
293
|
+
assert "not found" in result_str.lower()
|
|
294
|
+
assert "nonExistentTheorem" in result_str
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@pytest.mark.asyncio
|
|
298
|
+
async def test_diagnostic_messages_declaration_name_takes_precedence(
|
|
299
|
+
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
300
|
+
declaration_diagnostic_file: Path,
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Test that declaration_name takes precedence over start_line/end_line."""
|
|
303
|
+
async with mcp_client_factory() as client:
|
|
304
|
+
# Use declaration_name with conflicting start_line/end_line
|
|
305
|
+
# declaration_name should take precedence
|
|
306
|
+
diagnostics = await client.call_tool(
|
|
307
|
+
"lean_diagnostic_messages",
|
|
308
|
+
{
|
|
309
|
+
"file_path": str(declaration_diagnostic_file),
|
|
310
|
+
"declaration_name": "firstTheorem",
|
|
311
|
+
"start_line": 1, # These should be ignored
|
|
312
|
+
"end_line": 3, # These should be ignored
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
diag_text = result_text(diagnostics)
|
|
316
|
+
|
|
317
|
+
# Should get diagnostics for firstTheorem, not lines 1-3
|
|
318
|
+
# (which would only include imports)
|
|
319
|
+
assert len(diag_text) > 0
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import textwrap
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import AsyncContextManager
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from tests.helpers.mcp_client import MCPClient, result_text
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture()
|
|
14
|
-
def diagnostic_file(test_project_path: Path) -> Path:
|
|
15
|
-
path = test_project_path / "DiagnosticTest.lean"
|
|
16
|
-
content = textwrap.dedent(
|
|
17
|
-
"""
|
|
18
|
-
import Mathlib
|
|
19
|
-
|
|
20
|
-
-- Line 3: Valid definition
|
|
21
|
-
def validDef : Nat := 42
|
|
22
|
-
|
|
23
|
-
-- Line 6: Error on this line
|
|
24
|
-
def errorDef : Nat := "string"
|
|
25
|
-
|
|
26
|
-
-- Line 9: Another valid definition
|
|
27
|
-
def anotherValidDef : Nat := 100
|
|
28
|
-
|
|
29
|
-
-- Line 12: Another error
|
|
30
|
-
def anotherError : String := 123
|
|
31
|
-
|
|
32
|
-
-- Line 15: Valid theorem
|
|
33
|
-
theorem validTheorem : True := by
|
|
34
|
-
trivial
|
|
35
|
-
"""
|
|
36
|
-
).strip()
|
|
37
|
-
path.write_text(content + "\n", encoding="utf-8")
|
|
38
|
-
return path
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@pytest.mark.asyncio
|
|
42
|
-
async def test_diagnostic_messages_without_line_range(
|
|
43
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
44
|
-
diagnostic_file: Path,
|
|
45
|
-
) -> None:
|
|
46
|
-
"""Test getting all diagnostic messages without line range filtering."""
|
|
47
|
-
async with mcp_client_factory() as client:
|
|
48
|
-
diagnostics = await client.call_tool(
|
|
49
|
-
"lean_diagnostic_messages",
|
|
50
|
-
{"file_path": str(diagnostic_file)},
|
|
51
|
-
)
|
|
52
|
-
diag_text = result_text(diagnostics)
|
|
53
|
-
|
|
54
|
-
# Should contain both errors
|
|
55
|
-
assert "string" in diag_text.lower() or "error" in diag_text.lower()
|
|
56
|
-
# Check that multiple diagnostics are returned (at least the two errors we created)
|
|
57
|
-
assert diag_text.count("severity") >= 2
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@pytest.mark.asyncio
|
|
61
|
-
async def test_diagnostic_messages_with_start_line(
|
|
62
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
63
|
-
diagnostic_file: Path,
|
|
64
|
-
) -> None:
|
|
65
|
-
"""Test getting diagnostic messages starting from a specific line."""
|
|
66
|
-
async with mcp_client_factory() as client:
|
|
67
|
-
# First get all diagnostics to see what we have
|
|
68
|
-
all_diagnostics = await client.call_tool(
|
|
69
|
-
"lean_diagnostic_messages",
|
|
70
|
-
{
|
|
71
|
-
"file_path": str(diagnostic_file),
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
all_diag_text = result_text(all_diagnostics)
|
|
75
|
-
|
|
76
|
-
# Get diagnostics starting from line 10 (should only include the second error)
|
|
77
|
-
diagnostics = await client.call_tool(
|
|
78
|
-
"lean_diagnostic_messages",
|
|
79
|
-
{
|
|
80
|
-
"file_path": str(diagnostic_file),
|
|
81
|
-
"start_line": 10,
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
diag_text = result_text(diagnostics)
|
|
85
|
-
|
|
86
|
-
# Should contain the second error (line 13: anotherError)
|
|
87
|
-
assert "123" in diag_text or "error" in diag_text.lower()
|
|
88
|
-
# Should have fewer diagnostics than all_diagnostics
|
|
89
|
-
assert len(diag_text) < len(all_diag_text)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@pytest.mark.asyncio
|
|
93
|
-
async def test_diagnostic_messages_with_line_range(
|
|
94
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
95
|
-
diagnostic_file: Path,
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Test getting diagnostic messages for a specific line range."""
|
|
98
|
-
async with mcp_client_factory() as client:
|
|
99
|
-
# First, get all diagnostics to see what lines they're actually on
|
|
100
|
-
all_diagnostics = await client.call_tool(
|
|
101
|
-
"lean_diagnostic_messages",
|
|
102
|
-
{"file_path": str(diagnostic_file)},
|
|
103
|
-
)
|
|
104
|
-
all_diag_text = result_text(all_diagnostics)
|
|
105
|
-
|
|
106
|
-
# Extract line numbers from the diagnostics (format: "l7c23-l7c31")
|
|
107
|
-
import re
|
|
108
|
-
|
|
109
|
-
line_matches = re.findall(r"l(\d+)c", all_diag_text)
|
|
110
|
-
if line_matches:
|
|
111
|
-
first_error_line = int(line_matches[0])
|
|
112
|
-
|
|
113
|
-
# Get diagnostics only up to that error line
|
|
114
|
-
diagnostics = await client.call_tool(
|
|
115
|
-
"lean_diagnostic_messages",
|
|
116
|
-
{
|
|
117
|
-
"file_path": str(diagnostic_file),
|
|
118
|
-
"start_line": 1,
|
|
119
|
-
"end_line": first_error_line,
|
|
120
|
-
},
|
|
121
|
-
)
|
|
122
|
-
diag_text = result_text(diagnostics)
|
|
123
|
-
|
|
124
|
-
# Should contain the first error
|
|
125
|
-
assert "string" in diag_text.lower() or len(diag_text) > 0
|
|
126
|
-
# Should be fewer diagnostics than all
|
|
127
|
-
assert len(diag_text) < len(all_diag_text)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@pytest.mark.asyncio
|
|
131
|
-
async def test_diagnostic_messages_with_no_errors_in_range(
|
|
132
|
-
mcp_client_factory: Callable[[], AsyncContextManager[MCPClient]],
|
|
133
|
-
diagnostic_file: Path,
|
|
134
|
-
) -> None:
|
|
135
|
-
"""Test getting diagnostic messages for a range with no errors."""
|
|
136
|
-
async with mcp_client_factory() as client:
|
|
137
|
-
# Get diagnostics only for lines 14-17 (valid theorem, should have no errors)
|
|
138
|
-
diagnostics = await client.call_tool(
|
|
139
|
-
"lean_diagnostic_messages",
|
|
140
|
-
{
|
|
141
|
-
"file_path": str(diagnostic_file),
|
|
142
|
-
"start_line": 14,
|
|
143
|
-
"end_line": 17,
|
|
144
|
-
},
|
|
145
|
-
)
|
|
146
|
-
diag_text = result_text(diagnostics)
|
|
147
|
-
|
|
148
|
-
# Should indicate no errors or be empty
|
|
149
|
-
# The exact format depends on how the tool formats an empty result
|
|
150
|
-
assert (
|
|
151
|
-
"no" in diag_text.lower()
|
|
152
|
-
or len(diag_text.strip()) == 0
|
|
153
|
-
or diag_text == "[]"
|
|
154
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|