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(["((([]]))"]))