Skip to content

Instantly share code, notes, and snippets.

@snoack
Last active April 21, 2016 14:00
Show Gist options
  • Save snoack/d552a18a4e193a265c3726ff9fd1966c to your computer and use it in GitHub Desktop.
Save snoack/d552a18a4e193a265c3726ff9fd1966c 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()',
}
class TreeVisitor(ast.NodeVisitor):
def __init__(self):
self.errors = []
self.stack = []
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, or use the '
'+ operator when concatenating '
'just two strings'))
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 multiple strings'))
self.generic_visit(node)
def visit_For(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)
visit_comprehension = visit_For
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.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):
if node.orelse and isinstance(node.body[-1], (ast.Return,
ast.Continue,
ast.Break)):
self.errors.append((node, 'A151 redundant else statement'))
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 = literal.replace('\\', '\\\\')
literal = literal.replace('\\\\' + literal[0], '\\')
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