tcl_language_support #23
@@ -1,7 +1,7 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "*"
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -35,7 +35,13 @@ jobs:
|
||||
git add package.json
|
||||
git commit -m "Update version to ${{ github.ref_name }}"
|
||||
git push origin HEAD:main
|
||||
- name: Publish to Visual Studio Marketplace
|
||||
- name: Publish to Visual Studio Marketplace (Pre-Release)
|
||||
if: contains(github.ref_name, '-')
|
||||
run: vsce publish --pre-release
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||
- name: Publish to Visual Studio Marketplace (Stable)
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
run: vsce publish
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCODE_MARKETPALCE }}
|
||||
|
||||
Vendored
+2
-1
@@ -2,6 +2,7 @@
|
||||
"ruff.configuration": {
|
||||
"lint": {
|
||||
"extend-ignore": ["F821", "E402"]
|
||||
}
|
||||
},
|
||||
"line-length": 200
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ if debugger_path:
|
||||
# This will ensure that execution is paused as soon as the debugger
|
||||
# connects to VS Code. If you don't want to pause here comment this
|
||||
# line and set breakpoints as appropriate.
|
||||
debugpy.breakpoint()
|
||||
# debugpy.breakpoint()
|
||||
|
||||
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
|
||||
# NOTE: Set breakpoint in `lsp_server.py` before continuing.
|
||||
|
||||
+53
-43
@@ -47,8 +47,9 @@ from tclint.violations import Violation
|
||||
from plugins.poco_plugin import commands
|
||||
from tools import checks, parser
|
||||
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||
from tools.completion_items import completion
|
||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||
from tools.inlay_hint import InlayHintGenerator
|
||||
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||
|
||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||
|
||||
@@ -60,6 +61,8 @@ class TclLanguageServer(server.LanguageServer):
|
||||
for command in commands:
|
||||
self.parser._commands.update(command)
|
||||
self.diagnostics = {}
|
||||
self.poco_completion: dict = {}
|
||||
self.proc_signatures: dict = {}
|
||||
|
||||
def format(
|
||||
self,
|
||||
@@ -121,12 +124,8 @@ class TclLanguageServer(server.LanguageServer):
|
||||
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
|
||||
)
|
||||
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(
|
||||
@@ -164,9 +163,7 @@ RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"
|
||||
|
||||
|
||||
MAX_WORKERS = 5
|
||||
LSP_SERVER = TclLanguageServer(
|
||||
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
|
||||
)
|
||||
LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
|
||||
|
||||
# **********************************************************
|
||||
# Tool specific code goes below this.
|
||||
@@ -189,9 +186,6 @@ 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)
|
||||
completion.reset()
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(completion, recurse=True)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
|
||||
@@ -210,9 +204,6 @@ def did_change(params: lsp.DidChangeTextDocumentParams) -> None:
|
||||
"""LSP handler for textDocument/didChange request"""
|
||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
LSP_SERVER.compute_diagnostics(document)
|
||||
completion.reset()
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(completion, recurse=True)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
@@ -243,20 +234,21 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
||||
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
poco = []
|
||||
|
||||
items = (
|
||||
standard_items.tcl_keyword_list
|
||||
+ standard_items.nx_procs
|
||||
+ standard_items.nx_variables
|
||||
+ completion.custom_functions
|
||||
)
|
||||
for key, value in LSP_SERVER.poco_completion.items():
|
||||
poco.extend(value)
|
||||
items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
|
||||
# @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||
# def document_symbols(params: lsp.DocumentSymbolParams):
|
||||
# doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
# return []
|
||||
# ast = LSP_SERVER.parser.parse(doc.source)
|
||||
# symbols = LSP_SERVER.extract_tcl_symbols(ast)
|
||||
|
||||
# return symbols
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
||||
@@ -265,10 +257,12 @@ def inlay_hints(params: lsp.InlayHintParams):
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
|
||||
# collect Inlay Hints
|
||||
generator = InlayHintGenerator(completion.proc_signatures)
|
||||
tree.accept(generator, recurse=True)
|
||||
|
||||
return generator.hints
|
||||
hints = []
|
||||
for key, value in LSP_SERVER.proc_signatures.items():
|
||||
generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key])
|
||||
tree.accept(generator, recurse=True)
|
||||
hints += generator.hints
|
||||
return hints
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
@@ -283,7 +277,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
|
||||
|
||||
data = []
|
||||
plugins = []
|
||||
hl = _Highlighter(plugins, log_to_output=log_to_output)
|
||||
hl = _Highlighter(plugins, LSP_SERVER.poco_completion)
|
||||
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(hl, recurse=True)
|
||||
@@ -336,9 +330,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
||||
label = match.get("label", "")
|
||||
|
||||
parameters = match.get("parameters", [])
|
||||
param_lines = (
|
||||
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||
)
|
||||
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||
|
||||
example_data = match.get("example", [])
|
||||
example_md = "\n".join(f"{line}" for line in example_data)
|
||||
@@ -422,12 +414,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
|
||||
settings = params.initialization_options["settings"]
|
||||
_update_workspace_settings(settings)
|
||||
log_to_output(
|
||||
f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n"
|
||||
)
|
||||
log_to_output(
|
||||
f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n"
|
||||
)
|
||||
log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n")
|
||||
log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n")
|
||||
semantic_tokens_legend = lsp.SemanticTokensLegend(
|
||||
token_types=TOKEN_TYPES,
|
||||
token_modifiers=TokenModifier,
|
||||
@@ -435,13 +423,37 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
return lsp.InitializeResult(
|
||||
capabilities=lsp.ServerCapabilities(
|
||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||
semantic_tokens_provider=lsp.SemanticTokensOptions(
|
||||
legend=semantic_tokens_legend, full=True, range=False
|
||||
),
|
||||
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.INITIALIZED)
|
||||
def initialized(params: lsp.InitializedParams):
|
||||
root = LSP_SERVER.workspace.root_path
|
||||
psc_files = get_all_psc_files(pathlib.Path(root))
|
||||
for psc_file in psc_files:
|
||||
poco_files = read_psc_file(psc_file)
|
||||
for sourced_layer in poco_files:
|
||||
completion.reset()
|
||||
try:
|
||||
file_root = pathlib.Path(root).joinpath(sourced_layer.subfolder if sourced_layer.subfolder else "")
|
||||
for tcl_file in sourced_layer.files:
|
||||
filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl")
|
||||
if not filepath.exists():
|
||||
continue
|
||||
completion.reset()
|
||||
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8")
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
tree.accept(completion, recurse=True)
|
||||
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
|
||||
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
|
||||
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
|
||||
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
|
||||
except Exception as e:
|
||||
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.EXIT)
|
||||
def on_exit(_params: Optional[Any] = None) -> None:
|
||||
"""Handle clean up on exit."""
|
||||
@@ -533,9 +545,7 @@ def _get_settings_by_document(document: workspace.Document | None):
|
||||
# *****************************************************
|
||||
# Logging and notification.
|
||||
# *****************************************************
|
||||
def log_to_output(
|
||||
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
|
||||
) -> None:
|
||||
def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
|
||||
LSP_SERVER.show_message_log(message, msg_type)
|
||||
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
from tclint.parser import Parser as tcLintParser
|
||||
from tclint.lexer import Lexer as tclingLexer
|
||||
|
||||
|
||||
def main():
|
||||
parser = tcLintParser(True)
|
||||
tree = parser.parse("""puts hello
|
||||
proc myProc {arg {arg7 0}} {}
|
||||
set myVar 123
|
||||
puts puts
|
||||
LIB_SPF_prepend MOM_strt Start_Lib {
|
||||
set somthing 1
|
||||
set more 2
|
||||
} myTag
|
||||
|
||||
LIB_SPF_prepend MOM_strt Start_Lib {
|
||||
proc test {} {
|
||||
puts "Hello"
|
||||
}
|
||||
set somthing 1
|
||||
set someting 3
|
||||
} myTag""")
|
||||
|
||||
print(tree.pretty(2))
|
||||
|
||||
|
||||
def lexer_test():
|
||||
lexer = tclingLexer()
|
||||
tree = lexer.input("""
|
||||
if {$oem(custom_clamp_4th) == 1 && $oem(custom_clamp_5th) == 1 \\
|
||||
&& $oem(status_clamp_4th) == "off" && $oem(status_clamp_5th) == "off"}""")
|
||||
|
||||
# print("Lexing input:\n", code)
|
||||
# print("\nTokens:\n" + "-" * 30)
|
||||
|
||||
while lexer.type() is not None:
|
||||
tok_type = lexer.type()
|
||||
tok_value = lexer.value()
|
||||
tok_pos = lexer.pos()
|
||||
print(f"Type: {tok_type:20} | Value: {repr(tok_value):20} | Pos: {tok_pos}")
|
||||
lexer.next()
|
||||
|
||||
|
||||
def test_1():
|
||||
from tclint.lexer import Lexer, TOK_BACKSLASH_NEWLINE
|
||||
|
||||
code = "expr {1 == 2 \\\n&& 3 == 4}"
|
||||
|
||||
lexer = Lexer()
|
||||
lexer.input(code)
|
||||
|
||||
while lexer.type() is not None:
|
||||
print(
|
||||
f"Type: {lexer.type():<20} | Value: {lexer.value()!r} | Pos: {lexer.pos()}"
|
||||
)
|
||||
lexer.next()
|
||||
|
||||
|
||||
class NodeVisitor:
|
||||
def visit_script(self, node):
|
||||
for stmt in node.statements:
|
||||
stmt.accept(self)
|
||||
|
||||
def visit_proc(self, node):
|
||||
print(f"Proc: {node.name}, args={node.args}")
|
||||
|
||||
def visit_set(self, node):
|
||||
print(f"Set: {node.varname} = {node.value}")
|
||||
|
||||
def visit_namespace(self, node):
|
||||
print(f"Namespace: {node.name}")
|
||||
|
||||
def visit_command(self, node):
|
||||
print(f"Command: {node.name} {node.args}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_1()
|
||||
@@ -1,5 +1,6 @@
|
||||
from tclint.syntax_tree import Visitor, Command, BareWord, List
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
|
||||
|
||||
class CompletionItems:
|
||||
@@ -38,14 +39,18 @@ class _Completion(Visitor):
|
||||
|
||||
if routine.contents == "proc" and command.args:
|
||||
first_arg = command.args[0]
|
||||
if hasattr(first_arg, "value") and not any(
|
||||
item.label == first_arg.value for item in self._custom_functions
|
||||
):
|
||||
self._custom_functions.append(
|
||||
lsp.CompletionItem(
|
||||
label=first_arg.value, kind=lsp.CompletionItemKind.Function
|
||||
)
|
||||
)
|
||||
if not first_arg.value:
|
||||
return
|
||||
|
||||
if any(item.label == first_arg.value for item in standard_items.nx_procs):
|
||||
return
|
||||
|
||||
try:
|
||||
self._custom_functions.remove(first_arg.value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self._custom_functions.append(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
|
||||
if len(command.args) < 2:
|
||||
return
|
||||
|
||||
@@ -65,4 +70,33 @@ class _Completion(Visitor):
|
||||
self._proc_signatures[first_arg.value] = param_names
|
||||
|
||||
|
||||
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
|
||||
"""
|
||||
Entfernt alle CompletionItems aus dem store, deren label in der items-Liste vorkommt.
|
||||
Änderungen erfolgen in-place.
|
||||
"""
|
||||
labels_to_remove = {item.label for item in items}
|
||||
|
||||
for key in list(store.keys()):
|
||||
filtered = [ci for ci in store[key] if ci.label not in labels_to_remove]
|
||||
if filtered:
|
||||
store[key] = filtered
|
||||
else:
|
||||
del store[key]
|
||||
|
||||
|
||||
def remove_shared_keys(nested_dict: dict[str, dict[str, list]], flat_dict: dict[str, list]) -> None:
|
||||
"""
|
||||
Entfernt alle Keys aus nested_dict[file][func], wenn func auch in flat_dict vorhanden ist.
|
||||
Änderungen erfolgen in-place.
|
||||
"""
|
||||
for file_path, func_dict in list(nested_dict.items()):
|
||||
for func_name in list(func_dict.keys()):
|
||||
if func_name in flat_dict:
|
||||
del nested_dict[file_path][func_name]
|
||||
|
||||
if not nested_dict[file_path]:
|
||||
del nested_dict[file_path]
|
||||
|
||||
|
||||
completion = _Completion()
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourcedFile:
|
||||
layer_name: str
|
||||
subfolder: Optional[str]
|
||||
files: List[str]
|
||||
|
||||
|
||||
def read_psc_file(psc_file: Path) -> List[SourcedFile]:
|
||||
tree = ET.parse(psc_file)
|
||||
root = tree.getroot()
|
||||
|
||||
layers = root.findall(".//Layer")
|
||||
|
||||
layer_info_list: List[SourcedFile] = []
|
||||
for layer in layers:
|
||||
layer_name = layer.attrib.get("Name")
|
||||
subfolder = layer.attrib.get("SubFolder")
|
||||
# Scripts-Filenames
|
||||
scripts = layer.find("Scripts")
|
||||
script_names = []
|
||||
if scripts is not None:
|
||||
for filename in scripts.findall("Filename"):
|
||||
name = filename.attrib.get("Name")
|
||||
if name:
|
||||
script_names.append(name)
|
||||
|
||||
layer_info_list.append(
|
||||
SourcedFile(layer_name=layer_name, subfolder=subfolder, files=script_names)
|
||||
)
|
||||
return layer_info_list
|
||||
|
||||
|
||||
def get_all_psc_files(root_path: Path) -> list[Path]:
|
||||
return [path for path in root_path.rglob("*.psc")]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test = get_all_psc_files(
|
||||
Path(
|
||||
r"H:\janus-engineering-customers\KSB_Frankenthal\custom\library\machine\installed_machines\ksb_pe_grob_g550_sone\postprocessor"
|
||||
)
|
||||
)
|
||||
print(test)
|
||||
for pp in test:
|
||||
read_psc_file(pp)
|
||||
@@ -5,6 +5,7 @@ from tclint.commands import get_commands
|
||||
import attrs
|
||||
from common.load_data import standard_items
|
||||
from tools.completion_items import completion
|
||||
import lsprotocol.types as lsp
|
||||
|
||||
|
||||
class TokenModifier(enum.IntFlag):
|
||||
@@ -40,10 +41,10 @@ TOKEN_TYPES = [
|
||||
|
||||
|
||||
class _Highlighter(Visitor):
|
||||
def __init__(self, plugins, log_to_output):
|
||||
def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
|
||||
self._commands = get_commands(plugins)
|
||||
self._tokens = []
|
||||
self.log_to_output = log_to_output
|
||||
self.custom_functions = custom_functions
|
||||
|
||||
def _get_token_info(self, node):
|
||||
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
|
||||
@@ -58,11 +59,7 @@ class _Highlighter(Visitor):
|
||||
# CompoundBareWord: versuche erstes Segment
|
||||
if hasattr(node, "children") and node.children:
|
||||
first_segment = node.children[0]
|
||||
if (
|
||||
hasattr(first_segment, "value")
|
||||
and first_segment.value is not None
|
||||
and hasattr(first_segment, "pos")
|
||||
):
|
||||
if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"):
|
||||
line, col = first_segment.pos
|
||||
return (line - 1, col - 1), len(first_segment.value)
|
||||
|
||||
@@ -84,13 +81,15 @@ class _Highlighter(Visitor):
|
||||
pass
|
||||
|
||||
def visit_bare_word(self, word: BareWord):
|
||||
if any(item.label == word.value for item in standard_items.nx_procs) or any(
|
||||
item.label == word.value for item in completion.custom_functions
|
||||
):
|
||||
name = word.value
|
||||
|
||||
in_standard = any(item.label == name for item in standard_items.nx_procs)
|
||||
|
||||
in_custom = any(item.label == name for items in self.custom_functions.values() for item in items)
|
||||
|
||||
if in_standard or in_custom:
|
||||
line, col = word.pos
|
||||
self._tokens.append(
|
||||
(((line - 1, col - 1), len(word.value), "function", []))
|
||||
)
|
||||
self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
|
||||
|
||||
def visit_command(self, command: Command):
|
||||
routine = command.routine
|
||||
@@ -158,9 +157,7 @@ class _Highlighter(Visitor):
|
||||
# Parameter mit Default-Wert ist meist eine List (z. B. {arg default})
|
||||
elif hasattr(child, "children") and len(child.children) >= 1:
|
||||
name_node = child.children[0]
|
||||
if hasattr(name_node, "value") and hasattr(
|
||||
name_node, "pos"
|
||||
):
|
||||
if hasattr(name_node, "value") and hasattr(name_node, "pos"):
|
||||
line, col = name_node.pos
|
||||
self._tokens.append(
|
||||
(
|
||||
@@ -174,9 +171,7 @@ class _Highlighter(Visitor):
|
||||
first_arg = command.args[1]
|
||||
if hasattr(first_arg, "pos") and first_arg.value is not None:
|
||||
line, col = first_arg.pos
|
||||
self._tokens.append(
|
||||
(((line - 1, col - 1), len(first_arg.value), "class", []))
|
||||
)
|
||||
self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
|
||||
|
||||
def tokens(self) -> list[Token]:
|
||||
"""Encode tokens as described in
|
||||
@@ -185,9 +180,7 @@ class _Highlighter(Visitor):
|
||||
tokens = []
|
||||
last_line = 0
|
||||
last_col = 0
|
||||
for (line, col), length, tok_type, tok_modifier in sorted(
|
||||
self._tokens, key=lambda x: x[0]
|
||||
):
|
||||
for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
|
||||
line_delta = line - last_line
|
||||
col_delta = col
|
||||
if line == last_line:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import List, DefaultDict, Union
|
||||
|
||||
from tclint.syntax_tree import Visitor, Command, CommandSub, Node, Script
|
||||
|
||||
|
||||
class SymbolTable:
|
||||
"""Holds a symbol table (links symbols to nodes)."""
|
||||
|
||||
def __init__(self):
|
||||
self.proc_def: DefaultDict[str, list[Node]] = defaultdict(list)
|
||||
|
||||
def add_proc_definition(self, command: Command) -> None:
|
||||
"""Add definition of procedure"""
|
||||
# command holds the "proc" keyword, so the proc name is 1st argument
|
||||
proc_name_node = command.args[0]
|
||||
proc_name = proc_name_node.contents
|
||||
if not proc_name:
|
||||
return
|
||||
logging.debug(
|
||||
f"Definition of proc '{proc_name}' at {proc_name_node._pos_str()}"
|
||||
)
|
||||
self.proc_def[proc_name].append(proc_name_node)
|
||||
|
||||
def lookup_proc_definitions(self, symbol_text: str) -> List[Node]:
|
||||
"""Lookup definitions of the procedure pointed at by node"""
|
||||
if symbol_text is None or symbol_text not in self.proc_def:
|
||||
return []
|
||||
return self.proc_def[symbol_text]
|
||||
|
||||
|
||||
class SymbolTableBuilder(Visitor):
|
||||
"""Builds a symbol table."""
|
||||
|
||||
def __init__(self):
|
||||
self.table = SymbolTable()
|
||||
|
||||
def build(self, tree: Union[CommandSub, Script]) -> SymbolTable:
|
||||
"""Run the builder visitor through the syntax tree, building a table."""
|
||||
tree.accept(self, recurse=True)
|
||||
return self.table
|
||||
|
||||
def visit_command(self, command: Command) -> None:
|
||||
if command.routine.contents == "proc":
|
||||
self.table.add_proc_definition(command)
|
||||
|
||||
Reference in New Issue
Block a user