add linter

This commit is contained in:
2025-07-27 22:11:16 +02:00
parent c6f0758b97
commit 4d79f268b8
5 changed files with 190 additions and 29 deletions
+124 -13
View File
@@ -12,7 +12,7 @@ import re
import sys import sys
import sysconfig import sysconfig
import traceback import traceback
from typing import Any, Optional, Sequence, Tuple from typing import Any, List, Optional, Sequence, Tuple
import re import re
@@ -46,27 +46,35 @@ from pygls.workspace.text_document import TextDocument
from common.load_data import standard_items from common.load_data import standard_items
from common.formatter import format_tcl from common.formatter import format_tcl
from tclint.parser import Parser from tclint.parser import Parser
from tclint.lexer import TclSyntaxError
from tclint.format import Formatter, FormatterOpts from tclint.format import Formatter, FormatterOpts
from tclint.violations import Violation
from tools.semantic_tokens import ( from tools.semantic_tokens import (
SemanticTokenCollector, SemanticTokenCollector,
collect_semantic_tokens, collect_semantic_tokens,
encode_tokens, encode_tokens,
) )
from tools.nx_plugins import commands from nx_plugins.poco_plugin import commands
from tools import poco_check
DIAGNOSTIC_SOURCE = "tclint"
class TclLanguageServer(server.LanguageServer): class TclLanguageServer(server.LanguageServer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parser = Parser()
self.parser._commands.update(commands)
self.diagnostics = {}
def format( def format(
self, self,
document: TextDocument, document: TextDocument,
options: lsp.FormattingOptions, options: lsp.FormattingOptions,
range: Optional[Tuple[int, int]] = None, range: Optional[Tuple[int, int]] = None,
): ):
parser = Parser() # parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
parser._commands.update(commands) # parser._commands.update(commands)
tree = parser.parse(document.source)
log_to_output(tree.pretty(2))
indent = "\t" if not options.insert_spaces else " " * options.tab_size indent = "\t" if not options.insert_spaces else " " * options.tab_size
@@ -81,9 +89,87 @@ class TclLanguageServer(server.LanguageServer):
if range is not None: if range is not None:
start, end = range start, end = range
return formatter.format_partial(document.source[start:end], parser) return formatter.format_partial(document.source[start:end], self.parser)
return formatter.format_top(document.source, parser) return formatter.format_top(document.source, self.parser)
def linter(
self,
document: TextDocument,
) -> List[Violation]:
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():
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):
diagnostics = []
try:
violations = self.linter(document)
except TclSyntaxError as e:
return [
lsp.Diagnostic(
message=str(e),
severity=lsp.DiagnosticSeverity.Error,
range=lsp.Range(
start=lsp.Position(e.start[0] - 1, e.start[1] - 1),
end=lsp.Position(e.end[0] - 1, e.end[1] - 1),
),
code="syntax error",
source=DIAGNOSTIC_SOURCE,
)
]
for violation in violations:
message = violation.message
severity = lsp.DiagnosticSeverity.Warning
start = lsp.Position(
line=violation.start[0] - 1, character=violation.start[1] - 1
)
end = lsp.Position(
line=violation.end[0] - 1, character=violation.end[1] - 1
)
diagnostics.append(
lsp.Diagnostic(
message=message,
severity=severity,
range=lsp.Range(
start=start,
end=end,
),
code=violation.id,
source=DIAGNOSTIC_SOURCE,
)
)
return diagnostics
def _compute_diagnostics(self, document: TextDocument) -> List[lsp.Diagnostic]:
return self.lint(document)
def compute_diagnostics(self, document: TextDocument):
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not
# present.
_, previous = self.diagnostics.get(document, (0, None))
diagnostics = self._compute_diagnostics(document)
# Only update if the list has changed
if previous != diagnostics:
self.diagnostics[document.uri] = (document.version, diagnostics)
WORKSPACE_SETTINGS = {} WORKSPACE_SETTINGS = {}
@@ -126,6 +212,7 @@ TOOL_ARGS = [] # default arguments always passed to your tool.
def did_open(params: lsp.DidOpenTextDocumentParams) -> None: def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
"""LSP handler for textDocument/didOpen request.""" """LSP handler for textDocument/didOpen request."""
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
LSP_SERVER.compute_diagnostics(document)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
@@ -142,7 +229,33 @@ def did_close(params: lsp.DidCloseTextDocumentParams) -> None:
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(params: lsp.DidChangeTextDocumentParams) -> None: def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
"""LSP handler for textDocument/didChange request""" """LSP handler for textDocument/didChange request"""
log_to_output("Document has changed") document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
LSP_SERVER.compute_diagnostics(document)
@LSP_SERVER.feature(
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
lsp.DiagnosticOptions(
identifier="pull-diagnostics",
inter_file_dependencies=False,
workspace_diagnostics=False,
),
)
def document_diagnostic(params: lsp.DocumentDiagnosticParams):
"""Return diagnostics for the requested document"""
was_cached = True
if (uri := params.text_document.uri) not in LSP_SERVER.diagnostics:
was_cached = False
doc = LSP_SERVER.workspace.get_text_document(uri)
LSP_SERVER.compute_diagnostics(doc)
version, diagnostics = LSP_SERVER.diagnostics[uri]
result_id = f"{uri}@{version}"
if was_cached and result_id == params.previous_result_id:
return lsp.UnchangedDocumentDiagnosticReport(result_id)
return lsp.FullDocumentDiagnosticReport(items=diagnostics, result_id=result_id)
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
@@ -271,9 +384,7 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
# ********************************************************** # **********************************************************
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION) @LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
def did_change_configuration(params: lsp.DidChangeConfigurationParams): def did_change_configuration(params: lsp.DidChangeConfigurationParams):
settings = params.settings """LSP Handler for Config Changes"""
log_to_output(str(settings))
@LSP_SERVER.feature(lsp.INITIALIZE) @LSP_SERVER.feature(lsp.INITIALIZE)
View File
+13
View File
@@ -0,0 +1,13 @@
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}
-16
View File
@@ -1,16 +0,0 @@
from tclint.syntax_tree import BracedWord
def lib_spf_prepend(args, parser):
if len(args) >= 5:
# Heuristik: myTag wurde zu früh getrennt, hänge ihn an den BracedBlock
merged_contents = args[3].contents + "\n" + args[4].contents
merged = BracedWord(merged_contents, pos=args[3].pos, end_pos=args[4].end_pos)
script = parser.parse_script(merged)
return args[:3] + [script] # [routine, arg1, arg2, script]
elif len(args) >= 4:
args[2] = parser.parse_script(args[2])
return args
commands = {"LIB_SPF_prepend": lib_spf_prepend}
+53
View File
@@ -0,0 +1,53 @@
from enum import Enum
from tclint.syntax_tree import Visitor
from tclint.violations import Violation
class PoCoRule(Enum):
POCO_VALIDATION = "poco-validation"
def __str__(self):
return self.value
class PocoCommandChecker(Visitor):
def __init__(self):
self._violations = []
def check(self, _, tree):
self._violations.clear()
tree.accept(self, recurse=True)
return self._violations
def visit_command(self, command):
name = command.routine.contents
if name not in {"LIB_GE_command_buffer_edit_prepend"}:
return
if len(command.args) != 4:
self._violations.append(
Violation(
PoCoRule.POCO_VALIDATION,
f"{name}: expected 4 arguments, got {len(command.args)}",
command.pos,
command.end_pos,
)
)
return
script_arg = command.args[2]
if script_arg.contents is None:
self._violations.append(
Violation(
PoCoRule.POCO_VALIDATION,
f"{name}: third argument must be a braced script",
script_arg.pos,
script_arg.end_pos,
)
)
def get_checkers():
checkers = (PocoCommandChecker(),)
return checkers