134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from logging import getLogger
|
|
from typing import NamedTuple, Self
|
|
|
|
from lsprotocol.types import Diagnostic, DiagnosticSeverity, Position, Range
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
class Pair(NamedTuple):
|
|
start: str
|
|
end: str
|
|
|
|
|
|
class SyntaxPair(Enum):
|
|
Paren = Pair("(", ")")
|
|
Square = Pair("[", "]")
|
|
|
|
@classmethod
|
|
def by_start_elem(cls, start: str) -> Self:
|
|
for option in cls:
|
|
if option.value[0] == start:
|
|
return option
|
|
|
|
raise ValueError(f"`{start}` not a valid start character")
|
|
|
|
@classmethod
|
|
def by_end_elem(cls, end: str) -> Self:
|
|
for option in cls:
|
|
if option.value[1] == end:
|
|
return option
|
|
|
|
raise ValueError(f"`{end}` not a valid end character")
|
|
|
|
|
|
def char_range(line: int, char: int) -> Range:
|
|
return Range(Position(line, char), Position(line, char + 1))
|
|
|
|
|
|
def pair_mismatch(line: int, char: int, msg: str) -> Diagnostic:
|
|
return Diagnostic(
|
|
char_range(line, char),
|
|
msg,
|
|
severity=DiagnosticSeverity.Error,
|
|
)
|
|
|
|
|
|
class StackElement(NamedTuple):
|
|
range: Range
|
|
elem: SyntaxPair
|
|
|
|
|
|
@dataclass()
|
|
class IterativeParser:
|
|
_stack: list[StackElement] = field(default_factory=list)
|
|
|
|
def peek(self) -> StackElement:
|
|
return self._stack[-1]
|
|
|
|
def pop(self) -> StackElement:
|
|
return self._stack.pop()
|
|
|
|
def push(self, pair: StackElement) -> None:
|
|
return self._stack.append(pair)
|
|
|
|
def __call__(self, raw: list[str]) -> list[Diagnostic]:
|
|
in_string = False
|
|
errs = []
|
|
for line, raw_line in enumerate(raw):
|
|
for char, raw_char in enumerate(raw_line):
|
|
match raw_char:
|
|
case ";":
|
|
if not in_string:
|
|
break
|
|
case '"':
|
|
in_string = not in_string
|
|
case "(" | "[":
|
|
if not in_string:
|
|
self.push(
|
|
StackElement(
|
|
char_range(line, char),
|
|
SyntaxPair.by_start_elem(raw_char),
|
|
)
|
|
)
|
|
case "]" | ")":
|
|
if not in_string:
|
|
if not self._stack:
|
|
errs.append(
|
|
pair_mismatch(
|
|
line, char, f"one {raw_char} too much"
|
|
)
|
|
)
|
|
continue
|
|
|
|
expected = SyntaxPair.by_end_elem(raw_char)
|
|
elem = self._stack.pop()
|
|
if elem.elem == expected:
|
|
continue
|
|
|
|
if self._stack and self._stack[-1].elem == expected:
|
|
errs.append(
|
|
pair_mismatch(
|
|
line, char, f"unclosed {elem.elem.value.start}"
|
|
)
|
|
)
|
|
self._stack.pop()
|
|
self._stack.append(elem)
|
|
else:
|
|
errs.append(
|
|
pair_mismatch(
|
|
line, char, f"one {raw_char} too much"
|
|
)
|
|
)
|
|
self._stack.append(elem)
|
|
|
|
for rest in self._stack:
|
|
errs.append(
|
|
Diagnostic(
|
|
rest.range,
|
|
f"unclosed {rest.elem.value.start}",
|
|
severity=DiagnosticSeverity.Error,
|
|
)
|
|
)
|
|
|
|
self._stack = []
|
|
|
|
return errs
|
|
|
|
|
|
if __name__ == "__main__":
|
|
p = IterativeParser()
|
|
print(p(["((([]]))"]))
|