🐐 LSP
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from centvrion.lsp.analysis import (
|
||||
Definition, ParseFailure, collect_definitions, definition_at, parse,
|
||||
)
|
||||
from centvrion.lsp.diagnostics import build_diagnostics
|
||||
from centvrion.lsp.hover import hover_at
|
||||
|
||||
|
||||
# --- Diagnostics ---
|
||||
# (source, expected_diagnostic_count, expected_first_position_or_None)
|
||||
# Positions are 0-based (line, character).
|
||||
|
||||
diagnostic_tests = [
|
||||
("DESIGNA foo VT X", 0, None),
|
||||
("DIC(\"hello\")", 0, None),
|
||||
("DEFINI greet (x) VT {\n DIC(x)\n}", 0, None),
|
||||
("DESIGNA foo VT @@@", 1, (0, 15)),
|
||||
("DESIGNA foo VT", 1, (0, 14)),
|
||||
("DIC(\"unclosed", 1, (0, 4)),
|
||||
]
|
||||
|
||||
class TestDiagnostics(unittest.TestCase):
|
||||
@parameterized.expand(diagnostic_tests)
|
||||
def test_diagnostics(self, source, expected_count, expected_pos):
|
||||
diags = build_diagnostics(source)
|
||||
self.assertEqual(len(diags), expected_count)
|
||||
if expected_pos is not None:
|
||||
self.assertEqual((diags[0]["line"], diags[0]["character"]), expected_pos)
|
||||
self.assertEqual(diags[0]["severity"], 1)
|
||||
self.assertEqual(diags[0]["source"], "centvrion")
|
||||
|
||||
def test_parse_returns_program_on_success(self):
|
||||
result = parse("DIC(\"hi\")")
|
||||
self.assertNotIsInstance(result, ParseFailure)
|
||||
|
||||
def test_parse_returns_failure_on_error(self):
|
||||
result = parse("DESIGNA")
|
||||
self.assertIsInstance(result, ParseFailure)
|
||||
|
||||
|
||||
# --- Hover ---
|
||||
# (source, line, char, expected_text_substring_or_None)
|
||||
|
||||
hover_tests = [
|
||||
("DIC(X)", 0, 4, "10"),
|
||||
("DIC(III)", 0, 4, "3"),
|
||||
("DIC(MMXXV)", 0, 4, "2025"),
|
||||
("DIC(IV)", 0, 4, "4"),
|
||||
("DIC(X)", 0, 0, None),
|
||||
("DESIGNA foo VT X", 0, 8, None),
|
||||
("DESIGNA foo VT X\nDIC(@@@", 0, 15, "10"),
|
||||
]
|
||||
|
||||
class TestHover(unittest.TestCase):
|
||||
@parameterized.expand(hover_tests)
|
||||
def test_hover(self, source, line, char, expected):
|
||||
info = hover_at(source, line, char)
|
||||
if expected is None:
|
||||
self.assertIsNone(info)
|
||||
else:
|
||||
self.assertIsNotNone(info)
|
||||
self.assertIn(expected, info.text)
|
||||
|
||||
def test_hover_range_covers_numeral(self):
|
||||
info = hover_at("DIC(MMXXV)", 0, 4)
|
||||
self.assertEqual(info.line, 0)
|
||||
self.assertEqual(info.start_character, 4)
|
||||
self.assertEqual(info.end_character, 9)
|
||||
|
||||
|
||||
# --- Definitions / go-to-def ---
|
||||
|
||||
definition_tests = [
|
||||
# variable use in DIC -> DESIGNA site
|
||||
("DESIGNA foo VT X\nDIC(foo)", 1, 4, ("foo", 0, 8, "variable")),
|
||||
# function call -> DEFINI site
|
||||
("DEFINI greet (x) VT {\n DIC(x)\n}\nINVOCA greet (V)", 3, 8, ("greet", 0, 7, "function")),
|
||||
# parameter inside body -> not a definition site we track; returns None
|
||||
("DEFINI greet (x) VT {\n DIC(x)\n}", 1, 6, None),
|
||||
# undefined identifier
|
||||
("DIC(missing)", 0, 4, None),
|
||||
# destructuring assignment
|
||||
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 1, 4, ("a", 0, 8, "variable")),
|
||||
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 2, 4, ("b", 0, 11, "variable")),
|
||||
# cursor right after identifier (typical F12 position): the identifier wins
|
||||
# over the adjacent punctuation, matching standard editor behavior.
|
||||
("DESIGNA foo VT X\nDIC(foo)", 1, 7, ("foo", 0, 8, "variable")),
|
||||
]
|
||||
|
||||
class TestDefinition(unittest.TestCase):
|
||||
@parameterized.expand(definition_tests)
|
||||
def test_definition(self, source, line, char, expected):
|
||||
result = definition_at(source, line, char)
|
||||
if expected is None:
|
||||
self.assertIsNone(result)
|
||||
else:
|
||||
name, exp_line, exp_char, kind = expected
|
||||
self.assertEqual(
|
||||
result,
|
||||
Definition(name=name, line=exp_line, character=exp_char, kind=kind),
|
||||
)
|
||||
|
||||
def test_functions_shadow_variables_with_same_name(self):
|
||||
src = "DESIGNA bar VT X\nDEFINI bar () VT {\n DIC(I)\n}\nINVOCA bar ()"
|
||||
defs = collect_definitions(parse(src))
|
||||
self.assertEqual(defs["bar"].kind, "function")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user