feat(tcl): add static variable name extraction and array key completion

This adds tooling to statically extract Tcl variable names from syntax
trees without evaluating substitutions, enabling better completions
for array elements and plain variables. The new helpers are wired
into the completion and navigation flows and are supported by tests
covering array keys and substitutions.

- Introduce variable_names.py with variable_name() and array_key_parts()
- Wire static name extraction into completion and symbol indexing
- Add tests for array key completion with substitutions
This commit is contained in:
Christoph Brandau
2026-09-10 09:07:18 +02:00
parent 7e9a4359cd
commit d8611d7aea
7 changed files with 300 additions and 8 deletions
+44
View File
@@ -0,0 +1,44 @@
"""Extract statically known variable names without evaluating Tcl substitutions."""
from tclint.syntax_tree import BareWord, CompoundBareWord, Node, QuotedWord, VarSub
def variable_name(node: Node) -> str | None:
contents = node.contents
if isinstance(contents, str):
return contents
if isinstance(node, (CompoundBareWord, QuotedWord)) and node.children:
first = node.children[0]
last = node.children[-1]
if (
isinstance(first, BareWord)
and isinstance(last, BareWord)
and "(" in first.value
and last.value.endswith(")")
):
# Substitutions in an array index do not change the array's name.
return first.value.split("(", 1)[0] or None
return None
def array_key_parts(node: Node, *, preserve_variables: bool = False) -> tuple[str | None, ...]:
"""Keep comma-separated literal index components; substitutions are unknown."""
contents = node.contents
if not isinstance(contents, str):
if not isinstance(node, (CompoundBareWord, QuotedWord)) or not variable_name(node):
return ()
chunks = []
for child in node.children:
if isinstance(child, BareWord):
chunks.append(child.value)
elif preserve_variables and isinstance(child, VarSub) and not child.children:
chunks.append("${" + child.value + "}" if child.braced else "$" + child.value)
else:
chunks.append("\0")
contents = "".join(chunks)
if "(" not in contents or not contents.endswith(")"):
return ()
return tuple(
part if part and not any(char in part for char in ("\0[]\\" if preserve_variables else "\0$[]\\")) else None
for part in contents.split("(", 1)[1][:-1].split(",")
)