188 lines
7.4 KiB
Python
188 lines
7.4 KiB
Python
from enum import Enum
|
|
from tclint.syntax_tree import Visitor, BareWord, Command, VarSub, QuotedWord, BracedWord
|
|
from tclint.violations import Violation, Rule
|
|
|
|
|
|
class Rules(Enum):
|
|
VALIDATION = "validation"
|
|
OPTIONAL_ARG_POSITION = "optinal_args"
|
|
UNDECLARED_VARIABLE = "undeclared-variable"
|
|
|
|
def __str__(self):
|
|
return self.value
|
|
|
|
|
|
class CommandArgsCheck(Visitor):
|
|
def __init__(self):
|
|
self._violations = []
|
|
|
|
def check(self, _, tree):
|
|
self._violations.clear()
|
|
tree.accept(self, recurse=True)
|
|
return self._violations
|
|
|
|
def visit_command(self, command: Command):
|
|
if not hasattr(command.routine, "contents") or command.routine.contents != "proc":
|
|
return
|
|
if len(command.args) < 2:
|
|
return
|
|
|
|
args_node = command.args[1]
|
|
if not hasattr(args_node, "children"):
|
|
return
|
|
|
|
found_optional = False
|
|
for arg in args_node.children:
|
|
# Required argument
|
|
if isinstance(arg, BareWord):
|
|
if found_optional:
|
|
self._violations.append(
|
|
Violation(
|
|
Rules.OPTIONAL_ARG_POSITION,
|
|
"Required argument follows optional one",
|
|
arg.pos,
|
|
arg.end_pos,
|
|
)
|
|
)
|
|
# Optional argument
|
|
elif hasattr(arg, "children") and len(arg.children) >= 2:
|
|
found_optional = True
|
|
|
|
|
|
class UndeclaredVariableCheck(Visitor):
|
|
"""
|
|
Warn when inside a proc a variable is used ($var) that is neither:
|
|
- declared global in the proc via `global var`, nor
|
|
- referenced as namespaced global (e.g. $::var), nor
|
|
- assigned locally in the proc via `set var ...`, nor
|
|
- a proc argument.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._violations = []
|
|
|
|
def check(self, source, tree):
|
|
self._violations.clear()
|
|
# Traverse to find procs; inner scanning is handled per-proc
|
|
tree.accept(self, recurse=True)
|
|
return self._violations
|
|
|
|
def visit_command(self, command: Command):
|
|
# Only interested in procs
|
|
if not hasattr(command.routine, "contents"):
|
|
return
|
|
if command.routine.contents != "proc":
|
|
return
|
|
# Expect: proc <name> <args> <body>
|
|
if len(command.args) < 3:
|
|
return
|
|
|
|
proc_name_node = command.args[0]
|
|
args_node = command.args[1]
|
|
body_node = command.args[2]
|
|
|
|
# Collect proc arg names
|
|
arg_names = set()
|
|
if hasattr(args_node, "children"):
|
|
for child in args_node.children:
|
|
if isinstance(child, BareWord):
|
|
if child.contents:
|
|
arg_names.add(child.contents)
|
|
elif hasattr(child, "children") and child.children:
|
|
# Defaulted arg: first element should be the arg name
|
|
first = child.children[0]
|
|
if hasattr(first, "contents") and first.contents:
|
|
arg_names.add(first.contents)
|
|
|
|
# Two-pass scan over body: first collect locals (set) and declared globals
|
|
locals_set = set(arg_names)
|
|
declared_globals = set()
|
|
|
|
def _word_text(node) -> str | None:
|
|
if isinstance(node, BareWord) or isinstance(node, QuotedWord) or isinstance(node, BracedWord):
|
|
return node.contents
|
|
return getattr(node, "contents", None)
|
|
|
|
def _collect(node):
|
|
# Depth-first walk to collect set/global declarations
|
|
if isinstance(node, Command) and hasattr(node.routine, "contents"):
|
|
name = node.routine.contents
|
|
if name == "global":
|
|
for an in node.args:
|
|
text = _word_text(an)
|
|
if text:
|
|
declared_globals.add(text)
|
|
elif name == "set" and node.args:
|
|
# set var [value] - first arg is the variable name
|
|
first_arg = node.args[0]
|
|
text = _word_text(first_arg)
|
|
if text:
|
|
base = text.split("(", 1)[0] # Handle array syntax
|
|
if not base.startswith("::"):
|
|
locals_set.add(base)
|
|
elif name == "regsub" and node.args:
|
|
# regsub ?switches? exp string subSpec ?varName?
|
|
# If a varName is provided as the last arg, treat it as a local
|
|
last_arg = node.args[-1]
|
|
text = _word_text(last_arg)
|
|
if text:
|
|
base = text.split("(", 1)[0]
|
|
if base and not base.startswith("::"):
|
|
locals_set.add(base)
|
|
elif name == "foreach" and len(node.args) >= 3:
|
|
# foreach varlist1 list1 ?varlist2 list2 ...? body
|
|
# Treat loop variables as locals within the proc
|
|
# Iterate pairs (varlist, list) over all but the last arg (body)
|
|
pair_args = node.args[:-1]
|
|
i = 0
|
|
while i + 1 < len(pair_args):
|
|
varnode = pair_args[i]
|
|
text = _word_text(varnode) or ""
|
|
# Split var list by whitespace if braced list, else single name
|
|
names = text.split()
|
|
for nm in names:
|
|
base = nm.split("(", 1)[0]
|
|
if base and not base.startswith("::"):
|
|
locals_set.add(base)
|
|
i += 2
|
|
# Recurse
|
|
for ch in getattr(node, "children", []):
|
|
_collect(ch)
|
|
|
|
_collect(body_node)
|
|
|
|
# Second pass: flag VarSub usages that are not accounted for
|
|
def _scan(node):
|
|
# 1) Variable substitutions like $var
|
|
if isinstance(node, VarSub):
|
|
var_name = node.value or ""
|
|
if var_name.startswith("::"):
|
|
return
|
|
base = var_name
|
|
if base not in locals_set and base not in declared_globals:
|
|
msg = f"Variable '{base}' used in proc is not set locally and not declared global; use 'global {base}' or '$::{base}'"
|
|
self._violations.append(Violation(Rules.UNDECLARED_VARIABLE, msg, node.pos, node.end_pos))
|
|
# 2) Commands that take a variable name as first argument, e.g., 'incr var [amount]'
|
|
elif isinstance(node, Command) and hasattr(node.routine, "contents"):
|
|
if node.routine.contents == "incr" and node.args:
|
|
first_arg = node.args[0]
|
|
text = _word_text(first_arg) or ""
|
|
base = text.split("(", 1)[0]
|
|
if base and not base.startswith("::"):
|
|
if base not in locals_set and base not in declared_globals:
|
|
msg = f"Variable '{base}' used in proc is not set locally and not declared global; use 'global {base}'"
|
|
self._violations.append(Violation(Rules.UNDECLARED_VARIABLE, msg, first_arg.pos, first_arg.end_pos))
|
|
for ch in getattr(node, "children", []):
|
|
_scan(ch)
|
|
|
|
_scan(body_node)
|
|
|
|
|
|
def get_checkers():
|
|
checkers = (
|
|
CommandArgsCheck(),
|
|
UndeclaredVariableCheck(),
|
|
)
|
|
|
|
return checkers
|