import re from tclint.commands.plugins import PluginManager from tclint.config import Config from tclint.syntax_tree import ( BracedExpression, BracedWord, CommandSub, Expression, QuotedWord, Script, Visitor, ) from tclint.violations import Rule, Violation class LineLengthChecker: """Ensures lines aren't too long. Reports 'line-length' violations. """ # ref: https://github.com/eslint/eslint/blob/b29a16b22f234f6134475efb6c7be5ac946556ee/lib/rules/max-len.js#L101 # noqa: E501 # ^ ironic lint waiver... URL_RE = re.compile(r"[^:/?#]:\/\/[^?#]") def check(self, input, _, config): violations = [] for i, line in enumerate(input.split("\n")): if self.URL_RE.search(line) is not None: # ignore URLs continue lineno = i + 1 if len(line) > config.style_line_length: start = (lineno, 1) end = (lineno, len(line) + 1) violations.append( Violation( Rule.LINE_LENGTH, f"line length is {len(line)}, maximum allowed is" f" {config.style_line_length}", start, end, ) ) return violations class TrailingWhitespaceChecker: """Ensures lines don't include trailing whitespace. Reports 'trailing-whitespace' violations. """ def check(self, input, _, config): violations = [] for i, line in enumerate(input.split("\n")): lineno = i + 1 WHITESPACE = (" ", "\t") if line.endswith(WHITESPACE): start_col = len(line.rstrip("".join(WHITESPACE))) start = (lineno, start_col + 1) end = (lineno, len(line) + 1) violations.append( Violation( Rule.TRAILING_WHITESPACE, "line has trailing whitespace", start, end, ) ) return violations class RedefinedBuiltinChecker(Visitor): """Ensures names of built-in commands aren't reused by proc definitions. Reports 'redefined-builtin' violations. """ def __init__(self, plugin_manager: PluginManager): self._plugin_manager = plugin_manager def check(self, _, tree: Script, config: Config) -> list[Violation]: self._violations: list[Violation] = [] commands = self._plugin_manager.get_commands(config.commands) self._commands = commands.keys() tree.accept(self, recurse=True) return self._violations def visit_command(self, command): if command.routine.contents != "proc": return if len(command.args) == 0: # This is a syntax error, but should already be caught as a command-args # error by the parser's `proc` command handling. return name = command.args[0].contents if name in self._commands: self._violations.append( Violation( Rule.REDEFINED_BUILTIN, f"redefinition of built-in command '{name}'", command.pos, command.args[1].end_pos, ) ) class UnbracedExprChecker(Visitor): def check(self, _, tree, __) -> list[Violation]: self._violations: list[Violation] = [] tree.accept(self, recurse=True) return self._violations def visit_command(self, command): if command.routine.contents != "expr": return if len(command.args) == 0: # This is a syntax error, but should already be caught as a command-args # error by the parser's `expr` command handling. return if len(command.args) == 1 and isinstance( command.args[0], (BracedExpression, Expression) ): return # If we got here, tclint had trouble parsing the expression due to one of the # two following cases. for child in command.args: if child.contents is None: self._violations.append( Violation( Rule.UNBRACED_EXPR, "expression with substitutions should be enclosed by braces", command.args[0].pos, command.args[-1].end_pos, ) ) return for child in command.args: if isinstance(child, (BracedWord, QuotedWord)): self._violations.append( Violation( Rule.UNBRACED_EXPR, "expression containing braced or quoted words should be" " enclosed by braces", command.args[0].pos, command.args[-1].end_pos, ) ) return # If we reach here, there's probably a bug in expr parsing logic. assert False, ( "Children of expr node were different than expected, please file a bug" " report" ) class RedundantExprChecker(Visitor): def check(self, _, tree, __) -> list[Violation]: self._violations: list[Violation] = [] tree.accept(self, recurse=True) return self._violations def _check_operand(self, operand): if not isinstance(operand, CommandSub) or len(operand.children) != 1: return command = operand.children[0] if command.routine.contents == "expr": self._violations.append( Violation( Rule.REDUNDANT_EXPR, "unnecessary command substitution within expression", operand.pos, operand.end_pos, ) ) def visit_braced_expression(self, expression): if len(expression.children) == 1: self._check_operand(expression.children[0]) def visit_expression(self, expression): if len(expression.children) == 1: self._check_operand(expression.children[0]) def visit_unary_op(self, expr): self._check_operand(expr.children[1]) def visit_binary_op(self, expr): self._check_operand(expr.children[0]) self._check_operand(expr.children[2]) def visit_ternary_op(self, expr): self._check_operand(expr.children[0]) self._check_operand(expr.children[2]) self._check_operand(expr.children[4]) def visit_function(self, function): for arg in function.children[1:]: self._check_operand(arg) class UnopenedQuoteChecker(Visitor): # Matches a literal " not preceded by backslash (escaped quotes are intentional) BARE_QUOTE_RE = re.compile(r'(? list[Violation]: self._violations: list[Violation] = [] tree.accept(self, recurse=True) return self._violations def visit_bare_word(self, word): if self.BARE_QUOTE_RE.search(word.value): self._violations.append( Violation( Rule.UNOPENED_QUOTE, 'found " without opening quote', word.pos, word.end_pos, ) ) def get_checkers(plugin_manager: PluginManager): checkers = ( RedefinedBuiltinChecker(plugin_manager), UnbracedExprChecker(), RedundantExprChecker(), UnopenedQuoteChecker(), LineLengthChecker(), TrailingWhitespaceChecker(), ) return checkers