add linter
This commit is contained in:
+124
-13
@@ -12,7 +12,7 @@ import re
|
||||
import sys
|
||||
import sysconfig
|
||||
import traceback
|
||||
from typing import Any, Optional, Sequence, Tuple
|
||||
from typing import Any, List, Optional, Sequence, Tuple
|
||||
import re
|
||||
|
||||
|
||||
@@ -46,27 +46,35 @@ from pygls.workspace.text_document import TextDocument
|
||||
from common.load_data import standard_items
|
||||
from common.formatter import format_tcl
|
||||
from tclint.parser import Parser
|
||||
from tclint.lexer import TclSyntaxError
|
||||
from tclint.format import Formatter, FormatterOpts
|
||||
from tclint.violations import Violation
|
||||
from tools.semantic_tokens import (
|
||||
SemanticTokenCollector,
|
||||
collect_semantic_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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parser = Parser()
|
||||
self.parser._commands.update(commands)
|
||||
self.diagnostics = {}
|
||||
|
||||
def format(
|
||||
self,
|
||||
document: TextDocument,
|
||||
options: lsp.FormattingOptions,
|
||||
range: Optional[Tuple[int, int]] = None,
|
||||
):
|
||||
parser = Parser()
|
||||
parser._commands.update(commands)
|
||||
tree = parser.parse(document.source)
|
||||
|
||||
log_to_output(tree.pretty(2))
|
||||
# parser = Parser(command_plugins=["nx_plugins.poco_plugin.py"])
|
||||
# parser._commands.update(commands)
|
||||
|
||||
indent = "\t" if not options.insert_spaces else " " * options.tab_size
|
||||
|
||||
@@ -81,9 +89,87 @@ class TclLanguageServer(server.LanguageServer):
|
||||
|
||||
if range is not None:
|
||||
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 = {}
|
||||
@@ -126,6 +212,7 @@ TOOL_ARGS = [] # default arguments always passed to your tool.
|
||||
def did_open(params: lsp.DidOpenTextDocumentParams) -> None:
|
||||
"""LSP handler for textDocument/didOpen request."""
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
LSP_SERVER.compute_diagnostics(document)
|
||||
|
||||
|
||||
@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)
|
||||
def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
||||
"""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)
|
||||
@@ -271,9 +384,7 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
|
||||
# **********************************************************
|
||||
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
|
||||
def did_change_configuration(params: lsp.DidChangeConfigurationParams):
|
||||
settings = params.settings
|
||||
|
||||
log_to_output(str(settings))
|
||||
"""LSP Handler for Config Changes"""
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.INITIALIZE)
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user