| 1 | |
|---|
| 2 | """Pylint plugin checking for trailing whitespace.""" |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | import sys |
|---|
| 6 | import __builtin__ |
|---|
| 7 | |
|---|
| 8 | from pylint import interfaces, checkers |
|---|
| 9 | from logilab.astng import (nodes, raw_building, utils, |
|---|
| 10 | Name, Getattr, CallFunc, And, Or, Node) |
|---|
| 11 | |
|---|
| 12 | from snakeoil.lists import iflatten_func |
|---|
| 13 | |
|---|
| 14 | builtins = tuple(x for x in dir(__builtin__) if x[0].islower()) |
|---|
| 15 | ignore_shadowing = ('all', 'any') |
|---|
| 16 | |
|---|
| 17 | class SnakeoilChecker(checkers.BaseChecker): |
|---|
| 18 | |
|---|
| 19 | __implements__ = (interfaces.IRawChecker, interfaces.IASTNGChecker) |
|---|
| 20 | |
|---|
| 21 | name = 'snakeoil' |
|---|
| 22 | |
|---|
| 23 | # XXX move some of those over to RewriteDemandload somehow |
|---|
| 24 | # (current monkey patch running the rewriter does not support that) |
|---|
| 25 | |
|---|
| 26 | msgs = { |
|---|
| 27 | 'CPC01': ('line too long: length %d', |
|---|
| 28 | 'More complete version of the standard line too long check.'), |
|---|
| 29 | 'CPC02': ('trailing whitespace', 'trailing whitespace sucks.'), |
|---|
| 30 | 'WPC01': ('demandload with arglen < 2 ignored', |
|---|
| 31 | 'A call which is probably a demandload has too little' |
|---|
| 32 | 'arguments.'), |
|---|
| 33 | 'WPC02': ('demandload with non-string-constant arg ignored', |
|---|
| 34 | 'A call which is probably a demandload has a second arg ' |
|---|
| 35 | 'that is not a string constant. Fix the code to cooperate ' |
|---|
| 36 | 'with the dumb checker.'), |
|---|
| 37 | 'WPC03': ('old-style demandload call', |
|---|
| 38 | 'A call which uses the old way of callling demandload,' |
|---|
| 39 | 'with spaces.'), |
|---|
| 40 | 'WPC04': ('non new-style class', |
|---|
| 41 | 'All classes should be new-style classes.'), |
|---|
| 42 | 'WPC05': ('raise statement with two args', |
|---|
| 43 | 'A raise statement which has a message raised with the ' |
|---|
| 44 | 'exception instead of as an argument to the exception ' |
|---|
| 45 | 'class.'), |
|---|
| 46 | 'WPC06': ('raise of Exception base class', |
|---|
| 47 | 'A raise statement in which Exception is raised- make a ' |
|---|
| 48 | 'subclass of it and raise that instead.'), |
|---|
| 49 | 'WPC07': ('Shadowing builtin %s', |
|---|
| 50 | 'Assignment to a builtin name'), |
|---|
| 51 | 'WPC08': ('Iterating over dict.keys()', |
|---|
| 52 | 'Iterating over dict.keys()- use `for x in dict` or ' |
|---|
| 53 | '`for x in d.iteritems()` if you need the vals too'), |
|---|
| 54 | 'WPC09': ('Usage of bool(len(seq))', |
|---|
| 55 | 'Testing for emptiness with len(seq) instead of bool(seq)'), |
|---|
| 56 | } |
|---|
| 57 | |
|---|
| 58 | def process_module(self, stream): |
|---|
| 59 | for linenr, line in enumerate(stream): |
|---|
| 60 | line = line.rstrip('\r\n') |
|---|
| 61 | if len(line) > 80: |
|---|
| 62 | self.add_message('CPC01', linenr, args=len(line)) |
|---|
| 63 | if line.endswith(' ') or line.endswith('\t'): |
|---|
| 64 | self.add_message('CPC02', linenr) |
|---|
| 65 | |
|---|
| 66 | def visit_class(self, node): |
|---|
| 67 | if not node.bases: |
|---|
| 68 | self.add_message('WPC04', node=node) |
|---|
| 69 | |
|---|
| 70 | def visit_raise(self, node): |
|---|
| 71 | if node.expr2 is not None: |
|---|
| 72 | self.add_message('WPC05', node=node) |
|---|
| 73 | expr = node.expr1 |
|---|
| 74 | if expr is not None and isinstance(expr, Name) and \ |
|---|
| 75 | expr.name == "Exception": |
|---|
| 76 | self.add_message('WPC06', node=node) |
|---|
| 77 | |
|---|
| 78 | def visit_assname(self, node): |
|---|
| 79 | if node.name in builtins and node.name not in ignore_shadowing: |
|---|
| 80 | self.add_message('WPC07', args=node.name, node=node) |
|---|
| 81 | |
|---|
| 82 | def visit_for(self, node): |
|---|
| 83 | expr = node.getChildNodes()[1] |
|---|
| 84 | if isinstance(expr, CallFunc): |
|---|
| 85 | expr = expr.getChildNodes()[0] |
|---|
| 86 | if isinstance(expr, Getattr) and expr.attrname == 'keys': |
|---|
| 87 | self.add_message('WPC08', node=node) |
|---|
| 88 | |
|---|
| 89 | def visit_if(self, node): |
|---|
| 90 | f = lambda node: \ |
|---|
| 91 | isinstance(node, basestring) or \ |
|---|
| 92 | node is None or \ |
|---|
| 93 | (isinstance(node, Node) and isinstance(node.parent, (Or, |
|---|
| 94 | And))) |
|---|
| 95 | |
|---|
| 96 | for expr in iflatten_func(node.getChildNodes()[0], f): |
|---|
| 97 | if isinstance(expr, CallFunc): |
|---|
| 98 | expr = expr.getChildNodes()[0] |
|---|
| 99 | if isinstance(expr, Name) and expr.name == 'len': |
|---|
| 100 | self.add_message('WPC09', node=node) |
|---|
| 101 | |
|---|
| 102 | |
|---|
| 103 | class RewriteDemandload(utils.ASTWalker): |
|---|
| 104 | |
|---|
| 105 | def __init__(self, linter): |
|---|
| 106 | utils.ASTWalker.__init__(self, self) |
|---|
| 107 | self.linter = linter |
|---|
| 108 | |
|---|
| 109 | def visit_callfunc(self, node): |
|---|
| 110 | """Hack fake imports into the tree after demandload calls.""" |
|---|
| 111 | # XXX inaccurate hack |
|---|
| 112 | if not node.node.as_string().endswith('demandload'): |
|---|
| 113 | return |
|---|
| 114 | # sanity check. |
|---|
| 115 | if len(node.args) < 2: |
|---|
| 116 | self.linter.add_message('WPC01', node=node) |
|---|
| 117 | return |
|---|
| 118 | if not isinstance(node.args[1], nodes.Const): |
|---|
| 119 | self.linter.add_message('WPC02', node=node) |
|---|
| 120 | return |
|---|
| 121 | if node.args[1].value.find(" ") != -1: |
|---|
| 122 | self.linter.add_message('WPC03', node=node) |
|---|
| 123 | return |
|---|
| 124 | for mod in (module.value for module in node.args[1:]): |
|---|
| 125 | if not isinstance(mod, str): |
|---|
| 126 | self.linter.add_message('WPC02', node=node) |
|---|
| 127 | continue |
|---|
| 128 | col = mod.find(':') |
|---|
| 129 | if col == -1: |
|---|
| 130 | # Argument to Import probably works like this: |
|---|
| 131 | # "import foo, foon as spork" is |
|---|
| 132 | # nodes.Import([('foo', None), ('foon', 'spork')]) |
|---|
| 133 | # (not entirely sure though, have not found documentation. |
|---|
| 134 | # The asname/importedname might be the other way around fex). |
|---|
| 135 | newstuff = nodes.Import([(mod, None)]) |
|---|
| 136 | newstuff.fromlineno = node.fromlineno |
|---|
| 137 | raw_building._attach_local_node(node.frame(), newstuff, mod) |
|---|
| 138 | else: |
|---|
| 139 | for name in mod[col+1:].split(','): |
|---|
| 140 | if sys.version_info < (2, 5): |
|---|
| 141 | newstuff = nodes.From(mod[:col], ((name, None),)) |
|---|
| 142 | else: |
|---|
| 143 | newstuff = nodes.From(mod[:col], ((name, None),), 0) |
|---|
| 144 | newstuff.fromlineno = 1 |
|---|
| 145 | raw_building._attach_local_node(node.frame(), newstuff, |
|---|
| 146 | name) |
|---|
| 147 | |
|---|
| 148 | |
|---|
| 149 | def register(linter): |
|---|
| 150 | """Required method to get our checker registered.""" |
|---|
| 151 | |
|---|
| 152 | rewriter = RewriteDemandload(linter) |
|---|
| 153 | # XXX HACK: monkeypatch the linter to transform the tree before |
|---|
| 154 | # the astng checkers get at it. |
|---|
| 155 | # |
|---|
| 156 | # Why do we do this? Because a whole bunch of places work with |
|---|
| 157 | # copies of astng data, not the data itself, by the time a normal |
|---|
| 158 | # checker runs it is too late to manipulate the data reliably. And |
|---|
| 159 | # pylint does not provide a hook that gets run at the right point |
|---|
| 160 | # to do this tree rewriting. So we monkeypatch in the hook. |
|---|
| 161 | # |
|---|
| 162 | # Ideally we would do something like |
|---|
| 163 | # |
|---|
| 164 | # linter.register_preprocessor(rewriter) |
|---|
| 165 | # |
|---|
| 166 | # and the linter would call walk(astng) on everything registered |
|---|
| 167 | # that way before the IASTNGCheckers run (not sure if it should be |
|---|
| 168 | # before or after the raw checkers run, probably does not matter). |
|---|
| 169 | # Perhaps give those preprocessors a priority attribute too. |
|---|
| 170 | # Definitely give them a msgs attribute. |
|---|
| 171 | |
|---|
| 172 | original_check_astng_module = linter.check_astng_module |
|---|
| 173 | def snakeoil_check_astng_module(astng, checkers): |
|---|
| 174 | rewriter.walk(astng) |
|---|
| 175 | return original_check_astng_module(astng, checkers) |
|---|
| 176 | linter.check_astng_module = snakeoil_check_astng_module |
|---|
| 177 | |
|---|
| 178 | linter.register_checker(SnakeoilChecker(linter)) |
|---|
| 179 | |
|---|