Skip to content

Instantly share code, notes, and snippets.

@snoack
Last active April 21, 2016 18:17
Show Gist options
  • Save snoack/b1b4dfa401105497790f408e0fdb445d to your computer and use it in GitHub Desktop.
Save snoack/b1b4dfa401105497790f408e0fdb445d to your computer and use it in GitHub Desktop.
import ast
import re
import tokenize
import sys
__version__ = '0.1'
DEPRECATED_APIS = {
('re', 'match'): 'A101 use re.search() instead re.match()',
('codecs', 'open'): 'A102 use io.open() instead codecs.open()',
}
BAILOUT = (ast.Return, ast.Continue, ast.Break)
class TreeVisitor(ast.NodeVisitor):
def __init__(self):
self.errors = []
self.stack = []
def _visit_block(self, nodes, mandatory=False, docstring=False):
passNode = None
bailed = False
dead_code = False
for node in nodes:
if isinstance(node, ast.Pass):
passNode = node
if bailed and not dead_code:
dead_code = True
self.errors.append((node, 'A151 dead code after '
'return/continue/break'))
if isinstance(node, BAILOUT):
bailed = True
if not isinstance(node, ast.Expr):
continue
if isinstance(node.value, (ast.Call, ast.Yield)):
continue
if docstring and node is nodes[0] and isinstance(node.value, ast.Str):
continue
self.errors.append((node, 'A152 unused expression'))
if passNode:
if len(nodes) > 1:
self.errors.append((passNode, 'A153 redundant pass statement'))
if not mandatory and all(isinstance(n, ast.Pass) for n in nodes):
self.errors.append((passNode, 'A154 empty block'))
def _visit_block_node(self, node, **kwargs):
self._visit_block(node.body, **kwargs)
if hasattr(node, 'orelse'):
self._visit_block(node.orelse)
if hasattr(node, 'finalbody'):
self._visit_block(node.finalbody)
self.generic_visit(node)
visit_Try = visit_TryExcept = visit_TryFinally = _visit_block_node
visit_ExceptHandler = visit_While = \
lambda self, node: self._visit_block_node(node, mandatory=True)
visit_Module = visit_ClassDef = \
lambda self, node: self._visit_block_node(node, mandatory=True,
docstring=True)
def visit_Attribute(self, node):
if isinstance(node.ctx, ast.Load) and isinstance(node.value, ast.Name):
error = DEPRECATED_APIS.get((node.value.id, node.attr))
if error:
self.errors.append((node, error))
self.generic_visit(node)
def visit_ImportFrom(self, node):
for alias in node.names:
error = DEPRECATED_APIS.get((node.module, alias.name))
if error:
self.errors.append((node, error))
def visit_BinOp(self, node):
if isinstance(node.op, ast.Mod) and isinstance(node.left, ast.Str):
self.errors.append((node, 'A111 use format() instead % operator '
'for string formatting'))
multi_addition = (isinstance(node.op, ast.Add) and
isinstance(node.left, ast.BinOp) and
isinstance(node.left.op, ast.Add))
if multi_addition and (isinstance(node.left.left, ast.Str) or
isinstance(node.left.right, ast.Str) or
isinstance(node.right, ast.Str)):
self.errors.append((node, 'A112 use format() instead + operator '
'when concatenating >2 strings'))
self.generic_visit(node)
def visit_comprehension(self, node):
if isinstance(node.iter, (ast.Tuple, ast.Set, ast.Dict)):
self.errors.append((node.iter, 'A121 use lists for data '
'that have order'))
self.generic_visit(node)
def visit_For(self, node):
self._visit_block(node.body, mandatory=True)
self.visit_comprehension(node)
def visit_Call(self, node):
func = node.func
if isinstance(func, ast.Name) and func.id in {'filter', 'map'}:
if len(node.args) > 0 and isinstance(node.args[0], ast.Lambda):
self.errors.append((node, 'A131 use a comprehension '
'instead calling {}() with '
'lambda function'.format(func.id)))
self.generic_visit(node)
def visit_FunctionDef(self, node):
self._visit_block(node.body, mandatory=True, docstring=True)
self.stack.append((set(), []))
self.generic_visit(node)
targets, globals = self.stack.pop()
for var in globals:
if any(name not in targets for name in var.names):
self.errors.append((var, 'A141 redundant global/nonlocal '
'declaration'))
def visit_Name(self, node):
if self.stack and isinstance(node.ctx, ast.Store):
self.stack[-1][0].add(node.id)
def visit_Global(self, node):
if self.stack:
self.stack[-1][1].append(node)
else:
self.errors.append((node, 'A141 global/nonlocal declaration '
'on top-level'))
visit_Nonlocal = visit_Global
def visit_If(self, node):
has_else = bool(node.orelse)
if has_else and isinstance(node.body[-1], BAILOUT):
self.errors.append((node, 'A159 redundant else statement'))
self._visit_block(node.body, mandatory=has_else)
self._visit_block(node.orelse)
self.generic_visit(node)
class ASTChecker(object):
name = 'abp'
version = __version__
def __init__(self, tree, filename):
self.tree = tree
def run(self):
visitor = TreeVisitor()
visitor.visit(self.tree)
for node, error in visitor.errors:
yield (node.lineno, node.col_offset, error, type(self))
def check_non_default_encoding(physical_line, line_number):
if (line_number <= 2 and re.search(r'^\s*#.*coding[:=]', physical_line)):
return (0, 'A201 non-default file encoding')
check_non_default_encoding.name = 'abp-non-default-encoding'
check_non_default_encoding.version = __version__
def check_quotes(logical_line, tokens, previous_logical):
first_token = True
offset = 0
for kind, token, start, end, _ in tokens:
if kind == tokenize.INDENT:
offset = end[1]
continue
if kind == tokenize.STRING:
pos = start[1] - offset
match = re.search(r'^(u)?(b)?(r)?((""")?.*)$',
token, re.IGNORECASE | re.DOTALL)
(is_unicode, is_bytes, is_raw,
literal, has_doc_quotes) = match.groups()
if first_token and re.search(r'^(?:(?:def|class)\s|$)',
previous_logical):
if not has_doc_quotes:
yield (pos, 'A301 use triple double quotes for docstrings')
if is_unicode or is_bytes or is_raw:
yield (pos, "A302 don't use u, b or for doc strings")
elif start[0] == end[0]:
if is_raw:
literal = re.sub(r'\\(?!{})'.format(literal[0]),
'\\\\\\\\', literal)
if sys.version_info[0] >= 3:
if is_bytes:
literal = 'b' + literal
else:
literal = re.sub(r'(?<!\\)\\x(?!a[0d])([a-f][0-9a-f])',
lambda m: chr(int(m.group(1), 16)),
literal)
elif is_unicode:
literal = 'u' + literal
if repr(eval(literal)) != literal:
yield (pos, "A311 string literal doesn't match repr()")
first_token = False
check_quotes.name = 'abp-quotes'
check_quotes.version = __version__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment