import re from tclint.commands import get_commands from tclint.violations import Rule, Violation from tclint.syntax_tree import ( Visitor, BracedExpression, Expression, BracedWord, QuotedWord, CommandSub, ) 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 check(self, _, tree, config): self._violations = [] plugins = [config.commands] if config.commands is not None else [] commands = get_commands(plugins) 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, __): self._violations = [] 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, __): self._violations = [] 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) def get_checkers(): checkers = ( RedefinedBuiltinChecker(), UnbracedExprChecker(), RedundantExprChecker(), LineLengthChecker(), TrailingWhitespaceChecker(), ) return checkers