228 lines
6.7 KiB
Python
228 lines
6.7 KiB
Python
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
|