Last active
April 21, 2016 14:00
-
-
Save snoack/d552a18a4e193a265c3726ff9fd1966c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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