diff options
| author | Torsten Marek <shlomme@gmail.com> | 2014-07-24 15:07:51 +0200 |
|---|---|---|
| committer | Torsten Marek <shlomme@gmail.com> | 2014-07-24 15:07:51 +0200 |
| commit | 7aa57622583caab7dc14d0458bc3c4082036b766 (patch) | |
| tree | 8235d02ff30784eb6656abedba5daa71fbd8fd22 | |
| parent | a7d1515daef1234b3bf3e3991299771bea3c56bc (diff) | |
| download | pylint-7aa57622583caab7dc14d0458bc3c4082036b766.tar.gz | |
Implement confidence levels.
- attach confidence levels to a number of messages
- include confidence levels in the message object
- if the confidence of a message is not HIGH or UNDEFINED,
include it in the test output.
32 files changed, 276 insertions, 153 deletions
@@ -2,6 +2,11 @@ ChangeLog for Pylint ==================== -- + * Each message now comes with a confidence level attached, and + can be filtered base on this level. This allows to filter out + all messages that were emitted even though an inference failure + happened during checking. + * Improved presenting unused-import message. Closes issue #293. * Add new checker for finding spelling errors. New messages: diff --git a/checkers/__init__.py b/checkers/__init__.py index 693a5ff..8710f4a 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -46,6 +46,8 @@ from logilab.common.configuration import OptionsProviderMixIn from pylint.reporters import diff_string from pylint.utils import register_plugins +from pylint.interfaces import UNDEFINED + def table_lines_from_stats(stats, old_stats, columns): """get values listed in <columns> from <stats> and <old_stats>, @@ -90,9 +92,9 @@ class BaseChecker(OptionsProviderMixIn): OptionsProviderMixIn.__init__(self) self.linter = linter - def add_message(self, msg_id, line=None, node=None, args=None): + def add_message(self, msg_id, line=None, node=None, args=None, confidence=UNDEFINED): """add a message of a given type""" - self.linter.add_message(msg_id, line, node, args) + self.linter.add_message(msg_id, line, node, args, confidence) # dummy methods implementing the IChecker interface diff --git a/checkers/base.py b/checkers/base.py index d6553ed..298ac5f 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -24,7 +24,7 @@ from logilab.common.ureports import Table from astroid import are_exclusive, InferenceError import astroid.bases -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import EmptyReport from pylint.reporters import diff_string from pylint.checkers import BaseChecker @@ -36,6 +36,7 @@ from pylint.checkers.utils import ( overrides_a_method, safe_infer, get_argument_from_call, + has_known_bases, NoSuchArgumentError, is_import_error, ) @@ -995,10 +996,15 @@ class NameChecker(_BasicChecker): def visit_function(self, node): # Do not emit any warnings if the method is just an implementation # of a base class method. - if node.is_method() and overrides_a_method(node.parent.frame(), node.name): - return + confidence = HIGH + if node.is_method(): + if overrides_a_method(node.parent.frame(), node.name): + return + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) + self._check_name(_determine_function_name_type(node), - node.name, node) + node.name, node, confidence) # Check argument names args = node.args.args if args is not None: @@ -1047,20 +1053,22 @@ class NameChecker(_BasicChecker): def _find_name_group(self, node_type): return self._name_group.get(node_type, node_type) - def _is_multi_naming_match(self, match): + def _is_multi_naming_match(self, match, node_type, confidence): return (match is not None and match.lastgroup is not None and - match.lastgroup not in EXEMPT_NAME_CATEGORIES) + match.lastgroup not in EXEMPT_NAME_CATEGORIES + and (node_type != 'method' or confidence != INFERENCE_FAILURE)) - def _raise_name_warning(self, node, node_type, name): + def _raise_name_warning(self, node, node_type, name, confidence): type_label = _NAME_TYPES[node_type][1] hint = '' if self.config.include_naming_hint: hint = ' (hint: %s)' % (getattr(self.config, node_type + '_name_hint')) - self.add_message('invalid-name', node=node, args=(type_label, name, hint)) + self.add_message('invalid-name', node=node, args=(type_label, name, hint), + confidence=confidence) self.stats['badname_' + node_type] += 1 - def _check_name(self, node_type, name, node): + def _check_name(self, node_type, name, node, confidence=HIGH): """check for a name using the type's regexp""" if is_inside_except(node): clobbering, _ = clobber_in_except(node) @@ -1075,14 +1083,14 @@ class NameChecker(_BasicChecker): regexp = getattr(self.config, node_type + '_rgx') match = regexp.match(name) - if self._is_multi_naming_match(match): + if self._is_multi_naming_match(match, node_type, confidence): name_group = self._find_name_group(node_type) bad_name_group = self._bad_names.setdefault(name_group, {}) warnings = bad_name_group.setdefault(match.lastgroup, []) - warnings.append((node, node_type, name)) + warnings.append((node, node_type, name, confidence)) if match is None: - self._raise_name_warning(node, node_type, name) + self._raise_name_warning(node, node_type, name, confidence) class DocStringChecker(_BasicChecker): @@ -1133,6 +1141,8 @@ class DocStringChecker(_BasicChecker): ftype = node.is_method() and 'method' or 'function' if isinstance(node.parent.frame(), astroid.Class): overridden = False + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if node.name in ancestor and \ @@ -1140,11 +1150,13 @@ class DocStringChecker(_BasicChecker): overridden = True break self._check_docstring(ftype, node, - report_missing=not overridden) + report_missing=not overridden, + confidence=confidence) else: self._check_docstring(ftype, node) - def _check_docstring(self, node_type, node, report_missing=True): + def _check_docstring(self, node_type, node, report_missing=True, + confidence=HIGH): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: @@ -1170,10 +1182,12 @@ class DocStringChecker(_BasicChecker): return elif func.bound.name in ('str', 'unicode', 'bytes'): return - self.add_message('missing-docstring', node=node, args=(node_type,)) + self.add_message('missing-docstring', node=node, args=(node_type,), + confidence=confidence) elif not docstring.strip(): self.stats['undocumented_'+node_type] += 1 - self.add_message('empty-docstring', node=node, args=(node_type,)) + self.add_message('empty-docstring', node=node, args=(node_type,), + confidence=confidence) class PassChecker(_BasicChecker): diff --git a/checkers/exceptions.py b/checkers/exceptions.py index cd78719..9b1a071 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -25,8 +25,8 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import ( is_empty, is_raising, check_messages, inherit_from_std_ex, - EXCEPTIONS_MODULE) -from pylint.interfaces import IAstroidChecker + EXCEPTIONS_MODULE, has_known_bases) +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE def _annotated_unpack_infer(stmt, context=None): """ @@ -234,7 +234,9 @@ class ExceptionsChecker(BaseChecker): if expr.newstyle: self.add_message('raising-non-exception', node=node) else: - self.add_message('nonstandard-exception', node=node) + self.add_message( + 'nonstandard-exception', node=node, + confidence=INFERENCE if has_known_bases(expr) else INFERENCE_FAILURE) else: value_found = False else: diff --git a/checkers/newstyle.py b/checkers/newstyle.py index cf50229..1946f94 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -19,9 +19,9 @@ import sys import astroid -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.checkers import BaseChecker -from pylint.checkers.utils import check_messages +from pylint.checkers.utils import check_messages, has_known_bases MSGS = { 'E1001': ('Use of __slots__ on an old style class', @@ -78,12 +78,17 @@ class NewStyleConflictChecker(BaseChecker): style class definition. """ if '__slots__' in node and not node.newstyle: - self.add_message('slots-on-old-class', node=node) + confidence = (INFERENCE if has_known_bases(node) + else INFERENCE_FAILURE) + self.add_message('slots-on-old-class', node=node, + confidence=confidence) # The node type could be class, exception, metaclass, or # interface. Presumably, the non-class-type nodes would always # have an explicit base class anyway. if not node.bases and node.type == 'class' and not node.metaclass(): - self.add_message('old-style-class', node=node) + # We use confidence HIGH here because this message should only ever + # be emitted for classes at the root of the inheritance hierarchyself. + self.add_message('old-style-class', node=node, confidence=HIGH) @check_messages('property-on-old-class') def visit_callfunc(self, node): @@ -92,9 +97,12 @@ class NewStyleConflictChecker(BaseChecker): if (isinstance(parent, astroid.Class) and not parent.newstyle and isinstance(node.func, astroid.Name)): + confidence = (INFERENCE if has_known_bases(parent) + else INFERENCE_FAILURE) name = node.func.name if name == 'property': - self.add_message('property-on-old-class', node=node) + self.add_message('property-on-old-class', node=node, + confidence=confidence) @check_messages('super-on-old-class', 'bad-super-call', 'missing-super-argument') def visit_function(self, node): @@ -112,9 +120,12 @@ class NewStyleConflictChecker(BaseChecker): if isinstance(call, astroid.CallFunc) and \ isinstance(call.func, astroid.Name) and \ call.func.name == 'super': + confidence = (INFERENCE if has_known_bases(klass) + else INFERENCE_FAILURE) if not klass.newstyle: # super should not be used on an old style class - self.add_message('super-on-old-class', node=node) + self.add_message('super-on-old-class', node=node, + confidence=confidence) else: # super first arg should be the class if not call.args and sys.version_info[0] == 3: @@ -128,7 +139,8 @@ class NewStyleConflictChecker(BaseChecker): continue if supcls is None: - self.add_message('missing-super-argument', node=call) + self.add_message('missing-super-argument', node=call, + confidence=confidence) continue if klass is not supcls: @@ -144,7 +156,8 @@ class NewStyleConflictChecker(BaseChecker): if name is not None: self.add_message('bad-super-call', node=call, - args=(name, )) + args=(name, ), + confidence=confidence) def register(linter): diff --git a/checkers/typecheck.py b/checkers/typecheck.py index a8851a2..27ed9f5 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -23,23 +23,19 @@ import astroid from astroid import InferenceError, NotFoundError, YES, Instance from astroid.bases import BUILTINS -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer, is_super, check_messages MSGS = { 'E1101': ('%s %r has no %r member', 'no-member', - 'Used when a variable is accessed for an unexistent member.'), + 'Used when a variable is accessed for an unexistent member.', + {'old_names': [('E1103', 'maybe-no-member')]}), 'E1102': ('%s is not callable', 'not-callable', 'Used when an object being called has been inferred to a non \ callable object'), - 'E1103': ('%s %r has no %r member (but some types could not be inferred)', - 'maybe-no-member', - 'Used when a variable is accessed for an unexistent member, but \ - astroid was not able to interpret all possible types of this \ - variable.'), 'E1111': ('Assigning to function call which doesn\'t return', 'assignment-from-no-return', 'Used when an assignment is done on a function call but the \ @@ -187,7 +183,7 @@ accessed. Python regular expressions are accepted.'} def visit_delattr(self, node): self.visit_getattr(node) - @check_messages('no-member', 'maybe-no-member') + @check_messages('no-member') def visit_getattr(self, node): """check that the accessed attribute exists @@ -279,13 +275,11 @@ accessed. Python regular expressions are accepted.'} if actual in done: continue done.add(actual) - if inference_failure: - msgid = 'maybe-no-member' - else: - msgid = 'no-member' - self.add_message(msgid, node=node, + confidence = INFERENCE if not inference_failure else INFERENCE_FAILURE + self.add_message('no-member', node=node, args=(owner.display_type(), name, - node.attrname)) + node.attrname), + confidence=confidence) @check_messages('assignment-from-no-return', 'assignment-from-none') def visit_assign(self, node): diff --git a/checkers/utils.py b/checkers/utils.py index 000aee7..b3ba51b 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -462,3 +462,22 @@ def is_import_error(handler): return True except astroid.InferenceError: continue + +def has_known_bases(klass): + """Returns true if all base classes of a class could be inferred.""" + try: + return klass._all_bases_known + except AttributeError: + pass + try: + for base in klass.bases: + result = base.infer().next() + # TODO: check for A->B->A->B pattern in class structure too? + if not isinstance(result, astroid.Class) or result is klass or not has_known_bases(result): + klass._all_bases_known = False + return False + except astroid.InferenceError: + klass._all_bases_known = False + return False + klass._all_bases_known = True + return True diff --git a/checkers/variables.py b/checkers/variables.py index a88bf3b..9c0b15c 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -25,14 +25,14 @@ from astroid import are_exclusive, builtin_lookup, AstroidBuildingException from logilab.common.modutils import file_from_modpath -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import get_global_option from pylint.checkers import BaseChecker from pylint.checkers.utils import ( PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, assign_parent, check_messages, is_inside_except, clobber_in_except, - get_all_elements) + get_all_elements, has_known_bases) SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") @@ -486,6 +486,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' klass = node.parent.frame() if is_method and (klass.type == 'interface' or node.is_abstract()): return + if is_method and isinstance(klass, astroid.Class): + confidence = INFERENCE if has_known_bases(klass) else INFERENCE_FAILURE + else: + confidence = HIGH authorized_rgx = self.config.dummy_variables_rgx called_overridden = False argnames = node.argnames() @@ -539,7 +543,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' # don't check callback arguments XXX should be configurable if node.name.startswith('cb_') or node.name.endswith('_cb'): continue - self.add_message('unused-argument', args=name, node=stmt) + self.add_message('unused-argument', args=name, node=stmt, + confidence=confidence) else: if stmt.parent and isinstance(stmt.parent, astroid.Assign): if name in nonlocal_names: diff --git a/interfaces.py b/interfaces.py index 50f2c83..ea3b40f 100644 --- a/interfaces.py +++ b/interfaces.py @@ -11,9 +11,21 @@ # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Interfaces for PyLint objects""" +from collections import namedtuple from logilab.common.interface import Interface +Confidence = namedtuple('Confidence', ['name', 'description']) +# Warning Certainties +HIGH = Confidence('HIGH', 'No false positive possible.') +INFERENCE = Confidence('INFERENCE', 'Warning based on inference result.') +INFERENCE_FAILURE = Confidence('INFERENCE_FAILURE', + 'Warning based on inference with failures.') +UNDEFINED = Confidence('UNDEFINED', + 'Warning without any associated confidence level.') + +CONFIDENCE_LEVELS = [HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED] + class IChecker(Interface): """This is an base interface, not designed to be used elsewhere than for @@ -51,7 +51,7 @@ from pylint.utils import ( PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, MessagesStore, FileState, EmptyReport, expand_modules, tokenize_module) -from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker +from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker, CONFIDENCE_LEVELS from pylint.checkers import (BaseTokenChecker, table_lines_from_stats, initialize as checkers_initialize) @@ -238,6 +238,15 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 'help' : 'Add a comment according to your evaluation note. ' 'This is used by the global evaluation report (RP0004).'}), + ('confidence', + {'type' : 'multiple_choice', 'metavar': '<levels>', + 'default': '', + 'choices': [c.name for c in CONFIDENCE_LEVELS], + 'group': 'Messages control', + 'help' : 'Only show warnings with the listed confidence levels.' + ' Leave empty to show all. Valid levels: %s' % ( + ', '.join(c.name for c in CONFIDENCE_LEVELS),)}), + ('enable', {'type' : 'csv', 'metavar': '<msg ids>', 'short': 'e', @@ -879,6 +888,12 @@ group are mutually exclusive.'), 'group': 'Commands', 'level': 1, 'help' : "Generate pylint's messages."}), + ('list-conf-levels', + {'action' : 'callback', + 'callback' : self.cb_list_confidence_levels, + 'group': 'Commands', 'level': 1, + 'help' : "Generate pylint's messages."}), + ('full-documentation', {'action' : 'callback', 'metavar': '<msg-id>', 'callback' : self.cb_full_documentation, @@ -1037,6 +1052,11 @@ group are mutually exclusive.'), self.linter.msgs_store.list_messages() sys.exit(0) + def cb_list_confidence_levels(self, option, optname, value, parser): + for level in CONFIDENCE_LEVELS: + print '%-18s: %s' % level + sys.exit(0) + def cb_init_hook(optname, value): """exec arbitrary code to set sys.path for instance""" exec value diff --git a/test/functional/access_to__name__.txt b/test/functional/access_to__name__.txt index 2c0a953..ecf5ffd 100644 --- a/test/functional/access_to__name__.txt +++ b/test/functional/access_to__name__.txt @@ -1,3 +1,3 @@ old-style-class:7:Aaaa:Old-style class defined. -no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member -no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member +no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member:INFERENCE +no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member:INFERENCE diff --git a/test/functional/class_members_py27.txt b/test/functional/class_members_py27.txt index c7b017e..e5e6005 100644 --- a/test/functional/class_members_py27.txt +++ b/test/functional/class_members_py27.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member -no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member -no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member -no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member -no-member:50::Instance of 'TestMetaclass' has no 'register' member -no-member:51::Instance of 'UsingMetaclass' has no 'test' member +no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE +no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE +no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE +no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE +no-member:50::Instance of 'TestMetaclass' has no 'register' member:INFERENCE +no-member:51::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE diff --git a/test/functional/class_members_py30.txt b/test/functional/class_members_py30.txt index 0cb808f..4696579 100644 --- a/test/functional/class_members_py30.txt +++ b/test/functional/class_members_py30.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member
-no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member
-no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member
-no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member
-no-member:48::Instance of 'TestMetaclass' has no 'register' member
-no-member:49::Instance of 'UsingMetaclass' has no 'test' member
+no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE
+no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE
+no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE
+no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE
+no-member:48::Instance of 'TestMetaclass' has no 'register' member:INFERENCE
+no-member:49::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE
diff --git a/test/functional/confidence_filter.py b/test/functional/confidence_filter.py new file mode 100644 index 0000000..64cc8c4 --- /dev/null +++ b/test/functional/confidence_filter.py @@ -0,0 +1,14 @@ +"""Test for the confidence filter.""" + +class Client(object): + """use provider class""" + + def __init__(self): + self.set_later = 0 + + def set_set_later(self, value): + """set set_later attribute (introduce an inference ambiguity)""" + self.set_later = value + +print Client().set_later.lower() +print Client().foo # [no-member] diff --git a/test/functional/confidence_filter.rc b/test/functional/confidence_filter.rc new file mode 100644 index 0000000..5d21cb5 --- /dev/null +++ b/test/functional/confidence_filter.rc @@ -0,0 +1,3 @@ +[Messages Control] +disable=no-init,too-few-public-methods,undefined-variable +confidence=INFERENCE,HIGH,UNDEFINED diff --git a/test/functional/confidence_filter.txt b/test/functional/confidence_filter.txt new file mode 100644 index 0000000..308035d --- /dev/null +++ b/test/functional/confidence_filter.txt @@ -0,0 +1 @@ +no-member:14::Instance of 'Client' has no 'foo' member:INFERENCE diff --git a/test/functional/docstrings.txt b/test/functional/docstrings.txt index 177d02a..e30e462 100644 --- a/test/functional/docstrings.txt +++ b/test/functional/docstrings.txt @@ -2,7 +2,7 @@ missing-docstring:1::Missing module docstring empty-docstring:6:function0:Empty function docstring missing-docstring:10:function1:Missing function docstring missing-docstring:23:AAAA:Missing class docstring -missing-docstring:40:AAAA.method1:Missing method docstring -empty-docstring:48:AAAA.method3:Empty method docstring -empty-docstring:62:DDDD.method2:Empty method docstring -missing-docstring:70:DDDD.method4:Missing method docstring +missing-docstring:40:AAAA.method1:Missing method docstring:INFERENCE +empty-docstring:48:AAAA.method3:Empty method docstring:INFERENCE +empty-docstring:62:DDDD.method2:Empty method docstring:INFERENCE +missing-docstring:70:DDDD.method4:Missing method docstring:INFERENCE diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt index 3156a0b..ce9ab2a 100644 --- a/test/functional/invalid_exceptions_caught.txt +++ b/test/functional/invalid_exceptions_caught.txt @@ -1,6 +1,6 @@ catching-non-exception:25::"Catching an exception which doesn't inherit from BaseException: MyException" catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MyException" catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MySecondException" -catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: None -catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: list() -catching-non-exception:57::Catching an exception which doesn't inherit from BaseException: None +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: None" +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: list()" +catching-non-exception:57::"Catching an exception which doesn't inherit from BaseException: None" diff --git a/test/functional/invalid_exceptions_raised.txt b/test/functional/invalid_exceptions_raised.txt index c40eca7..54ea0f4 100644 --- a/test/functional/invalid_exceptions_raised.txt +++ b/test/functional/invalid_exceptions_raised.txt @@ -1,13 +1,9 @@ -nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class" -raising-non-exception:23:bad_case0:"""Raising a new style class which doesn't inherit from BaseException -"" -" +nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE +raising-non-exception:23:bad_case0:Raising a new style class which doesn't inherit from BaseException raising-non-exception:27:bad_case1:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class" +nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE old-raise-syntax:33:bad_case2:Use raise ErrorClass(args) instead of raise ErrorClass, args. -raising-non-exception:33:bad_case2:"""Raising a new style class which doesn't inherit from BaseException -"" -" +raising-non-exception:33:bad_case2:Raising a new style class which doesn't inherit from BaseException raising-non-exception:37:bad_case3:Raising a new style class which doesn't inherit from BaseException notimplemented-raised:42:bad_case4:NotImplemented raised - should raise NotImplementedError old-raise-syntax:42:bad_case4:Use raise ErrorClass(args) instead of raise ErrorClass, args. diff --git a/test/input/func_typecheck_getattr.py b/test/functional/member_checks.py index b120ca4..fccf160 100644 --- a/test/input/func_typecheck_getattr.py +++ b/test/functional/member_checks.py @@ -1,7 +1,6 @@ # pylint: disable= """check getattr if inference succeed""" -__revision__ = None class Provider(object): """provide some attributes and method""" @@ -16,13 +15,13 @@ class Provider(object): print 'hop hop hop', self -class Client: +class Client(object): """use provider class""" def __init__(self): self._prov = Provider() self._prov_attr = Provider.cattr - self._prov_attr2 = Provider.cattribute + self._prov_attr2 = Provider.cattribute # [no-member] self.set_later = 0 def set_set_later(self, value): @@ -32,12 +31,12 @@ class Client: def use_method(self): """use provider's method""" self._prov.hophop() - self._prov.hophophop() + self._prov.hophophop() # [no-member] def use_attr(self): """use provider's attr""" print self._prov.attr - print self._prov.attribute + print self._prov.attribute # [no-member] def debug(self): """print debug information""" @@ -49,21 +48,16 @@ class Client: def test_bt_types(self): """test access to unexistant member of builtin types""" lis = [] - lis.apppend(self) + lis.apppend(self) # [no-member] dic = {} - dic.set(self) + dic.set(self) # [no-member] tup = () - tup.append(self) + tup.append(self) # [no-member] string = 'toto' - print string.loower() - # unicode : moved to func_3k_removed_stuff_py_30.py - # + print string.loower() # [no-member] integer = 1 - print integer.whatever + print integer.whatever # [no-member] print object.__init__ print property.__init__ -print Client().set_later.lower() - -# should detect mixing new style / old style classes -Client.__bases__ += (object,) +print Client().set_later.lower() # [no-member] diff --git a/test/functional/member_checks.txt b/test/functional/member_checks.txt new file mode 100644 index 0000000..12fe6ee --- /dev/null +++ b/test/functional/member_checks.txt @@ -0,0 +1,9 @@ +no-member:24:Client.__init__:Class 'Provider' has no 'cattribute' member:INFERENCE +no-member:34:Client.use_method:Instance of 'Provider' has no 'hophophop' member:INFERENCE +no-member:39:Client.use_attr:Instance of 'Provider' has no 'attribute' member:INFERENCE +no-member:51:Client.test_bt_types:Instance of 'list' has no 'apppend' member:INFERENCE +no-member:53:Client.test_bt_types:Instance of 'dict' has no 'set' member:INFERENCE +no-member:55:Client.test_bt_types:Instance of 'tuple' has no 'append' member:INFERENCE +no-member:57:Client.test_bt_types:Instance of 'str' has no 'loower' member:INFERENCE +no-member:59:Client.test_bt_types:Instance of 'int' has no 'whatever' member:INFERENCE +no-member:63::Instance of 'int' has no 'lower' member:INFERENCE_FAILURE diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt index 985e9a2..e81d27a 100644 --- a/test/functional/name_styles.txt +++ b/test/functional/name_styles.txt @@ -6,12 +6,12 @@ invalid-name:25:bad_class_name:"Invalid class name ""bad_class_name""" invalid-name:36:CorrectClassName.__init__:"Invalid attribute name ""_Bad_AtTR_name""" invalid-name:37:CorrectClassName.__init__:"Invalid attribute name ""Bad_PUBLIC_name""" invalid-name:39:CorrectClassName:"Invalid class attribute name ""zz""" -invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""" -invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""" +invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""":INFERENCE +invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""":INFERENCE invalid-name:78::"Invalid class name ""BAD_NAME_FOR_CLASS""" invalid-name:79::"Invalid class name ""NEXT_BAD_NAME_FOR_CLASS""" invalid-name:86::"Invalid class name ""NOT_CORRECT""" invalid-name:92:test_globals:"Invalid constant name ""AlsoCorrect""" -invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""" -invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""" -invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""" +invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""":INFERENCE +invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""":INFERENCE +invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""":INFERENCE diff --git a/test/functional/namedtuple_member_inference.txt b/test/functional/namedtuple_member_inference.txt index 87d9da4..308336f 100644 --- a/test/functional/namedtuple_member_inference.txt +++ b/test/functional/namedtuple_member_inference.txt @@ -1,3 +1,3 @@ -no-member:15:test:Class 'Thing' has no 'x' member +no-member:15:test:Class 'Thing' has no 'x' member:INFERENCE protected-access:19:test:Access to a protected member _replace of a client class -no-member:21:test:Instance of 'Fantastic' has no 'foo' member +no-member:21:test:Instance of 'Fantastic' has no 'foo' member:INFERENCE diff --git a/test/functional/newstyle__slots__.txt b/test/functional/newstyle__slots__.txt index afc9117..4320390 100644 --- a/test/functional/newstyle__slots__.txt +++ b/test/functional/newstyle__slots__.txt @@ -1,2 +1,2 @@ old-style-class:10:OldStyleClass:Old-style class defined. -slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class +slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class:INFERENCE diff --git a/test/functional/newstyle_properties.txt b/test/functional/newstyle_properties.txt index 9f060c0..a16686b 100644 --- a/test/functional/newstyle_properties.txt +++ b/test/functional/newstyle_properties.txt @@ -1,2 +1,2 @@ old-style-class:13:OldStyleClass:Old-style class defined. -property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class" +property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class":INFERENCE diff --git a/test/functional/super_checks.txt b/test/functional/super_checks.txt index 2d69098..79f6ae1 100644 --- a/test/functional/super_checks.txt +++ b/test/functional/super_checks.txt @@ -1,8 +1,8 @@ old-style-class:6:Aaaa:Old-style class defined. -super-on-old-class:8:Aaaa.hop:Use of super on an old style class -super-on-old-class:12:Aaaa.__init__:Use of super on an old style class -bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super() -missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super() -bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super() -bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super() -bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super() +super-on-old-class:8:Aaaa.hop:Use of super on an old style class:INFERENCE +super-on-old-class:12:Aaaa.__init__:Use of super on an old style class:INFERENCE +bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super():INFERENCE +missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super():INFERENCE +bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE +bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super():INFERENCE +bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE diff --git a/test/messages/func_typecheck_getattr.txt b/test/messages/func_typecheck_getattr.txt deleted file mode 100644 index bd91228..0000000 --- a/test/messages/func_typecheck_getattr.txt +++ /dev/null @@ -1,10 +0,0 @@ -C: 19:Client: Old-style class defined. -E: 25:Client.__init__: Class 'Provider' has no 'cattribute' member -E: 35:Client.use_method: Instance of 'Provider' has no 'hophophop' member -E: 40:Client.use_attr: Instance of 'Provider' has no 'attribute' member -E: 52:Client.test_bt_types: Instance of 'list' has no 'apppend' member -E: 54:Client.test_bt_types: Instance of 'dict' has no 'set' member -E: 56:Client.test_bt_types: Instance of 'tuple' has no 'append' member -E: 58:Client.test_bt_types: Instance of 'str' has no 'loower' member -E: 62:Client.test_bt_types: Instance of 'int' has no 'whatever' member -E: 66: Instance of 'int' has no 'lower' member (but some types could not be inferred) diff --git a/test/test_functional.py b/test/test_functional.py index 759a447..c2467b3 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -11,6 +11,7 @@ import sys import unittest from pylint import checkers +from pylint import interfaces from pylint import lint from pylint import reporters from pylint import utils @@ -30,6 +31,10 @@ csv.register_dialect('test', test_dialect) class NoFileError(Exception): pass +# Notes: +# - for the purpose of this test, the confidence levels HIGH and UNDEFINED +# are treated as the same. + # TODOs # - implement exhaustivity tests @@ -37,14 +42,24 @@ class NoFileError(Exception): UPDATE = False class OutputLine(collections.namedtuple('OutputLine', - ['symbol', 'lineno', 'object', 'msg'])): + ['symbol', 'lineno', 'object', 'msg', 'confidence'])): @classmethod def from_msg(cls, msg): - return cls(msg.symbol, msg.line, msg.obj or '', msg.msg) + return cls( + msg.symbol, msg.line, msg.obj or '', msg.msg, + msg.confidence.name + if msg.confidence != interfaces.UNDEFINED else interfaces.HIGH.name) @classmethod def from_csv(cls, row): - return cls(row[0], int(row[1]), row[2], row[3]) + confidence = row[4] if len(row) == 5 else interfaces.HIGH.name + return cls(row[0], int(row[1]), row[2], row[3], confidence) + + def to_csv(self): + if self.confidence == interfaces.HIGH.name: + return self[:-1] + else: + return self # Common sub-expressions. @@ -292,7 +307,7 @@ class LintModuleOutputUpdate(LintModuleTest): try: return super(LintModuleOutputUpdate, self)._open_expected_file() except IOError: - return contextlib.closing(cStringIO.StringIO()) + return io.StringIO() def _check_output_text(self, expected_messages, expected_lines, received_lines): @@ -305,8 +320,7 @@ class LintModuleOutputUpdate(LintModuleTest): with open(self._test_file.expected_output, 'w') as fobj: writer = csv.writer(fobj, dialect='test') for line in remaining: - #fobj.write('{0}:{1}:{2}:{3}'.format(*line)) - writer.writerow(line) + writer.writerow(line.to_csv()) def suite(): input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), diff --git a/test/unittest_lint.py b/test/unittest_lint.py index b2022cc..7dc7090 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -26,12 +26,13 @@ from logilab.common.compat import reload from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError -from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ +from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, MSG_STATE_CONFIDENCE, \ MessagesStore, PyLintASTWalker, MessageDefinition, FileState, \ build_message_def, tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers +from pylint import interfaces if sys.platform == 'win32': HOME = 'USERPROFILE' @@ -139,19 +140,26 @@ class PyLinterTC(unittest.TestCase): self.assertTrue(linter.is_message_enabled('C0121', line=1)) def test_message_state_scope(self): + class FakeConfig(object): + confidence = ['HIGH'] + linter = self.init_linter() - fs = linter.file_state linter.disable('C0121') self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) linter.disable('W0101', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0101', 3)) + linter.get_message_state_scope('W0101', 3)) linter.enable('W0102', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0102', 3)) + linter.get_message_state_scope('W0102', 3)) + linter.config = FakeConfig() + self.assertEqual( + MSG_STATE_CONFIDENCE, + linter.get_message_state_scope('this-is-bad', + confidence=interfaces.INFERENCE)) def test_enable_message_block(self): linter = self.init_linter() diff --git a/test/unittest_utils.py b/test/unittest_utils.py index ef0cda2..d631dbb 100644 --- a/test/unittest_utils.py +++ b/test/unittest_utils.py @@ -15,6 +15,7 @@ import unittest from astroid import test_utils from pylint import utils +from pylint import interfaces from pylint.checkers.utils import check_messages @@ -59,4 +60,3 @@ class PyLintASTWalkerTest(unittest.TestCase): if __name__ == '__main__': unittest.main() - diff --git a/testutils.py b/testutils.py index e53ec70..86539ac 100644 --- a/testutils.py +++ b/testutils.py @@ -141,7 +141,7 @@ class UnittestLinter(object): finally: self._messages = [] - def add_message(self, msg_id, line=None, node=None, args=None): + def add_message(self, msg_id, line=None, node=None, args=None, confidence=None): self._messages.append(Message(msg_id, line, node, args)) def is_message_enabled(self, *unused_args): @@ -35,7 +35,7 @@ from astroid import nodes, Module from astroid.modutils import modpath_from_file, get_module_files, \ file_from_modpath, load_module_from_file -from pylint.interfaces import IRawChecker, ITokenChecker +from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED class UnknownMessage(Exception): @@ -67,6 +67,7 @@ MSG_TYPES_STATUS = { _MSG_ORDER = 'EWRCIF' MSG_STATE_SCOPE_CONFIG = 0 MSG_STATE_SCOPE_MODULE = 1 +MSG_STATE_CONFIDENCE = 2 OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') @@ -79,15 +80,16 @@ class WarningScope(object): _MsgBase = collections.namedtuple( '_MsgBase', - ['msg_id', 'symbol', 'msg', 'C', 'category', 'abspath', 'module', 'obj', - 'line', 'column']) + ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence', + 'abspath', 'module', 'obj', 'line', 'column']) class Message(_MsgBase): """This class represent a message to be issued by the reporters""" - def __new__(cls, msg_id, symbol, location, msg): + def __new__(cls, msg_id, symbol, location, msg, confidence): return _MsgBase.__new__( - cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], *location) + cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], + confidence, *location) def format(self, template): """Format the message according to the given template. @@ -302,12 +304,25 @@ class MessagesHandlerMixIn(object): # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - def is_message_enabled(self, msg_descr, line=None): + def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED): + """Returns the scope at which a message was enabled/disabled.""" + if self.config.confidence and confidence.name not in self.config.confidence: + return MSG_STATE_CONFIDENCE + try: + if line in self.file_state._module_msgs_state[msgid]: + return MSG_STATE_SCOPE_MODULE + except (KeyError, TypeError): + return MSG_STATE_SCOPE_CONFIG + + def is_message_enabled(self, msg_descr, line=None, confidence=None): """return true if the message associated to the given message id is enabled msgid may be either a numeric or symbolic message id. """ + if self.config.confidence and confidence: + if confidence.name not in self.config.confidence: + return False try: msgid = self.msgs_store.check_message_id(msg_descr).msgid except UnknownMessage: @@ -322,7 +337,7 @@ class MessagesHandlerMixIn(object): except KeyError: return self._msgs_state.get(msgid, True) - def add_message(self, msg_descr, line=None, node=None, args=None): + def add_message(self, msg_descr, line=None, node=None, args=None, confidence=UNDEFINED): """Adds a message given by ID or name. If provided, the message string is expanded using args @@ -352,8 +367,10 @@ class MessagesHandlerMixIn(object): else: col_offset = None # should this message be displayed - if not self.is_message_enabled(msgid, line): - self.file_state.handle_ignored_message(msgid, line, node, args) + if not self.is_message_enabled(msgid, line, confidence): + self.file_state.handle_ignored_message( + self.get_message_state_scope(msgid, line, confidence), + msgid, line, node, args, confidence) return # update stats msg_cat = MSG_TYPES[msgid[0]] @@ -377,8 +394,8 @@ class MessagesHandlerMixIn(object): path = node.root().file # add the message self.reporter.handle_message( - Message(msgid, symbol, - (path, module, obj, line or 1, col_offset or 0), msg)) + Message(msgid, symbol, + (path, module, obj, line or 1, col_offset or 0), msg, confidence)) def print_full_documentation(self): """output a full documentation in ReST format""" @@ -519,14 +536,13 @@ class FileState(object): except KeyError: self._module_msgs_state[msg.msgid] = {line: status} - def handle_ignored_message(self, msgid, line, node, args): + def handle_ignored_message(self, state_scope, msgid, line, node, args, confidence): """Report an ignored message. state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, depending on whether the message was disabled locally in the module, or globally. The other arguments are the same as for add_message. """ - state_scope = self._message_state_scope(msgid, line) if state_scope == MSG_STATE_SCOPE_MODULE: try: orig_line = self._suppression_mapping[(msgid, line)] @@ -534,14 +550,6 @@ class FileState(object): except KeyError: pass - def _message_state_scope(self, msgid, line=None): - """Returns the scope at which a message was enabled/disabled.""" - try: - if line in self._module_msgs_state[msgid]: - return MSG_STATE_SCOPE_MODULE - except KeyError: - return MSG_STATE_SCOPE_CONFIG - def iter_spurious_suppression_messages(self, msgs_store): for warning, lines in self._raw_module_msgs_state.iteritems(): for line, enable in lines.iteritems(): |
