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