97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Set
|
|
|
|
from tclint.syntax_tree import Command, Node, Script
|
|
|
|
|
|
@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]
|
|
if base.startswith("::"):
|
|
base = base[2:]
|
|
|
|
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:
|
|
procs.setdefault(proc_name, set())
|
|
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 = getattr(node.args[0], "contents", None)
|
|
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
|