Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(82)

Side by Side Diff: third_party/pylint/checkers/classes.py

Issue 10447014: Add pylint to depot_tools. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix unittests. Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/pylint/checkers/base.py ('k') | third_party/pylint/checkers/design_analysis.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr
3 #
4 # This program is free software; you can redistribute it and/or modify it under
5 # the terms of the GNU General Public License as published by the Free Software
6 # Foundation; either version 2 of the License, or (at your option) any later
7 # version.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along with
14 # this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16 """classes checker for Python code
17 """
18 from __future__ import generators
19
20 from logilab import astng
21 from logilab.astng import YES, Instance, are_exclusive
22
23 from pylint.interfaces import IASTNGChecker
24 from pylint.checkers import BaseChecker
25 from pylint.checkers.utils import PYMETHODS, overrides_a_method, check_messages
26
27 def class_is_abstract(node):
28 """return true if the given class node should be considered as an abstract
29 class
30 """
31 for method in node.methods():
32 if method.parent.frame() is node:
33 if method.is_abstract(pass_is_abstract=False):
34 return True
35 return False
36
37
38 MSGS = {
39 'F0202': ('Unable to check methods signature (%s / %s)',
40 'Used when PyLint has been unable to check methods signature \
41 compatibility for an unexpected reason. Please report this kind \
42 if you don\'t make sense of it.'),
43
44 'E0202': ('An attribute affected in %s line %s hide this method',
45 'Used when a class defines a method which is hidden by an '
46 'instance attribute from an ancestor class or set by some '
47 'client code.'),
48 'E0203': ('Access to member %r before its definition line %s',
49 'Used when an instance member is accessed before it\'s actually\
50 assigned.'),
51 'W0201': ('Attribute %r defined outside __init__',
52 'Used when an instance attribute is defined outside the __init__\
53 method.'),
54
55 'W0212': ('Access to a protected member %s of a client class', # E0214
56 'Used when a protected member (i.e. class member with a name \
57 beginning with an underscore) is access outside the class or a \
58 descendant of the class where it\'s defined.'),
59
60 'E0211': ('Method has no argument',
61 'Used when a method which should have the bound instance as \
62 first argument has no argument defined.'),
63 'E0213': ('Method should have "self" as first argument',
64 'Used when a method has an attribute different the "self" as\
65 first argument. This is considered as an error since this is\
66 a so common convention that you shouldn\'t break it!'),
67 'C0202': ('Class method should have %s as first argument', # E0212
68 'Used when a class method has an attribute different than "cls"\
69 as first argument, to easily differentiate them from regular \
70 instance methods.'),
71 'C0203': ('Metaclass method should have "mcs" as first argument', # E0214
72 'Used when a metaclass method has an attribute different the \
73 "mcs" as first argument.'),
74
75 'W0211': ('Static method with %r as first argument',
76 'Used when a static method has "self" or "cls" as first argument.'
77 ),
78 'R0201': ('Method could be a function',
79 'Used when a method doesn\'t use its bound instance, and so could\
80 be written as a function.'
81 ),
82
83 'E0221': ('Interface resolved to %s is not a class',
84 'Used when a class claims to implement an interface which is not \
85 a class.'),
86 'E0222': ('Missing method %r from %s interface',
87 'Used when a method declared in an interface is missing from a \
88 class implementing this interface'),
89 'W0221': ('Arguments number differs from %s method',
90 'Used when a method has a different number of arguments than in \
91 the implemented interface or in an overridden method.'),
92 'W0222': ('Signature differs from %s method',
93 'Used when a method signature is different than in the \
94 implemented interface or in an overridden method.'),
95 'W0223': ('Method %r is abstract in class %r but is not overridden',
96 'Used when an abstract method (i.e. raise NotImplementedError) is \
97 not overridden in concrete class.'
98 ),
99 'F0220': ('failed to resolve interfaces implemented by %s (%s)', # W0224
100 'Used when a PyLint as failed to find interfaces implemented by \
101 a class'),
102
103
104 'W0231': ('__init__ method from base class %r is not called',
105 'Used when an ancestor class method has an __init__ method \
106 which is not called by a derived class.'),
107 'W0232': ('Class has no __init__ method',
108 'Used when a class has no __init__ method, neither its parent \
109 classes.'),
110 'W0233': ('__init__ method from a non direct base class %r is called',
111 'Used when an __init__ method is called on a class which is not \
112 in the direct ancestors for the analysed class.'),
113
114 }
115
116
117 class ClassChecker(BaseChecker):
118 """checks for :
119 * methods without self as first argument
120 * overridden methods signature
121 * access only to existent members via self
122 * attributes not defined in the __init__ method
123 * supported interfaces implementation
124 * unreachable code
125 """
126
127 __implements__ = (IASTNGChecker,)
128
129 # configuration section name
130 name = 'classes'
131 # messages
132 msgs = MSGS
133 priority = -2
134 # configuration options
135 options = (('ignore-iface-methods',
136 {'default' : (#zope interface
137 'isImplementedBy', 'deferred', 'extends', 'names',
138 'namesAndDescriptions', 'queryDescriptionFor', 'getBases',
139 'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue',
140 'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue',
141 'isImplementedByInstancesOf',
142 # twisted
143 'adaptWith',
144 # logilab.common interface
145 'is_implemented_by'),
146 'type' : 'csv',
147 'metavar' : '<method names>',
148 'help' : 'List of interface methods to ignore, \
149 separated by a comma. This is used for instance to not check methods defines \
150 in Zope\'s Interface base class.'}
151 ),
152
153 ('defining-attr-methods',
154 {'default' : ('__init__', '__new__', 'setUp'),
155 'type' : 'csv',
156 'metavar' : '<method names>',
157 'help' : 'List of method names used to declare (i.e. assign) \
158 instance attributes.'}
159 ),
160 ('valid-classmethod-first-arg',
161 {'default' : ('cls',),
162 'type' : 'csv',
163 'metavar' : '<argument names>',
164 'help' : 'List of valid names for the first argument in \
165 a class method.'}
166 ),
167
168 )
169
170 def __init__(self, linter=None):
171 BaseChecker.__init__(self, linter)
172 self._accessed = []
173 self._first_attrs = []
174 self._meth_could_be_func = None
175
176 def visit_class(self, node):
177 """init visit variable _accessed and check interfaces
178 """
179 self._accessed.append({})
180 self._check_bases_classes(node)
181 self._check_interfaces(node)
182 # if not an interface, exception, metaclass
183 if node.type == 'class':
184 try:
185 node.local_attr('__init__')
186 except astng.NotFoundError:
187 self.add_message('W0232', args=node, node=node)
188
189 @check_messages('E0203', 'W0201')
190 def leave_class(self, cnode):
191 """close a class node:
192 check that instance attributes are defined in __init__ and check
193 access to existent members
194 """
195 # check access to existent members on non metaclass classes
196 accessed = self._accessed.pop()
197 if cnode.type != 'metaclass':
198 self._check_accessed_members(cnode, accessed)
199 # checks attributes are defined in an allowed method such as __init__
200 if 'W0201' not in self.active_msgs:
201 return
202 defining_methods = self.config.defining_attr_methods
203 for attr, nodes in cnode.instance_attrs.items():
204 nodes = [n for n in nodes if not
205 isinstance(n.statement(), (astng.Delete, astng.AugAssign))]
206 if not nodes:
207 continue # error detected by typechecking
208 attr_defined = False
209 # check if any method attr is defined in is a defining method
210 for node in nodes:
211 if node.frame().name in defining_methods:
212 attr_defined = True
213 if not attr_defined:
214 # check attribute is defined in a parent's __init__
215 for parent in cnode.instance_attr_ancestors(attr):
216 attr_defined = False
217 # check if any parent method attr is defined in is a definin g method
218 for node in parent.instance_attrs[attr]:
219 if node.frame().name in defining_methods:
220 attr_defined = True
221 if attr_defined:
222 # we're done :)
223 break
224 else:
225 # check attribute is defined as a class attribute
226 try:
227 cnode.local_attr(attr)
228 except astng.NotFoundError:
229 self.add_message('W0201', args=attr, node=node)
230
231 def visit_function(self, node):
232 """check method arguments, overriding"""
233 # ignore actual functions
234 if not node.is_method():
235 return
236 klass = node.parent.frame()
237 self._meth_could_be_func = True
238 # check first argument is self if this is actually a method
239 self._check_first_arg_for_type(node, klass.type == 'metaclass')
240 if node.name == '__init__':
241 self._check_init(node)
242 return
243 # check signature if the method overloads inherited method
244 for overridden in klass.local_attr_ancestors(node.name):
245 # get astng for the searched method
246 try:
247 meth_node = overridden[node.name]
248 except KeyError:
249 # we have found the method but it's not in the local
250 # dictionary.
251 # This may happen with astng build from living objects
252 continue
253 if not isinstance(meth_node, astng.Function):
254 continue
255 self._check_signature(node, meth_node, 'overridden')
256 break
257 # check if the method overload an attribute
258 try:
259 overridden = klass.instance_attr(node.name)[0] # XXX
260 args = (overridden.root().name, overridden.fromlineno)
261 self.add_message('E0202', args=args, node=node)
262 except astng.NotFoundError:
263 pass
264
265 def leave_function(self, node):
266 """on method node, check if this method couldn't be a function
267
268 ignore class, static and abstract methods, initializer,
269 methods overridden from a parent class and any
270 kind of method defined in an interface for this warning
271 """
272 if node.is_method():
273 if node.args.args is not None:
274 self._first_attrs.pop()
275 if 'R0201' not in self.active_msgs:
276 return
277 class_node = node.parent.frame()
278 if (self._meth_could_be_func and node.type == 'method'
279 and not node.name in PYMETHODS
280 and not (node.is_abstract() or
281 overrides_a_method(class_node, node.name))
282 and class_node.type != 'interface'):
283 self.add_message('R0201', node=node)
284
285 def visit_getattr(self, node):
286 """check if the getattr is an access to a class member
287 if so, register it. Also check for access to protected
288 class member from outside its class (but ignore __special__
289 methods)
290 """
291 attrname = node.attrname
292 if self._first_attrs and isinstance(node.expr, astng.Name) and \
293 node.expr.name == self._first_attrs[-1]:
294 self._accessed[-1].setdefault(attrname, []).append(node)
295 return
296 if 'W0212' not in self.active_msgs:
297 return
298 if attrname[0] == '_' and not attrname == '_' and not (
299 attrname.startswith('__') and attrname.endswith('__')):
300 # XXX move this in a reusable function
301 klass = node.frame()
302 while klass is not None and not isinstance(klass, astng.Class):
303 if klass.parent is None:
304 klass = None
305 else:
306 klass = klass.parent.frame()
307 # XXX infer to be more safe and less dirty ??
308 # in classes, check we are not getting a parent method
309 # through the class object or through super
310 callee = node.expr.as_string()
311 if klass is None or not (callee == klass.name or
312 callee in klass.basenames
313 or (isinstance(node.expr, astng.CallFunc)
314 and isinstance(node.expr.func, astng.Name)
315 and node.expr.func.name == 'super')):
316 self.add_message('W0212', node=node, args=attrname)
317
318
319 def visit_name(self, node):
320 """check if the name handle an access to a class member
321 if so, register it
322 """
323 if self._first_attrs and (node.name == self._first_attrs[-1] or
324 not self._first_attrs[-1]):
325 self._meth_could_be_func = False
326
327 def _check_accessed_members(self, node, accessed):
328 """check that accessed members are defined"""
329 # XXX refactor, probably much simpler now that E0201 is in type checker
330 for attr, nodes in accessed.items():
331 # deactivate "except doesn't do anything", that's expected
332 # pylint: disable=W0704
333 # is it a class attribute ?
334 try:
335 node.local_attr(attr)
336 # yes, stop here
337 continue
338 except astng.NotFoundError:
339 pass
340 # is it an instance attribute of a parent class ?
341 try:
342 node.instance_attr_ancestors(attr).next()
343 # yes, stop here
344 continue
345 except StopIteration:
346 pass
347 # is it an instance attribute ?
348 try:
349 defstmts = node.instance_attr(attr)
350 except astng.NotFoundError:
351 pass
352 else:
353 if len(defstmts) == 1:
354 defstmt = defstmts[0]
355 # check that if the node is accessed in the same method as
356 # it's defined, it's accessed after the initial assignment
357 frame = defstmt.frame()
358 lno = defstmt.fromlineno
359 for _node in nodes:
360 if _node.frame() is frame and _node.fromlineno < lno \
361 and not are_exclusive(_node.statement(), defstmt, ('A ttributeError', 'Exception', 'BaseException')):
362 self.add_message('E0203', node=_node,
363 args=(attr, lno))
364
365 def _check_first_arg_for_type(self, node, metaclass=0):
366 """check the name of first argument, expect:
367
368 * 'self' for a regular method
369 * 'cls' for a class method
370 * 'mcs' for a metaclass
371 * not one of the above for a static method
372 """
373 # don't care about functions with unknown argument (builtins)
374 if node.args.args is None:
375 return
376 first_arg = node.args.args and node.argnames()[0]
377 self._first_attrs.append(first_arg)
378 first = self._first_attrs[-1]
379 # static method
380 if node.type == 'staticmethod':
381 if first_arg in ('self', 'cls', 'mcs'):
382 self.add_message('W0211', args=first, node=node)
383 self._first_attrs[-1] = None
384 # class / regular method with no args
385 elif not node.args.args:
386 self.add_message('E0211', node=node)
387 # metaclass method
388 elif metaclass:
389 if first != 'mcs':
390 self.add_message('C0203', node=node)
391 # class method
392 elif node.type == 'classmethod':
393 if first not in self.config.valid_classmethod_first_arg:
394 if len(self.config.valid_classmethod_first_arg) == 1:
395 valid = repr(self.config.valid_classmethod_first_arg[0])
396 else:
397 valid = ', '.join(
398 repr(v)
399 for v in self.config.valid_classmethod_first_arg[:-1])
400 valid = '%s or %r' % (
401 valid, self.config.valid_classmethod_first_arg[-1])
402 self.add_message('C0202', args=valid, node=node)
403 # regular method without self as argument
404 elif first != 'self':
405 self.add_message('E0213', node=node)
406
407 def _check_bases_classes(self, node):
408 """check that the given class node implements abstract methods from
409 base classes
410 """
411 # check if this class abstract
412 if class_is_abstract(node):
413 return
414 for method in node.methods():
415 owner = method.parent.frame()
416 if owner is node:
417 continue
418 # owner is not this class, it must be a parent class
419 # check that the ancestor's method is not abstract
420 if method.is_abstract(pass_is_abstract=False):
421 self.add_message('W0223', node=node,
422 args=(method.name, owner.name))
423
424 def _check_interfaces(self, node):
425 """check that the given class node really implements declared
426 interfaces
427 """
428 e0221_hack = [False]
429 def iface_handler(obj):
430 """filter interface objects, it should be classes"""
431 if not isinstance(obj, astng.Class):
432 e0221_hack[0] = True
433 self.add_message('E0221', node=node,
434 args=(obj.as_string(),))
435 return False
436 return True
437 ignore_iface_methods = self.config.ignore_iface_methods
438 try:
439 for iface in node.interfaces(handler_func=iface_handler):
440 for imethod in iface.methods():
441 name = imethod.name
442 if name.startswith('_') or name in ignore_iface_methods:
443 # don't check method beginning with an underscore,
444 # usually belonging to the interface implementation
445 continue
446 # get class method astng
447 try:
448 method = node_method(node, name)
449 except astng.NotFoundError:
450 self.add_message('E0222', args=(name, iface.name),
451 node=node)
452 continue
453 # ignore inherited methods
454 if method.parent.frame() is not node:
455 continue
456 # check signature
457 self._check_signature(method, imethod,
458 '%s interface' % iface.name)
459 except astng.InferenceError:
460 if e0221_hack[0]:
461 return
462 implements = Instance(node).getattr('__implements__')[0]
463 assignment = implements.parent
464 assert isinstance(assignment, astng.Assign)
465 # assignment.expr can be a Name or a Tuple or whatever.
466 # Use as_string() for the message
467 # FIXME: in case of multiple interfaces, find which one could not
468 # be resolved
469 self.add_message('F0220', node=implements,
470 args=(node.name, assignment.value.as_string()))
471
472 def _check_init(self, node):
473 """check that the __init__ method call super or ancestors'__init__
474 method
475 """
476 if not set(('W0231', 'W0233')) & self.active_msgs:
477 return
478 klass_node = node.parent.frame()
479 to_call = _ancestors_to_call(klass_node)
480 not_called_yet = dict(to_call)
481 for stmt in node.nodes_of_class(astng.CallFunc):
482 expr = stmt.func
483 if not isinstance(expr, astng.Getattr) \
484 or expr.attrname != '__init__':
485 continue
486 # skip the test if using super
487 if isinstance(expr.expr, astng.CallFunc) and \
488 isinstance(expr.expr.func, astng.Name) and \
489 expr.expr.func.name == 'super':
490 return
491 try:
492 klass = expr.expr.infer().next()
493 if klass is YES:
494 continue
495 try:
496 del not_called_yet[klass]
497 except KeyError:
498 if klass not in to_call:
499 self.add_message('W0233', node=expr, args=klass.name)
500 except astng.InferenceError:
501 continue
502 for klass in not_called_yet.keys():
503 if klass.name == 'object':
504 continue
505 self.add_message('W0231', args=klass.name, node=node)
506
507 def _check_signature(self, method1, refmethod, class_type):
508 """check that the signature of the two given methods match
509
510 class_type is in 'class', 'interface'
511 """
512 if not (isinstance(method1, astng.Function)
513 and isinstance(refmethod, astng.Function)):
514 self.add_message('F0202', args=(method1, refmethod), node=method1)
515 return
516 # don't care about functions with unknown argument (builtins)
517 if method1.args.args is None or refmethod.args.args is None:
518 return
519 # if we use *args, **kwargs, skip the below checks
520 if method1.args.vararg or method1.args.kwarg:
521 return
522 if len(method1.args.args) != len(refmethod.args.args):
523 self.add_message('W0221', args=class_type, node=method1)
524 elif len(method1.args.defaults) < len(refmethod.args.defaults):
525 self.add_message('W0222', args=class_type, node=method1)
526
527
528 def _ancestors_to_call(klass_node, method='__init__'):
529 """return a dictionary where keys are the list of base classes providing
530 the queried method, and so that should/may be called from the method node
531 """
532 to_call = {}
533 for base_node in klass_node.ancestors(recurs=False):
534 try:
535 base_node.local_attr(method)
536 to_call[base_node] = 1
537 except astng.NotFoundError:
538 continue
539 return to_call
540
541
542 def node_method(node, method_name):
543 """get astng for <method_name> on the given class node, ensuring it
544 is a Function node
545 """
546 for n in node.local_attr(method_name):
547 if isinstance(n, astng.Function):
548 return n
549 raise astng.NotFoundError(method_name)
550
551 def register(linter):
552 """required method to auto register this checker """
553 linter.register_checker(ClassChecker(linter))
OLDNEW
« no previous file with comments | « third_party/pylint/checkers/base.py ('k') | third_party/pylint/checkers/design_analysis.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698