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
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from tclint.syntax_tree import Command, Node, Script
|
|
from tclint.syntax_tree import List as TclList
|
|
from tools.variable_names import variable_name
|
|
|
|
|
|
@dataclass
|
|
class ProcRange:
|
|
name: str
|
|
start_line: int
|
|
end_line: int | None = None
|
|
|
|
|
|
def _normalize_var_name(raw_name: str | None) -> str | None:
|
|
if not raw_name:
|
|
return None
|
|
|
|
base = raw_name.split("(", 1)[0]
|
|
base = base.removeprefix("::")
|
|
|
|
return base or None
|
|
|
|
|
|
def build_variable_index(
|
|
source: str, tree: Node | None = None
|
|
) -> tuple[set[str], dict[str, set[str]], list[ProcRange]]:
|
|
"""
|
|
Parse Tcl source text and build:
|
|
- globals: set of variable names considered global suggestions
|
|
- procs: mapping proc_name -> set of local variable names inside that proc
|
|
- proc_ranges: list of ProcRange (name, start_line, end_line)
|
|
|
|
Rules:
|
|
- set ::var -> global var suggestion (strip leading :: and any array index "(")
|
|
- set var without :: at top level (not in namespace/proc) -> global suggestion
|
|
- set var without :: inside proc -> local to that proc
|
|
- global var1 var2 inside a proc -> global suggestions for those names
|
|
- set var inside namespace (no ::) is ignored for global suggestions
|
|
"""
|
|
_ = source # Kept for signature compatibility with callers.
|
|
|
|
globals_set: set[str] = set()
|
|
procs: dict[str, set[str]] = {}
|
|
proc_ranges: list[ProcRange] = []
|
|
|
|
if tree is None:
|
|
return globals_set, procs, proc_ranges
|
|
|
|
def walk(node: Node, scope_stack: list[tuple[str, str]]) -> None:
|
|
if isinstance(node, Command):
|
|
routine = getattr(node.routine, "contents", None)
|
|
|
|
if routine == "proc" and len(node.args) >= 3 and isinstance(node.args[2], Script):
|
|
proc_name = _normalize_var_name(getattr(node.args[0], "contents", None))
|
|
if proc_name is not None:
|
|
local_variables = procs.setdefault(proc_name, set())
|
|
for parameter in getattr(node.args[1], "children", []):
|
|
parameter_node = parameter
|
|
if isinstance(parameter, TclList) and parameter.children:
|
|
parameter_node = parameter.children[0]
|
|
parameter_name = _normalize_var_name(
|
|
getattr(parameter_node, "contents", None)
|
|
)
|
|
if parameter_name is not None:
|
|
local_variables.add(parameter_name)
|
|
proc_ranges.append(
|
|
ProcRange(
|
|
name=proc_name,
|
|
start_line=node.pos[0] - 1,
|
|
end_line=node.args[2].end_pos[0] - 1,
|
|
)
|
|
)
|
|
walk(node.args[2], [*scope_stack, ("proc", proc_name)])
|
|
return
|
|
|
|
if routine == "namespace" and len(node.args) >= 3 and getattr(node.args[0], "contents", None) == "eval" and isinstance(node.args[2], Script):
|
|
namespace_name = _normalize_var_name(getattr(node.args[1], "contents", None)) or ""
|
|
walk(node.args[2], [*scope_stack, ("namespace", namespace_name)])
|
|
return
|
|
|
|
if routine == "set" and node.args:
|
|
raw_name = variable_name(node.args[0])
|
|
base = _normalize_var_name(raw_name)
|
|
if base is not None:
|
|
if raw_name and raw_name.startswith("::"):
|
|
globals_set.add(base)
|
|
else:
|
|
scope_kind, scope_name = scope_stack[-1]
|
|
if scope_kind == "root":
|
|
globals_set.add(base)
|
|
elif scope_kind == "proc":
|
|
procs.setdefault(scope_name, set()).add(base)
|
|
|
|
if routine == "global":
|
|
for arg in node.args:
|
|
base = _normalize_var_name(getattr(arg, "contents", None))
|
|
if base is not None:
|
|
globals_set.add(base)
|
|
|
|
for child in getattr(node, "children", []):
|
|
walk(child, scope_stack)
|
|
|
|
walk(tree, [("root", "")])
|
|
return globals_set, procs, proc_ranges
|