diff options
-rw-r--r-- | ChangeLog | 32 | ||||
-rw-r--r-- | checkers/__init__.py | 14 | ||||
-rw-r--r-- | checkers/base.py | 27 | ||||
-rw-r--r-- | checkers/classes.py | 9 | ||||
-rw-r--r-- | checkers/design_analysis.py | 22 | ||||
-rw-r--r-- | checkers/exceptions.py | 34 | ||||
-rw-r--r-- | checkers/format.py | 11 | ||||
-rw-r--r-- | checkers/imports.py | 6 | ||||
-rw-r--r-- | checkers/misc.py | 2 | ||||
-rw-r--r-- | checkers/newstyle.py | 8 | ||||
-rw-r--r-- | checkers/similar.py | 17 | ||||
-rw-r--r-- | checkers/typecheck.py | 116 | ||||
-rw-r--r-- | checkers/utils.py | 42 | ||||
-rw-r--r-- | doc/makefile | 7 | ||||
-rw-r--r-- | lint.py | 196 | ||||
-rw-r--r-- | reporters/html.py | 11 | ||||
-rw-r--r-- | reporters/text.py | 24 | ||||
-rw-r--r-- | setup.py | 5 | ||||
-rw-r--r-- | test/func_test.py | 2 | ||||
-rw-r--r-- | test/input/func_base_stmt_without_effect.py | 12 | ||||
-rw-r--r-- | test/input/func_block_disable_msg.py | 890 | ||||
-rw-r--r-- | test/messages/func_base_stmt_without_effect.txt | 9 | ||||
-rw-r--r-- | test/messages/func_dotted_ancestor.txt | 2 | ||||
-rw-r--r-- | test/messages/func_r0903.txt | 2 | ||||
-rw-r--r-- | test/messages/func_w0302.txt | 2 | ||||
-rw-r--r-- | utils.py | 90 |
26 files changed, 1296 insertions, 296 deletions
@@ -2,10 +2,32 @@ ChangeLog for PyLint ==================== -- - * new W0212 message for access to protected member from client code - * new W0105 message extracted from W0104 (statement seems to have no effect) - with string + * usability changes: + - parseable, html and color options are now handled by a single + output-format option + - enable-<checkerid> and disable-all options are now handled by + two (exclusive) enable-checker and disable-checker options + taking a comma separated list of checker names as value + - renamed debug-mode option to errors-only + * started a reference user manual + + * new W0212 message for access to protected member from client code + (close #14081) + + * new W0105 and W0106 messages extracted from W0104 (statement seems + to have no effect) respectivly when the statement is actually string + (that's sometimes used instead of comments for documentation) or an + empty statement generated by a useless semicolumn + + * reclassified W0302 to C0302 + + * fix so that global messages are not anymore connected to the last + analyzed module (close #10106) + * fix some bugs related to local disabling of messages + * fix cr/lf pb when generating the rc file on windows platforms + + 2006-04-19 -- 0.11.0 * fix crash caused by the exceptions checker in some case * fix some E1101 false positive with abstract method or classes defining @@ -18,8 +40,8 @@ ChangeLog for PyLint * added an option in the similarity checker to ignore docstrings, enabled by default - * included patch Benjamin Niemann to allow block level enabling/disabling - of messages + * included patch from Benjamin Niemann to allow block level + enabling/disabling of messages 2006-03-06 -- 0.10.0 diff --git a/checkers/__init__.py b/checkers/__init__.py index fdf015a..de8f6fa 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -26,6 +26,7 @@ Base id of standard checkers (used in msg and report ids): 08: similar 09: design_analysis 10: newstyle +11: typecheck """ __revision__ = "$Id: __init__.py,v 1.21 2005-11-21 23:08:11 syt Exp $" @@ -75,13 +76,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): """ ASTWalker.__init__(self, self) self.name = self.name.lower() - if self.may_be_disabled: - opt_name = 'enable-' + self.name - self.options = ( - (opt_name, - {'type' : 'yn', 'default' : 1, 'metavar': '<y_or_n>', - 'help' : "Enable / disable this checker"}) - ,) + self.options OptionsProviderMixIn.__init__(self) self.linter = linter @@ -92,14 +86,14 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): def is_enabled(self): """return true if the checker is enabled""" opt = 'enable_' + self.name - return getattr(self.config, opt, 1) + return getattr(self.config, opt, True) def enable(self, enable): """enable / disable this checker if true / false is given it false values has no effect if the checker can't be disabled """ - if self.may_be_disabled: + if enable or self.may_be_disabled: setattr(self.config, 'enable_' + self.name, enable) def package_dir(self): diff --git a/checkers/base.py b/checkers/base.py index 8b103af..d8d0a9e 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -40,6 +40,7 @@ NO_REQUIRED_DOC_RGX = re.compile('__.*__') del re def in_loop(node): + """return True if the node is inside a kind of for loop""" parent = node.parent while parent is not None: if isinstance(parent, (astng.For, astng.ListComp, astng.GenExpr)): @@ -118,6 +119,9 @@ MSGS = { has no effect). This is a particular case of W0104 with its \ own message so you can easily disable it if you\'re using \ those strings as documentation, instead of comments.'), + 'W0106': ('Unnecessary semi-column', + 'Used when a statement is endend by a semi-colon (";"), which \ + isn\'t necessary (that\'s python, not C ;)'), 'W0122': ('Use of the exec statement', 'Used when you use the "exec" statement, to discourage its \ @@ -308,11 +312,24 @@ functions, methods self._check_name('attr', attr, anodes[0]) def visit_discard(self, node): - """check for statement without effect""" - if isinstance(node.expr, astng.Const) and \ - isinstance(node.expr.value, basestring): - self.add_message('W0105', node=node) - elif not isinstance(node.expr, astng.CallFunc): + """check for various kind of statements without effect""" + expr = node.expr + if isinstance(node.expr, astng.Const): + # XXX lineno maybe dynamically set incidently + if expr.value is None and expr.lineno is None: + # const None node with lineno to None are inserted + # on unnecessary semi-column + # XXX navigate to get a correct lineno + brothers = list(node.parent.getChildNodes()) + previoussibling = brothers[brothers.index(node)-1] + self.add_message('W0106', node=previoussibling) + return + if isinstance(expr.value, basestring): + # tread string statement in a separated message + self.add_message('W0105', node=node) + return + # ignore if this is a function call (can't predicate side effects) + if not isinstance(node.expr, astng.CallFunc): self.add_message('W0104', node=node) def visit_function(self, node): diff --git a/checkers/classes.py b/checkers/classes.py index 3a9e468..c1da969 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -291,6 +291,8 @@ instance attributes.'} """check that accessed members are defined""" # XXX refactor, probably much simpler now that E0201 is in type checker for attr, nodes in accessed.items(): + # deactivate "except doesn't do anything", that's expected + # pylint: disable-msg=W0704 ## # is it a builtin attribute ? ## if attr in ('__dict__', '__class__', '__doc__'): ## # FIXME: old class object doesn't have __class__ @@ -312,9 +314,10 @@ instance attributes.'} # is it an instance attribute ? try: def_nodes = node.instance_attr(attr) # XXX - instance_attribute = True + #instance_attribute = True except astng.NotFoundError: - instance_attribute = False + pass + #instance_attribute = False else: if len(def_nodes) == 1: def_node = def_nodes[0] @@ -443,7 +446,7 @@ instance attributes.'} del to_call[klass] except KeyError: self.add_message('W0233', node=expr, args=klass.name) - except astng.InferenceError, ex: + except astng.InferenceError: continue for klass in to_call.keys(): if klass.name == 'object': diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py index 1d51e74..7623979 100644 --- a/checkers/design_analysis.py +++ b/checkers/design_analysis.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -40,18 +40,24 @@ def class_is_abstract(klass): MSGS = { 'R0901': ('Too many ancestors (%s/%s)', - 'Used when class has too many parent classes.'), + 'Used when class has too many parent classes, try to reduce \ + this to get a more simple (and so easier to use) class.'), 'R0902': ('Too many instance attributes (%s/%s)', - 'Used when class has too many instance attributes.'), - 'R0903': ('Not enough public methods (%s/%s)', - 'Used when class has not enough public methods.'), + 'Used when class has too many instance attributes, try to reduce \ + this to get a more simple (and so easier to use) class.'), + 'R0903': ('To few public methods (%s/%s)', + 'Used when class has to few public methods, so be sure it\'s \ + really worth it.'), 'R0904': ('Too many public methods (%s/%s)', - 'Used when class has too many public methods.'), + 'Used when class has too many public methods, try to reduce \ + this to get a more simple (and so easier to use) class.'), 'R0911': ('Too many return statements (%s/%s)', - 'Used when a function or method has too many return statement.'), + 'Used when a function or method has too many return statement, \ + making it hard to follow.'), 'R0912': ('Too many branches (%s/%s)', - 'Used when a function or method has too many branches.'), + 'Used when a function or method has too many branches, \ + making it hard to follow.'), 'R0913': ('Too many arguments (%s/%s)', 'Used when a function or method takes too many arguments.'), 'R0914': ('Too many local variables (%s/%s)', diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 0f5fddc..44510de 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -1,3 +1,5 @@ +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -10,10 +12,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr - -exceptions checkers for Python code +"""exceptions handling (raising, catching, exceptions classes) checker """ __revision__ = '$Id: exceptions.py,v 1.27 2006-03-08 15:53:42 syt Exp $' @@ -23,7 +22,7 @@ from logilab import astng from logilab.astng.inference import unpack_infer from pylint.checkers import BaseChecker -from pylint.checkers.utils import is_empty +from pylint.checkers.utils import is_empty, is_raising from pylint.interfaces import IASTNGChecker MSGS = { @@ -51,14 +50,6 @@ MSGS = { 'Used when a variable used to raise an exception is initially \ assigned to a value which can\'t be used as an exception.'), } - -def is_raising(stmt): - """return true if the given statement node raise an exception - """ - for node in stmt.nodes: - if isinstance(node, astng.Raise): - return 1 - return 0 class ExceptionsChecker(BaseChecker): """checks for @@ -97,14 +88,12 @@ class ExceptionsChecker(BaseChecker): value = unpack_infer(expr).next() except astng.InferenceError: return - if value is astng.YES: - return - # must to be carefull since Const, Dict, .. inherit from + # have to be careful since Const, Dict, .. inherit from # Instance now - if isinstance(value, (astng.Class, astng.Module)): - return - if isinstance(value, astng.Instance) and \ - isinstance(value._proxied, astng.Class): + if (value is astng.YES or + isinstance(value, (astng.Class, astng.Module)) or + (isinstance(value, astng.Instance) and + isinstance(value._proxied, astng.Class))): return if isinstance(value, astng.Const) and \ (value.value is None or @@ -145,10 +134,9 @@ class ExceptionsChecker(BaseChecker): except astng.InferenceError: continue for exc in excs: - if exc is astng.YES: + # XXX skip other non class nodes + if exc is astng.YES or not isinstance(exc, astng.Class): continue - if not isinstance(exc, astng.Class): - continue # XXX exc_ancestors = [anc for anc in exc.ancestors() if isinstance(anc, astng.Class)] for previous_exc in exceptions_classes: diff --git a/checkers/format.py b/checkers/format.py index 9cfd63d..cfbaf72 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -1,3 +1,5 @@ +# Copyright (c) 2003-2006 Sylvain Thenault (thenault@gmail.com). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -10,10 +12,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr - -Python code format's checker. +"""Python code format's checker. By default try to follow Guido's style guide : @@ -37,7 +36,7 @@ from pylint.checkers import BaseRawChecker MSGS = { 'C0301': ('Line too long (%s/%s)', 'Used when a line is longer than a given number of characters.'), - 'W0302': ('Too many lines in module (%s)', + 'C0302': ('Too many lines in module (%s)', # W0302 'Used when a module has too much lines, reducing its readibility.' ), @@ -248,7 +247,7 @@ class FormatChecker(BaseRawChecker): self.check_indent_level(line, indents[-1], line_num) if line_num > self.config.max_module_lines: - self.add_message('W0302', args=line_num) + self.add_message('C0302', args=line_num, line=0) def visit_default(self, node): """check the node line number and check it if not yet done diff --git a/checkers/imports.py b/checkers/imports.py index 6ca248a..8f9c972 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -228,8 +228,10 @@ given file (report R0402 must not be disabled)'} def close(self): """called before visiting project (i.e set of modules)""" - for cycle in get_cycles(self.import_graph): - self.add_message('R0401', args=' -> '.join(cycle)) + # don't try to compute cycles if the associated message is disabled + if self.linter.is_message_enabled('R0401'): + for cycle in get_cycles(self.import_graph): + self.add_message('R0401', args=' -> '.join(cycle)) def visit_import(self, node): """triggered when an import statement is seen""" diff --git a/checkers/misc.py b/checkers/misc.py index 7b59b95..9160645 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -80,7 +80,7 @@ class EncodingChecker(BaseChecker): {'type' : 'csv', 'metavar' : '<comma separated values>', 'default' : ('FIXME', 'XXX', 'TODO'), 'help' : 'List of note tags to take in consideration, \ -separated by a comma. Default to FIXME, XXX, TODO' +separated by a comma.' }), ) diff --git a/checkers/newstyle.py b/checkers/newstyle.py index 540b60f..7383a56 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -47,10 +47,10 @@ MSGS = { class NewStyleConflictChecker(BaseChecker): """checks for usage of new style capabilities on old style classes and - other new/old styles conflicts problems - * use of property, __slots__, super - * "super" usage - * raising a new style class as exception + other new/old styles conflicts problems + * use of property, __slots__, super + * "super" usage + * raising a new style class as exception """ __implements__ = (IASTNGChecker,) diff --git a/checkers/similar.py b/checkers/similar.py index f4b3a6c..903c19e 100644 --- a/checkers/similar.py +++ b/checkers/similar.py @@ -1,5 +1,5 @@ -# pylint: disable-msg=w0622 -# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE). +# pylint: disable-msg=W0622 +# Copyright (c) 2004-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -32,7 +32,8 @@ from pylint.checkers import BaseChecker, table_lines_from_stats class Similar: """finds copy-pasted lines of code in a project""" - def __init__(self, min_lines=4, ignore_comments=False, ignore_docstrings=False): + def __init__(self, min_lines=4, ignore_comments=False, + ignore_docstrings=False): self.min_lines = min_lines self.ignore_comments = ignore_comments self.ignore_docstrings = ignore_docstrings @@ -79,6 +80,7 @@ class Similar: couples.sort() for lineset, idx in couples: print "==%s:%s" % (lineset.name, idx) + # pylint: disable-msg=W0631 for line in lineset._real_lines[idx:idx+num]: print " ", line, nb_lignes_dupliquees += num * (len(couples)-1) @@ -141,16 +143,18 @@ def stripped_lines(lines, ignore_comments, ignore_docstrings): line = '' # XXX cut when a line begins with code but end with a comment if ignore_comments and line.startswith('#'): - line = '' + line = '' strippedlines.append(line) return strippedlines class LineSet: """Holds and indexes all the lines of a single source file""" - def __init__(self, name, lines, ignore_comments=False, ignore_docstrings=False): + def __init__(self, name, lines, ignore_comments=False, + ignore_docstrings=False): self.name = name self._real_lines = lines - self._stripped_lines = stripped_lines(lines, ignore_comments, ignore_docstrings) + self._stripped_lines = stripped_lines(lines, ignore_comments, + ignore_docstrings) self._index = self._mk_index() def __str__(self): @@ -282,6 +286,7 @@ class SimilarChecker(BaseChecker, Similar): for lineset, idx in couples: msg.append("==%s:%s" % (lineset.name, idx)) msg.sort() + # pylint: disable-msg=W0631 for line in lineset._real_lines[idx:idx+num]: msg.append(line.rstrip()) self.add_message('R0801', args=(len(couples), '\n'.join(msg))) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index e8c4355..4324f67 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -22,6 +22,7 @@ from logilab import astng from pylint.interfaces import IASTNGChecker from pylint.checkers import BaseChecker +from pylint.checkers.utils import safe_infer, is_super, display_type MSGS = { 'E1101': ('%s %r has no %r member', @@ -37,7 +38,6 @@ MSGS = { infered function returns nothing but None.'), } - class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ @@ -70,61 +70,64 @@ acquired-members option to ignore access to some undefined attributes.'} 'metavar' : '<members names>', 'help' : 'List of members which are usually get through \ zope\'s acquisition mecanism and so shouldn\'t trigger E0201 when accessed \ -(need zope=yes to be considered.'} +(need zope=yes to be considered).'} ), ) def visit_getattr(self, node): - """check that the accessed attribute exists""" - # if we are running in zope mode, is it an acquired attribute ? + """check that the accessed attribute exists + + to avoid to much false positives for now, we'll consider the code as + correct if a single of the infered nodes has the accessed attribute. + + function/method, super call and metaclasses are ignored + """ + # if we are running in zope mode and this is an acquired attribute, + # stop there if self.config.zope and node.attrname in self.config.acquired_members: return try: infered = list(node.expr.infer()) - for owner in infered: - # skip yes object - if owner is astng.YES: - continue - # if there is ambiguity, skip None - if len(infered) > 1 and isinstance(owner, astng.Const) \ - and owner.value is None: - continue - # XXX "super" call - owner_name = getattr(owner, 'name', 'None') - if owner_name == 'super' and \ - owner.root().name == '__builtin__': - continue - if getattr(owner, 'type', None) == 'metaclass': - continue - if self.config.ignore_mixin_members \ - and owner_name[-5:].lower() == 'mixin': - continue - #print owner.name, owner.root().name - try: - owner.getattr(node.attrname) - except AttributeError: - # XXX method / function - continue - except astng.NotFoundError: - if isinstance(owner, astng.Instance): - if hasattr(owner, 'has_dynamic_getattr') and owner.has_dynamic_getattr(): - continue - # XXX - if getattr(owner, 'name', None) == 'Values' and \ - owner.root().name == 'optparse': - continue - _type = 'Instance of' - elif isinstance(owner, astng.Module): - _type = 'Module' - else: - _type = 'Class' - self.add_message('E1101', node=node, - args=(_type, owner_name, node.attrname)) - # XXX: stop on the first found - # this is a bad solution to fix func_noerror_socket_member.py - break except astng.InferenceError: - pass + return + # list of (node, nodename) which are missing the attribute + missingattr = [] + ignoremim = self.config.ignore_mixin_members + for owner in infered: + # skip yes object + if owner is astng.YES: + continue + # if there is ambiguity, skip None + if len(infered) > 1 and isinstance(owner, astng.Const) \ + and owner.value is None: + continue + # XXX "super" / metaclass call + if is_super(owner) or getattr(owner, 'type', None) == 'metaclass': + continue + name = getattr(owner, 'name', 'None') + if ignoremim and name[-5:].lower() == 'mixin': + continue + try: + owner.getattr(node.attrname) + except AttributeError: + # XXX method / function + continue + except astng.NotFoundError: + if isinstance(owner, astng.Instance) \ + and owner.has_dynamic_getattr(): + continue + missingattr.append((owner, name)) + continue + # stop on the first found + break + else: + # we have not found any node with the attributes, display the + # message for infered nodes + for owner, name in missingattr: + self.add_message('E1101', node=node, + args=(display_type(owner), name, + node.attrname)) + def visit_assign(self, node): """check that if assigning to a function call, the function is @@ -132,7 +135,7 @@ zope\'s acquisition mecanism and so shouldn\'t trigger E0201 when accessed \ """ if not isinstance(node.expr, astng.CallFunc): return - function_node = self._safe_infer(node.expr.node) + function_node = safe_infer(node.expr.node) # skip class, generator and uncomplete function definition if not (isinstance(function_node, astng.Function) and function_node.root().fully_defined()): @@ -155,26 +158,11 @@ zope\'s acquisition mecanism and so shouldn\'t trigger E0201 when accessed \ def visit_callfunc(self, node): """check that called method are infered to callable objects """ - called = self._safe_infer(node.node) + called = safe_infer(node.node) # only function, generator and object defining __call__ are allowed if called is not None and not called.callable(): self.add_message('E1102', node=node, args=node.node.as_string()) - def _safe_infer(self, node): - """return the infered value for the given node. - Return None if inference failed or if there is some ambiguity (more than - one node has been infered) - """ - try: - inferit = node.infer() - value = inferit.next() - except astng.InferenceError: - return - try: - inferit.next() - return # None if there is ambiguity on the infered node - except StopIteration: - return value def register(linter): """required method to auto register this checker """ diff --git a/checkers/utils.py b/checkers/utils.py index cb3a99b..8c6612a 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -1,6 +1,6 @@ # pylint: disable-msg=W0611 # -# Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -30,6 +30,30 @@ except AttributeError: COMP_NODE_TYPES = astng.ListComp FOR_NODE_TYPES = (astng.For, astng.ListCompFor) +def safe_infer(node): + """return the infered value for the given node. + Return None if inference failed or if there is some ambiguity (more than + one node has been infered) + """ + try: + inferit = node.infer() + value = inferit.next() + except astng.InferenceError: + return + try: + inferit.next() + return # None if there is ambiguity on the infered node + except StopIteration: + return value + +def is_super(node): + """return True if the node is referencing the "super" builtin function + """ + if getattr(node, 'name', None) == 'super' and \ + node.root().name == '__builtin__': + return True + return False + def is_error(node): """return true if the function does nothing but raising an exception""" for child_node in node.code.getChildNodes(): @@ -37,6 +61,14 @@ def is_error(node): return True return False +def is_raising(stmt): + """return true if the given statement node raise an exception + """ + for node in stmt.nodes: + if isinstance(node, astng.Raise): + return True + return False + def is_empty(node): """return true if the given node does nothing but 'pass'""" for child_node in node.getChildNodes(): @@ -142,3 +174,11 @@ def overrides_a_method(class_node, name): if name in ancestor and isinstance(ancestor[name], astng.Function): return True return False + +def display_type(node): + """return the type of this node for screen display""" + if isinstance(node, astng.Instance): + return 'Instance of' + elif isinstance(node, astng.Module): + return 'Module' + return 'Class' diff --git a/doc/makefile b/doc/makefile index 944893e..2e19479 100644 --- a/doc/makefile +++ b/doc/makefile @@ -1,10 +1,10 @@ MKHTML=mkdoc -MKHTML_OPT=--doctype article --param toc.section.depth=1 --target html --stylesheet single-file +MKHTML_OPT=--doctype article --param toc.section.depth=1 --target html --stylesheet single-file SRC=. -all: quickstart.html features.html FAQ.html examples man +all: manual.html quickstart.html features.html FAQ.html examples man FAQ.html: ${SRC}/FAQ.txt ${MKHTML} ${MKHTML_OPT} ${SRC}/FAQ.txt @@ -12,6 +12,9 @@ FAQ.html: ${SRC}/FAQ.txt quickstart.html: ${SRC}/quickstart.txt ${MKHTML} ${MKHTML_OPT} ${SRC}/quickstart.txt +manual.html: ${SRC}/manual.txt + ${MKHTML} ${MKHTML_OPT} ${SRC}/manual.txt + features.html: chmod u+w ${SRC}/features.txt echo "PyLint features" > ${SRC}/features.txt @@ -38,8 +38,7 @@ import re import tokenize from os.path import dirname, basename, splitext, exists, isdir, join, normpath -from logilab.common.configuration import OptionsManagerMixIn, \ - check_yn, check_csv +from logilab.common.configuration import OptionsManagerMixIn, check_csv from logilab.common.modutils import modpath_from_file, get_module_files, \ file_from_modpath, load_module_from_name from logilab.common.interface import implements @@ -66,9 +65,10 @@ from pylint.__pkginfo__ import version OPTION_RGX = re.compile('\s*#*\s*pylint:(.*)') -REPORTER_OPT_MAP = {'html': HTMLReporter, +REPORTER_OPT_MAP = {'text': TextReporter, 'parseable': TextReporter2, - 'color': ColorizedTextReporter} + 'colorized': ColorizedTextReporter, + 'html': HTMLReporter,} # Python Linter class ######################################################### @@ -92,10 +92,10 @@ MSGS = { 'Used when an inline option is either badly formatted or can\'t \ be used inside modules.'), - 'I0011': ('Locally disabling %r', + 'I0011': ('Locally disabling %s', 'Used when an inline option disable a message or a messages \ category.'), - 'I0012': ('Locally enabling %r', + 'I0012': ('Locally enabling %s', 'Used when an inline option enable a message or a messages \ category.'), @@ -105,7 +105,7 @@ be used inside modules.'), 'E0011': ('Unrecognized file option %r', 'Used when an unknown inline option is encountered.'), 'E0012': ('Bad option value %r', - 'Used when an bad value for an inline option is encountered.'), + 'Used when a bad value for an inline option is encountered.'), } class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, @@ -132,6 +132,19 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 'help' : 'Add <file or directory> to the black list. It \ should be a base name, not a path. You may set this option multiple times.'}), + ('enable-checker', + {'type' : 'csv', 'metavar': '<checker ids>', + 'group': 'Messages control', + 'help' : 'Enable only checker(s) with the given id(s).\ + This option conflict with the disable-checker option'}), + + ('disable-checker', + {'type' : 'csv', 'metavar': '<checker ids>', + 'group': 'Messages control', + 'help' : 'Enable all checker(s) except those with the \ + given id(s).\ + This option conflict with the disable-checker option'}), + ('persistent', {'default': 1, 'type' : 'yn', 'metavar' : '<y_or_n>', 'help' : 'Pickle collected data for later comparisons.'}), @@ -145,28 +158,19 @@ should be a base name, not a path. You may set this option multiple times.'}), 'help' : 'List of plugins (as comma separated values of \ python modules names) to load, usually to register additional checkers.'}), - ('reports', - {'default': 1, 'type' : 'yn', 'metavar' : '<y_or_n>', - 'short': 'r', + ('output-format', + {'default': 'text', 'type': 'choice', 'metavar' : '<format>', + 'choices': ('text', 'parseable', 'colorized', 'html'), + 'short': 'f', 'group': 'Reports', - 'help' : 'Tells wether to display a full report or only the\ - messages'}), - ('html', - {'default': 0, 'type' : 'yn', 'metavar' : '<y_or_n>', - 'group': 'Reports', - 'help' : 'Use HTML as output format instead of text'}), - - ('parseable', - {'default': 0, 'type' : 'yn', 'metavar' : '<y_or_n>', - 'short': 'p', - 'group': 'Reports', - 'help' : 'Use a parseable text output format, so your favorite\ - text editor will be able to jump to the line corresponding to a message.'}), - - ('color', - {'default': 0, 'type' : 'yn', 'metavar' : '<y_or_n>', + 'help' : 'set the output format. Available formats are text,\ + parseable, colorized and html'}), + + ('include-ids', + {'type' : 'yn', 'metavar' : '<y_or_n>', 'default' : 0, + 'short': 'i', 'group': 'Reports', - 'help' : 'Colorizes text output using ansi escape codes'}), + 'help' : 'Include message\'s id in output'}), ('files-output', {'default': 0, 'type' : 'yn', 'metavar' : '<y_or_n>', @@ -175,6 +179,13 @@ python modules names) to load, usually to register additional checkers.'}), package specified on the command line instead of printing them on stdout. \ Reports (if any) will be written in a file name "pylint_global.[txt|html]".'}), + ('reports', + {'default': 1, 'type' : 'yn', 'metavar' : '<y_or_n>', + 'short': 'r', + 'group': 'Reports', + 'help' : 'Tells wether to display a full report or only the\ + messages'}), + ('evaluation', {'type' : 'string', 'metavar' : '<python_expression>', 'group': 'Reports', @@ -192,46 +203,39 @@ warning, statement which respectivly contain the number of errors / warnings\ 'help' : 'Add a comment according to your evaluation note. \ This is used by the global evaluation report (R0004).'}), - ('include-ids', - {'type' : 'yn', 'metavar' : '<y_or_n>', 'default' : 0, - 'short': 'i', + ('enable-report', + {'type' : 'csv', 'metavar': '<rpt ids>', 'group': 'Reports', - 'help' : 'Include message\'s id in output'}), + 'help' : 'Enable the report(s) with the given id(s).'}), + + ('disable-report', + {'type' : 'csv', 'metavar': '<rpt ids>', + 'group': 'Reports', + 'help' : 'Disable the report(s) with the given id(s).'}), ('enable-msg-cat', {'type' : 'csv', 'metavar': '<msg cats>', - 'group': 'Reports', + 'group': 'Messages control', 'help' : 'Enable all messages in the listed categories.'}), ('disable-msg-cat', {'type' : 'csv', 'metavar': '<msg cats>', - 'group': 'Reports', + 'group': 'Messages control', 'help' : 'Disable all messages in the listed categories.'}), ('enable-msg', {'type' : 'csv', 'metavar': '<msg ids>', - 'group': 'Reports', - 'help' : 'Enable the message with the given id.'}), + 'group': 'Messages control', + 'help' : 'Enable the message(s) with the given id(s).'}), ('disable-msg', {'type' : 'csv', 'metavar': '<msg ids>', - 'group': 'Reports', - 'help' : 'Disable the message with the given id.'}), - - ('enable-report', - {'type' : 'csv', 'metavar': '<rpt ids>', - 'group': 'Reports', - 'help' : 'Enable the report with the given id.'}), - - ('disable-report', - {'type' : 'csv', 'metavar': '<rpt ids>', - 'group': 'Reports', - 'help' : 'Disable the report with the given id.'}), - + 'group': 'Messages control', + 'help' : 'Disable the message(s) with the given id(s).'}), ) - option_groups = ( - ('Reports', 'Options related to messages / statistics reporting'), + ('Messages control', 'Options controling analysis messages'), + ('Reports', 'Options related to output formating and reporting'), ) def __init__(self, options=(), reporter=None, option_groups=(), @@ -268,10 +272,10 @@ This is used by the global evaluation report (R0004).'}), ReportsHandlerMixIn.__init__(self) BaseRawChecker.__init__(self) # provided reports - self.reports = (('R0001', 'Total errors / warnings', - report_error_warning_stats), + self.reports = (('R0001', 'Messages by category', + report_total_messages_stats), ('R0002', '% errors / warnings by module', - report_error_warning_by_module_stats), + report_messages_by_module_stats), ('R0003', 'Messages', report_messages_stats), ('R0004', 'Global evaluation', @@ -311,10 +315,13 @@ This is used by the global evaluation report (R0004).'}), meth(value) elif opt_name == 'cache-size': self.manager.set_cache_size(int(value)) -# elif opt_name = 'load-plugins': -# self.load_plugin_modules(get_csv(value)) - elif opt_name in REPORTER_OPT_MAP and check_yn(None, opt_name, value): - self.set_reporter(REPORTER_OPT_MAP[opt_name]()) + elif opt_name == 'output-format': + self.set_reporter(REPORTER_OPT_MAP[value.lower()]()) + elif opt_name in ('enable-checker', 'disable-checker'): + if not value: + return + checkerids = [v.lower() for v in check_csv(None, opt_name, value)] + self.enable_checkers(checkerids, opt_name == 'enable-checker') BaseRawChecker.set_option(self, opt_name, value, action, opt_dict) # checkers manipulation methods ########################################### @@ -329,25 +336,24 @@ This is used by the global evaluation report (R0004).'}), if hasattr(checker, 'reports'): for r_id, r_title, r_cb in checker.reports: self.register_report(r_id, r_title, r_cb, checker) - if checker.__doc__ is None: - checker.__doc__ = 'no documentation available for this checker' - need_space = 80 - (len(checker.__doc__.splitlines()[-1]) % 80) - checker.__doc__ += """%s -This checker also defines the following reports: - * %s -""" % (' ' * need_space, - '\n * '.join(['% -76s' % ('%s: %s' % report[:2]) - for report in checker.reports])) self.register_options_provider(checker) if hasattr(checker, 'msgs'): self.register_messages(checker) - def disable_all_checkers(self): - """disable all possible checkers """ + def enable_checkers(self, listed, enabled): + """only enable/disable checkers from the given list""" + idmap = {} for checker in self._checkers.keys(): - checker.enable(False) - + checker.enable(not enabled) + idmap[checker.name] = checker + for checkerid in listed: + try: + checker = idmap[checkerid] + except KeyError: + raise Exception('no checker named %s' % checkerid) + checker.enable(enabled) + def disable_noerror_checkers(self): """disable all checkers without error messages, and the 'miscellaneous' checker which can be safely deactivated in debug @@ -475,6 +481,7 @@ This checker also defines the following reports: self.reporter.set_output(open(reportfile, 'w')) self.check_file(filepath, modname, checkers) # notify global end + self.set_current_module('') for checker in rev_checkers: checker.close() @@ -482,7 +489,6 @@ This checker also defines the following reports: """check a module or package from its name if modname is a package, recurse on its subpackages / submodules """ -## print 'CHECKING', filepath, modname # get the given module representation self.base_name = modname self.base_file = normpath(filepath) @@ -524,8 +530,12 @@ This checker also defines the following reports: self.stats['by_module'][modname]['statement'] = 0 for msg_cat in MSG_TYPES.values(): self.stats['by_module'][modname][msg_cat] = 0 - self._module_msgs_state = {} - self._module_msg_cats_state = {} + # XXX hack, to be correct we need to keep module_msgs_state + # for every analyzed module (the problem stands with localized + # messages which are only detected in the .close step) + if modname: + self._module_msgs_state = {} + self._module_msg_cats_state = {} def get_astng(self, filepath, modname): """return a astng representation for a module""" @@ -634,7 +644,7 @@ This checker also defines the following reports: # some reporting functions #################################################### -def report_error_warning_stats(sect, stats, old_stats): +def report_total_messages_stats(sect, stats, old_stats): """make total errors / warnings report""" lines = ['type', 'number', 'previous', 'difference'] lines += table_lines_from_stats(stats, old_stats, @@ -657,7 +667,7 @@ def report_messages_stats(sect, stats, _): lines += (msg_id, str(value)) sect.append(Table(children=lines, cols=2, rheaders=1)) -def report_error_warning_by_module_stats(sect, stats, _): +def report_messages_by_module_stats(sect, stats, _): """make errors / warnings by modules report""" if len(stats['by_module']) == 1: # don't print this report when we are analysing a single module @@ -713,7 +723,6 @@ def preprocess_options(args, search_for): """ for i, arg in enumerate(args): for option in search_for: - ##print arg, option if arg.startswith('--%s=' % option): search_for[option](option, arg[len(option)+3:]) del args[i] @@ -726,6 +735,10 @@ class Run: run(*sys.argv[1:]) """ + option_groups = ( + ('Commands', 'Options which are actually commands. Options in this \ +group are mutually exclusive.'), + ) def __init__(self, args, reporter=None, quiet=0): self._rcfile = None @@ -738,36 +751,34 @@ class Run: 'type': 'string', 'metavar': '<file>', 'help' : 'Specify a configuration file.'}), - ('disable-all', - {'action' : 'callback', - 'callback' : self.cb_disable_all_checkers, - 'help' : '''Disable all possible checkers. This option should - precede enable-* options.'''}), - ('help-msg', {'action' : 'callback', 'type' : 'string', 'metavar': '<msg-id>', 'callback' : self.cb_help_message, + 'group': 'Commands', 'help' : '''Display a help message for the given message id and \ -exit. This option may be a comma separated list.'''}), +exit. The value may be a comma separated list of message ids.'''}), ('list-msgs', {'action' : 'callback', 'metavar': '<msg-id>', 'callback' : self.cb_list_messages, - 'help' : 'List and explain every available messages.'}), + 'group': 'Commands', + 'help' : "Generate pylint's full documentation."}), ('generate-rcfile', {'action' : 'callback', 'callback' : self.cb_generate_config, + 'group': 'Commands', 'help' : '''Generate a sample configuration file according to \ -the current configuration. You can put other options before this one to use \ -them in the configuration. This option causes the program to exit'''}), +the current configuration. You can put other options before this one to get \ +them in the generated configuration.'''}), ('generate-man', {'action' : 'callback', 'callback' : self.cb_generate_manpage, - 'help' : '''Generate a man page for pylint. This option causes \ -the program to exit'''}), + 'group': 'Commands', + 'help' : "Generate pylint's man page."}), - ('debug-mode', + ('errors-only', {'action' : 'callback', 'callback' : self.cb_debug_mode, + 'short': 'e', 'help' : '''In debug mode, checkers without error messages are \ disabled and for others, only the ERROR messages are displayed, and no reports \ are done by default'''}), @@ -777,7 +788,8 @@ are done by default'''}), 'default': False, 'help' : 'Profiled execution.'}), - ), reporter=reporter, pylintrc=self._rcfile) + ), option_groups=self.option_groups, + reporter=reporter, pylintrc=self._rcfile) linter.quiet = quiet # register standard checkers from pylint import checkers @@ -836,10 +848,6 @@ processing. """callback for option preprocessing (ie before optik parsing)""" self._plugins.extend(get_csv(value)) - def cb_disable_all_checkers(self, *args, **kwargs): - """optik callback for disabling all checkers""" - self.linter.disable_all_checkers() - def cb_debug_mode(self, *args, **kwargs): """debug mode: * checkers without error messages are disabled diff --git a/reporters/html.py b/reporters/html.py index 625c447..5fd4350 100644 --- a/reporters/html.py +++ b/reporters/html.py @@ -1,3 +1,5 @@ +# Copyright (c) 2003-2006 Sylvain Thenault (thenault@gmail.com). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -10,10 +12,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2002-2006 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr - -HTML reporter +"""HTML reporter """ __revision__ = "$Id: html.py,v 1.14 2006-03-08 15:53:41 syt Exp $" @@ -50,7 +49,9 @@ class HTMLReporter(BaseReporter): def _display(self, layout): """launch layouts display - overriden from BaseReporter to add a blank line... + overriden from BaseReporter to add insert the messages section + (in add_message, message is not displayed, just collected so it + can be displayed in an html table) """ if self.msgs: # add stored messages to the layout diff --git a/reporters/text.py b/reporters/text.py index f4dfd2c..5bffe64 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -1,3 +1,5 @@ +# Copyright (c) 2003-2006 Sylvain Thenault (thenault@gmail.com). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -10,10 +12,11 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2000-2003 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr +"""Plain text reporters: -Plain text reporter +* the default one grouping messages by module +* the parseable one with module path on each message +* an ANSI colorized text reporter """ __revision__ = "$Id: text.py,v 1.21 2005-12-28 00:24:35 syt Exp $" @@ -51,8 +54,11 @@ class TextReporter(BaseReporter): """manage message of different type and in the context of path""" module, obj, line = location[1:] if not self._modules.has_key(module): - self.writeln('************* Module %s' % module) - self._modules[module] = 1 + if module: + self.writeln('************* Module %s' % module) + self._modules[module] = 1 + else: + self.writeln('************* %s' % module) if obj: obj = ':%s' % obj if self.include_ids: @@ -134,8 +140,12 @@ class ColorizedTextReporter(TextReporter): module, obj, line = location[1:] if not self._modules.has_key(module): color, style = self._get_decoration('S') - modsep = colorize_ansi('************* Module %s' % module, - color, style) + if module: + modsep = colorize_ansi('************* Module %s' % module, + color, style) + else: + modsep = colorize_ansi('************* %s' % module, + color, style) self.writeln(modsep) self._modules[module] = 1 if obj: @@ -1,5 +1,6 @@ #!/usr/bin/env python -# pylint: disable-msg=W0142,W0403,W0404,E0611,W0613,W0622,W0622,W0704,R0904 +# pylint: disable-msg=W0142,W0403,W0404,E0611,W0613,W0622,W0622,W0704 +# pylint: disable-msg=R0904,C0103 # # Copyright (c) 2003 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr @@ -58,7 +59,7 @@ try: except ImportError: ext_modules = None -BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog') +BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog', '.svn') IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc') diff --git a/test/func_test.py b/test/func_test.py index 3de8891..26c803d 100644 --- a/test/func_test.py +++ b/test/func_test.py @@ -1,4 +1,4 @@ -# Copyright (c) 2002-2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under diff --git a/test/input/func_base_stmt_without_effect.py b/test/input/func_base_stmt_without_effect.py index f9ff608..8eb8736 100644 --- a/test/input/func_base_stmt_without_effect.py +++ b/test/input/func_base_stmt_without_effect.py @@ -1,7 +1,15 @@ """ - 'W0103': ('Statement seems to have no effect', + 'W0104': ('Statement seems to have no effect', 'Used when a statement doesn\'t have (or at least seems to) \ any effect.'), + 'W0105': ('String statement has no effect', + 'Used when a string is used as a statement (which of course \ + has no effect). This is a particular case of W0104 with its \ + own message so you can easily disable it if you\'re using \ + those strings as documentation, instead of comments.'), + 'W0106': ('Unnecessary semi-column', + 'Used when a statement is endend by a semi-colon (";"), which \ + isn\'t necessary (that\'s python, not C ;)'), """ __revision__ = '' @@ -16,3 +24,5 @@ __revision__.lower() # ok """inline doc string should use a separated message""" + +__revision__.lower(); # unnecessary ; diff --git a/test/input/func_block_disable_msg.py b/test/input/func_block_disable_msg.py index 7de7acd..3394d61 100644 --- a/test/input/func_block_disable_msg.py +++ b/test/input/func_block_disable_msg.py @@ -1,5 +1,5 @@ +# pylint: disable-msg=C0302 """pylint option block-disable-msg""" - __revision__ = None class Foo(object): @@ -105,31 +105,899 @@ class ClassLevelMessage(object): self.attr0 = 1 def too_complex_but_thats_ok(self, attr1, attr2): - """THIS Method has too much branchs but i don't care + """THIS Method has too much branchs and returns but i don't care """ - # pylint: disable-msg=R0912 + # pylint: disable-msg=R0912,R0911 try: attr3 = attr1+attr2 except ValueError: attr3 = None except: - print 'duh', self + return 'duh', self if attr1: for i in attr1: if attr2: - print i + return i else: - print 'duh' + return 'duh' elif attr2: for i in attr2: if attr2: - print i + return i else: - print 'duh' + return 'duh' else: for i in range(15): if attr3: - print i + return i else: - print 'doh' - + return 'doh' + return None + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +print 'hop, to much lines but i don\'t care' diff --git a/test/messages/func_base_stmt_without_effect.txt b/test/messages/func_base_stmt_without_effect.txt index e1cce0c..c00d959 100644 --- a/test/messages/func_base_stmt_without_effect.txt +++ b/test/messages/func_base_stmt_without_effect.txt @@ -1,4 +1,5 @@ -W: 9: Statement seems to have no effect -W: 11: Statement seems to have no effect -W: 15: Statement seems to have no effect -W: 18: String statement has no effect +W: 17: Statement seems to have no effect +W: 19: Statement seems to have no effect +W: 23: Statement seems to have no effect +W: 26: String statement has no effect +W: 28: Unnecessary semi-column diff --git a/test/messages/func_dotted_ancestor.txt b/test/messages/func_dotted_ancestor.txt index 84247b8..5347191 100644 --- a/test/messages/func_dotted_ancestor.txt +++ b/test/messages/func_dotted_ancestor.txt @@ -1 +1 @@ -R: 8:Aaaa: Not enough public methods (0/2) +R: 8:Aaaa: To few public methods (0/2) diff --git a/test/messages/func_r0903.txt b/test/messages/func_r0903.txt index 6ab8888..c59f740 100644 --- a/test/messages/func_r0903.txt +++ b/test/messages/func_r0903.txt @@ -1 +1 @@ -R: 4:Aaaa: Not enough public methods (1/2) +R: 4:Aaaa: To few public methods (1/2) diff --git a/test/messages/func_w0302.txt b/test/messages/func_w0302.txt index ae8a99f..e3d8fc9 100644 --- a/test/messages/func_w0302.txt +++ b/test/messages/func_w0302.txt @@ -1,2 +1,2 @@ -W: 0: Too many lines in module (1017) +C: 0: Too many lines in module (1017) @@ -1,5 +1,5 @@ -# Copyright (c) 2003-2005 Sylvain Thenault (thenault@gmail.com). -# Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2006 Sylvain Thenault (thenault@gmail.com). +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -20,12 +20,15 @@ main pylint class __revision__ = "$Id: utils.py,v 1.13 2006-04-19 09:17:40 syt Exp $" +import sys from os import linesep -from logilab.astng import Module from logilab.common.textutils import normalize_text +from logilab.common.configuration import rest_format_section from logilab.common.ureports import Section +from logilab.astng import Module + from pylint.checkers import EmptyReport class UnknownMessage(Exception): @@ -55,7 +58,7 @@ def sort_checkers(checkers, enabled_only=True): def sort_msgs(msg_ids): """sort message identifiers according to their category first""" - msg_order = ['I', 'C', 'R', 'W', 'E', 'F'] + msg_order = ['E', 'W', 'R', 'C', 'I', 'F'] def cmp_func(msgid1, msgid2): """comparison function for two message identifiers""" if msgid1[0] != msgid2[0]: @@ -117,18 +120,25 @@ class MessagesHandlerMixIn: assert msg_id[0] in MSG_CATEGORIES, \ 'Bad message type %s in %r' % (msg_id[0], msg_id) chk_id = msg_id[1:3] - if checker is not None: - add = ' This message belongs to the %s checker.' % checker.name - msg_help += add self._messages_help[msg_id] = msg_help self._messages[msg_id] = msg - def get_message_help(self, msg_id): + def get_message_help(self, msg_id, checkerref=False): """return the help string for the given message id""" msg_id = self.check_message_id(msg_id) msg = self._messages_help[msg_id] msg = normalize_text(' '.join(msg.split()), indent=' ') - return '%s:\n%s' % (msg_id, msg) + if checkerref: + for checker in self._checkers: + if msg_id in checker.msgs: + msg += ' This message belongs to the %s checker.' % \ + checker.name + break + title = self._messages[msg_id] + if title != '%s': + title = title.splitlines()[0] + return ':%s: *%s*\n%s' % (msg_id, title, msg) + return ':%s:\n%s' % (msg_id, msg) def disable_message(self, msg_id, scope='package', line=None): """don't output message of the given id""" @@ -205,7 +215,7 @@ class MessagesHandlerMixIn: if not self._msg_cats_state.get(msg_id[0], True): return False if line is None: - return self._msgs_state.get(msg_id, True) + return self._msgs_state.get(msg_id, True) try: return self._module_msgs_state[msg_id][line] except (KeyError, TypeError): @@ -252,7 +262,7 @@ class MessagesHandlerMixIn: """display help messages for the given message identifiers""" for msg_id in msgids: try: - print self.get_message_help(msg_id) + print self.get_message_help(msg_id, True) print except UnknownMessage, ex: print ex @@ -260,26 +270,50 @@ class MessagesHandlerMixIn: continue def list_messages(self): - """list available messages""" - for checker in sort_checkers(self._checkers.keys()): - print checker.name.capitalize() - print '-' * len(checker.name) - print - if checker.__doc__: # __doc__ is None with -OO - print 'Description' - print '~~~~~~~~~~~' - print linesep.join([line.strip() - for line in checker.__doc__.splitlines()]) + """output a full documentation in ReST format""" + for checker in sort_checkers(self._checkers): + if checker.name == 'master': + prefix = 'Main ' + if checker.options: + for section, options in checker.options_by_section(): + if section is None: + title = 'General options' + else: + title = '%s options' % section.capitalize() + print title + print '~' * len(title) + rest_format_section(sys.stdout, None, options) + print + else: + prefix = '' + title = '%s checker' % checker.name.capitalize() + print title + print '-' * len(title) + if checker.__doc__: # __doc__ is None with -OO + print linesep.join([line.strip() + for line in checker.__doc__.splitlines()]) + if checker.options: + title = 'Options' + print title + print '~' * len(title) + for section, options in checker.options_by_section(): + rest_format_section(sys.stdout, section, options) + print + if checker.msgs: + title = ('%smessages' % prefix).capitalize() + print title + print '~' * len(title) + for msg_id in sort_msgs(checker.msgs.keys()): + print self.get_message_help(msg_id, False) print - if not checker.msgs: - continue - print 'Messages' - print '~~~~~~~~' - for msg_id in sort_msgs(checker.msgs.keys()): - print self.get_message_help(msg_id) + if getattr(checker, 'reports', None): + title = ('%sreports' % prefix).capitalize() + print title + print '~' * len(title) + for report in checker.reports: + print ':%s: %s' % report[:2] print print - print class ReportsHandlerMixIn: |