From 9a56aca6ef244c9ac9b4b6c3f9b7d9492ac48b21 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Mon, 28 Jul 2025 22:15:19 +0200 Subject: [PATCH] modify linter / checks --- server/src/lsp_server.py | 20 +- server/src/nx_plugins/poco_plugin.py | 13 - server/src/parser/ast.py | 93 ------- server/src/parser/lexer.py | 243 ------------------ server/src/parser/parser.py | 124 --------- .../src/{nx_plugins => plugins}/__init__.py | 0 server/src/plugins/poco_plugin.py | 48 ++++ server/src/tools/checks.py | 4 + 8 files changed, 59 insertions(+), 486 deletions(-) delete mode 100644 server/src/nx_plugins/poco_plugin.py delete mode 100644 server/src/parser/ast.py delete mode 100644 server/src/parser/lexer.py delete mode 100644 server/src/parser/parser.py rename server/src/{nx_plugins => plugins}/__init__.py (100%) create mode 100644 server/src/plugins/poco_plugin.py create mode 100644 server/src/tools/checks.py diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index a359eb1..707f401 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -54,17 +54,18 @@ from tools.semantic_tokens import ( collect_semantic_tokens, encode_tokens, ) -from nx_plugins.poco_plugin import commands -from tools import poco_check +from plugins.poco_plugin import commands +from tools import checks -DIAGNOSTIC_SOURCE = "tclint" +DIAGNOSTIC_SOURCE = "nx-post-support" class TclLanguageServer(server.LanguageServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.parser = Parser() - self.parser._commands.update(commands) + for command in commands: + self.parser._commands.update(command) self.diagnostics = {} def format( @@ -98,19 +99,12 @@ class TclLanguageServer(server.LanguageServer): document: TextDocument, ) -> List[Violation]: violations = [] + self.parser.violations = [] tree = self.parser.parse(document.source) violations += self.parser.violations - # if debug > 0: - # print(tree.pretty(positions=(debug > 1))) - - for checker in poco_check.get_checkers(): + for checker in checks.get_checkers(): violations += checker.check(document.source, tree) - - # v = CommentVisitor() - # ignore_lines = v.run(tree, path) - # violations = filter_violations(violations, config.ignore, ignore_lines) - return violations def lint(self, document: TextDocument): diff --git a/server/src/nx_plugins/poco_plugin.py b/server/src/nx_plugins/poco_plugin.py deleted file mode 100644 index 622864e..0000000 --- a/server/src/nx_plugins/poco_plugin.py +++ /dev/null @@ -1,13 +0,0 @@ -from tclint.commands.checks import CommandArgError - - -def _lib_ge_command_buffer(args, parser): - if len(args) != 4: - raise CommandArgError( - f"wrong # of args to LIB_GE_command_buffer_edit_*: got {len(args)}, expected 4" - ) - args[2] = parser.parse_script(args[2]) - return args - - -commands = {"LIB_GE_command_buffer_edit_prepend": _lib_ge_command_buffer} diff --git a/server/src/parser/ast.py b/server/src/parser/ast.py deleted file mode 100644 index f7c937c..0000000 --- a/server/src/parser/ast.py +++ /dev/null @@ -1,93 +0,0 @@ -class Node: - def _pos_str(self): - start_pos_str = "?" - if self.pos is not None: - start_pos_str = f"{self.pos[0]}:{self.pos[1]}" - end_pos_str = "?" - if self.end_pos is not None: - end_pos_str = f"{self.end_pos[0]}:{self.end_pos[1]}" - - return f" # {start_pos_str}-{end_pos_str}" - - def _make_str(self, indent=None, positions=False): - if indent is not None: - s = " " * indent - else: - s = "" - - s += self.__class__.__name__ - s += "(" - - if self.value: - s += repr(self.value) - if self.children: - s += ", " - - if positions and self.children: - s += self._pos_str() - - for i, child in enumerate(self.children): - if indent is not None: - s += "\n" - s += child._make_str( - indent=None if indent is None else indent + 1, positions=positions - ) - if i < len(self.children) - 1: - s += ", " - s += ")" - - if positions and not self.children: - s += self._pos_str() - - return s - - def pretty(self, positions=False): - return self._make_str(indent=0, positions=positions) - - def accept(self, visitor): - raise NotImplementedError() - - -class Script(Node): - def __init__(self, statements): - self.statements = statements - - def accept(self, visitor): - return visitor.visit_script(self) - - -class ProcDef(Node): - def __init__(self, name, args, body): - self.name = name - self.args = args - self.body = body - - def accept(self, visitor): - return visitor.visit_proc(self) - - -class SetStmt(Node): - def __init__(self, varname, value): - self.varname = varname - self.value = value - - def accept(self, visitor): - return visitor.visit_set(self) - - -class Namespace(Node): - def __init__(self, name, body): - self.name = name - self.body = body - - def accept(self, visitor): - return visitor.visit_namespace(self) - - -class CommandSubst(Node): - def __init__(self, name, args): - self.name = name - self.args = args - - def accept(self, visitor): - return visitor.visit_command(self) diff --git a/server/src/parser/lexer.py b/server/src/parser/lexer.py deleted file mode 100644 index dc18312..0000000 --- a/server/src/parser/lexer.py +++ /dev/null @@ -1,243 +0,0 @@ -import ply.lex as lex -from typing import Tuple - -TOK_BACKSLASH_NEWLINE = "BACKSLASH_NEWLINE" -TOK_BACKSLASH_SUB = "BACKSLASH_SUB" -TOK_NEWLINE = "NEWLINE" -TOK_SEMI = "SEMI" -TOK_WS = "WS" -TOK_QUOTE = "QUOTE" -TOK_ARG_EXPANSION = "ARG_EXPANSION" -TOK_LBRACE = "LBRACE" -TOK_RBRACE = "RBRACE" -TOK_STAR = "STAR" -TOK_LBRACKET = "LBRACKET" -TOK_RBRACKET = "RBRACKET" -TOK_DOLLAR = "DOLLAR" -TOK_LPAREN = "LPAREN" -TOK_RPAREN = "RPAREN" -TOK_HASH = "HASH" -TOK_ALPHA_CHARS = "ALPHA_CHARS" -TOK_NUM_CHARS = "NUM_CHARS" -TOK_NAMESPACE_SEP = "NAMESPACE_SEP" -TOK_CHAR = "CHAR" -TOK_CONTENTS = "CONTENTS" -TOK_EOF = None - -STATE_BRACEDWORD = "bracedword" - - -class TclSyntaxError(Exception): - def __init__(self, message, start: Tuple[int, int], end: Tuple[int, int]): - super().__init__(message) - self.start = start - self.end = end - - -class _LexTable: - tokens = ( - TOK_BACKSLASH_NEWLINE, - TOK_BACKSLASH_SUB, - TOK_NEWLINE, - TOK_SEMI, - TOK_WS, - TOK_QUOTE, - TOK_ARG_EXPANSION, - TOK_LBRACE, - TOK_RBRACE, - TOK_STAR, - TOK_LBRACKET, - TOK_RBRACKET, - TOK_DOLLAR, - TOK_LPAREN, - TOK_RPAREN, - TOK_HASH, - TOK_ALPHA_CHARS, - TOK_NUM_CHARS, - TOK_NAMESPACE_SEP, - TOK_CHAR, - TOK_CONTENTS, - ) - - # This defines a conditional lexing state for parsing braced words. This is a - # performance optimization; since there are few special characters in this context, - # we can use a smaller set of tokens to parse them faster. This has a large impact - # since most Tcl programs have a large number of braced words. Any token with - # `bracedword` in its name is included in this state. Tokens that are included in - # this state and the default state also include `INITIAL` in their name. - states = ((STATE_BRACEDWORD, "exclusive"),) - - def _tok(self, t): - pos = (t.lexer.lineno, t.lexer.colno) - t.lexer.lineno += t.value.count("\n") - index = t.value.rfind("\n") - if index == -1: - t.lexer.colno += len(t.value) - else: - remaining = t.value[index + 1 :] - t.lexer.colno = len(remaining) + 1 - - t.value = (t.value, pos) - return t - - # Priority important - def t_bracedword_INITIAL_BACKSLASH_NEWLINE(self, t): - r"\\\n" - return self._tok(t) - - # Priority important - def t_bracedword_INITIAL_BACKSLASH_SUB(self, t): - r"\\." - return self._tok(t) - - def t_NEWLINE(self, t): - r"\n" - return self._tok(t) - - def t_SEMI(self, t): - r";" - return self._tok(t) - - # TODO: should use \s? - def t_WS(self, t): - r"[\t\v\f\r ]+" - return self._tok(t) - - def t_QUOTE(self, t): - r'"' - return self._tok(t) - - # Must be higher priority than LBRACE - def t_ARG_EXPANSION(self, t): - r"\{\*\}" - return self._tok(t) - - def t_bracedword_INITIAL_LBRACE(self, t): - r"\{" - return self._tok(t) - - def t_bracedword_INITIAL_RBRACE(self, t): - r"\}" - return self._tok(t) - - def t_STAR(self, t): - r"\*" - return self._tok(t) - - def t_LBRACKET(self, t): - r"\[" - return self._tok(t) - - def t_RBRACKET(self, t): - r"\]" - return self._tok(t) - - def t_DOLLAR(self, t): - r"\$" - return self._tok(t) - - def t_LPAREN(self, t): - r"\(" - return self._tok(t) - - def t_RPAREN(self, t): - r"\)" - return self._tok(t) - - def t_HASH(self, t): - r"\#" - return self._tok(t) - - # Valid non-numeric chars in variable names - def t_ALPHA_CHARS(self, t): - r"[A-Za-z_]+" - return self._tok(t) - - # Valid numeric chars in variable names - # This is split up from the above to facilitate expression parsing, since - # e.g. 1eq1 can't be a single token. - def t_NUM_CHARS(self, t): - r"[0-9]+" - return self._tok(t) - - def t_NAMESPACE_SEP(self, t): - r"::+" - return self._tok(t) - - def t_bracedword_CONTENTS(self, t): - r"[^{}\\]+" - return self._tok(t) - - # Catch-all. TODO: inefficient, should probably munch multiple chars - def t_CHAR(self, t): - r"." - return self._tok(t) - - # Error handling rule - # TODO: do we need this? since we have a catch-all... - # there is a warning - def t_bracedword_INITIAL_error(self, t): - print("Illegal character '%s'" % t.value[0]) - t.lexer.skip(1) - - def __init__(self): - self.lexer = lex.lex(object=self) - self.lexer.lineno = 1 - self.lexer.colno = 1 - - def new_lexer(self, pos=None): - lexer = self.lexer.clone() - lexer.lineno = 1 - lexer.colno = 1 - - if pos is not None: - line, col = pos - lexer.lineno = line - lexer.colno = col - - return lexer - - -# Calling `lex.lex()` performs an expensive reflection process to generate the lexer. -# This singleton class holds a preinitialized lexer that can then be cloned to create -# individual instances. -LexTable = _LexTable() - - -class Lexer: - def __init__(self, pos=None): - self.lexer = LexTable.new_lexer(pos) - self.current = None - - def input(self, text): - self.lexer.input(text) - self.current = self.lexer.token() - - def type(self): - if self.current is None: - return TOK_EOF - return self.current.type - - def value(self): - if self.current is None: - return None - return self.current.value[0] - - def pos(self): - if self.current is None: - return (self.lexer.lineno, self.lexer.colno) - return self.current.value[1] - - def next(self): - self.current = self.lexer.token() - - def expect(self, *tokens, message, pos): - if self.type() not in tokens: - self.next() # munch another token to update position - raise TclSyntaxError(message, pos, self.pos()) - - self.next() - - def assert_(self, *tokens): - assert self.current.type in tokens - self.next() diff --git a/server/src/parser/parser.py b/server/src/parser/parser.py deleted file mode 100644 index 5238b69..0000000 --- a/server/src/parser/parser.py +++ /dev/null @@ -1,124 +0,0 @@ -from parser.lexer import ( - TOK_ALPHA_CHARS, - TOK_DOLLAR, - TOK_EOF, - TOK_LBRACE, - TOK_LBRACKET, - TOK_NEWLINE, - TOK_NUM_CHARS, - TOK_RBRACE, - TOK_SEMI, - TOK_WS, - TclSyntaxError, -) -from parser.ast import CommandSubst, Namespace, ProcDef, Script, SetStmt - - -class Parser: - def __init__(self, lexer): - self.lexer = lexer - - def parse(self): - statements = [] - while self.lexer.type() != TOK_EOF: - if self.lexer.type() in (TOK_WS, TOK_NEWLINE, TOK_SEMI): - self.lexer.next() - continue - statements.append(self.parse_statement()) - return Script(statements) - - def parse_statement(self): - if self.lexer.type() == TOK_ALPHA_CHARS: - cmd = self.lexer.value() - if cmd == "proc": - return self.parse_proc() - elif cmd == "set": - return self.parse_set() - elif cmd == "namespace": - return self.parse_namespace() - else: - return self.parse_command() - else: - raise TclSyntaxError( - "Unknown statement", self.lexer.pos(), self.lexer.pos() - ) - - def parse_proc(self): - self.lexer.next() # skip 'proc' - name = self.expect_value(TOK_ALPHA_CHARS) - args = self.parse_arguments() - body = self.parse_body() - return ProcDef(name, args, body) - - def parse_arguments(self): - args = [] - self.expect_token(TOK_LBRACE) - while self.lexer.type() != TOK_RBRACE: - if self.lexer.type() == TOK_LBRACE: - self.lexer.next() - arg_name = self.expect_value(TOK_ALPHA_CHARS) - default = ( - self.expect_value(TOK_ALPHA_CHARS) - if self.lexer.type() != TOK_RBRACE - else None - ) - self.expect_token(TOK_RBRACE) - args.append((arg_name, default)) - else: - args.append((self.expect_value(TOK_ALPHA_CHARS), None)) - self.expect_token(TOK_RBRACE) - return args - - def parse_body(self): - if self.lexer.type() == TOK_LBRACE: - self.lexer.next() - body_tokens = [] - while self.lexer.type() != TOK_RBRACE: - body_tokens.append(self.lexer.value()) - self.lexer.next() - self.lexer.next() # skip RBRACE - return " ".join(body_tokens) - else: - raise TclSyntaxError("Expected body", self.lexer.pos(), self.lexer.pos()) - - def parse_set(self): - self.lexer.next() - varname = self.expect_value(TOK_ALPHA_CHARS) - value = self.expect_value(TOK_ALPHA_CHARS) - return SetStmt(varname, value) - - def parse_namespace(self): - self.lexer.next() # skip 'namespace' - self.expect_token(TOK_ALPHA_CHARS) # 'eval' - name = self.expect_value(TOK_ALPHA_CHARS) - body = self.parse_body() - return Namespace(name, body) - - def parse_command(self): - name = self.expect_value(TOK_ALPHA_CHARS) - args = [] - while self.lexer.type() in ( - TOK_ALPHA_CHARS, - TOK_NUM_CHARS, - TOK_LBRACKET, - TOK_DOLLAR, - ): - args.append(self.lexer.value()) - self.lexer.next() - return CommandSubst(name, args) - - def expect_token(self, token): - if self.lexer.type() != token: - raise TclSyntaxError( - f"Expected {token}", self.lexer.pos(), self.lexer.pos() - ) - self.lexer.next() - - def expect_value(self, token): - if self.lexer.type() != token: - raise TclSyntaxError( - f"Expected {token}", self.lexer.pos(), self.lexer.pos() - ) - value = self.lexer.value() - self.lexer.next() - return value diff --git a/server/src/nx_plugins/__init__.py b/server/src/plugins/__init__.py similarity index 100% rename from server/src/nx_plugins/__init__.py rename to server/src/plugins/__init__.py diff --git a/server/src/plugins/poco_plugin.py b/server/src/plugins/poco_plugin.py new file mode 100644 index 0000000..eac8dd3 --- /dev/null +++ b/server/src/plugins/poco_plugin.py @@ -0,0 +1,48 @@ +from tclint.commands.checks import CommandArgError +from tclint.syntax_tree import BracedWord + + +def _lib_ge_command_buffer_edit(args, parser, command_name, pos_script, len_args): + if len(args) != len_args: + raise CommandArgError( + f"wrong # of args to {command_name}: got {len(args)}, expected {len_args}" + ) + args[pos_script] = parser.parse_script(args[pos_script]) + return args + + +def _lib_ge_command_buffer(args, parser): + if ( + len(args) < 1 + or len(args) > 2 + or (len(args) == 1 and isinstance(args[0], BracedWord)) + ): + raise CommandArgError( + f"wrong # of args to LIB_GE_command_buffer: got {len(args)}, expected 1 or 2" + ) + if len(args) == 1: + return args + args[0] = parser.parse_script(args[0]) + return args + + +def lib_ge_command_buffer_edit_append(args, parser): + _lib_ge_command_buffer_edit(args, parser, "LIB_GE_command_buffer_edit_append", 2, 4) + + +def lib_ge_command_buffer_edit_prepend(args, parser): + _lib_ge_command_buffer_edit( + args, parser, "LIB_GE_command_buffer_edit_prepend", 2, 4 + ) + + +def lib_ge_command_buffer_edit_insert(args, parser): + _lib_ge_command_buffer_edit(args, parser, "LIB_GE_command_buffer_edit_insert", 2, 6) + + +commands = [ + {"LIB_GE_command_buffer_edit_append": lib_ge_command_buffer_edit_append}, + {"LIB_GE_command_buffer_edit_prepend": lib_ge_command_buffer_edit_prepend}, + {"LIB_GE_command_buffer_edit_insert": lib_ge_command_buffer_edit_insert}, + {"LIB_GE_command_buffer": _lib_ge_command_buffer}, +] diff --git a/server/src/tools/checks.py b/server/src/tools/checks.py new file mode 100644 index 0000000..f714128 --- /dev/null +++ b/server/src/tools/checks.py @@ -0,0 +1,4 @@ +def get_checkers(): + checkers = () + + return checkers