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.
Files changed (27) hide show
  1. {lean_lsp_mcp-0.12.1/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.13.0}/PKG-INFO +2 -2
  2. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/pyproject.toml +2 -2
  3. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/instructions.py +2 -2
  4. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/server.py +14 -0
  5. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/utils.py +75 -0
  6. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0/src/lean_lsp_mcp.egg-info}/PKG-INFO +2 -2
  7. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/requires.txt +1 -1
  8. lean_lsp_mcp-0.13.0/tests/test_diagnostic_line_range.py +319 -0
  9. lean_lsp_mcp-0.12.1/tests/test_diagnostic_line_range.py +0 -154
  10. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/LICENSE +0 -0
  11. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/README.md +0 -0
  12. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/setup.cfg +0 -0
  13. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/__init__.py +0 -0
  14. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/__main__.py +0 -0
  15. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/client_utils.py +0 -0
  16. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/file_utils.py +0 -0
  17. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp/search_utils.py +0 -0
  18. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
  19. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
  20. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
  21. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
  22. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_editor_tools.py +0 -0
  23. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_file_caching.py +0 -0
  24. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_logging.py +0 -0
  25. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_misc_tools.py +0 -0
  26. {lean_lsp_mcp-0.12.1 → lean_lsp_mcp-0.13.0}/tests/test_project_tools.py +0 -0
  27. {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.12.1
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.1
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.12.1"
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.1",
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 (use lean_file_contents if unsure).
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 the current proof situation.
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.12.1
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.1
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,4 +1,4 @@
1
- leanclient==0.5.1
1
+ leanclient==0.5.2
2
2
  mcp[cli]==1.21.0
3
3
  orjson>=3.11.1
4
4
 
@@ -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