diff options
| author | Torsten Marek <shlomme@gmail.com> | 2014-07-24 14:13:09 +0200 |
|---|---|---|
| committer | Torsten Marek <shlomme@gmail.com> | 2014-07-24 14:13:09 +0200 |
| commit | faae5267f9541ffbce013f3505e26295698a43b9 (patch) | |
| tree | 9f309e21d8f3184da5d80cde0253d4abb62c53a0 | |
| parent | 54e8ee1d68f66b5a9f89d48209c24494a56d68d9 (diff) | |
| download | pylint-faae5267f9541ffbce013f3505e26295698a43b9.tar.gz | |
Change the multi-style name checker from first-style-wins to majority-style-wins.
| -rw-r--r-- | checkers/base.py | 44 | ||||
| -rw-r--r-- | doc/options.rst | 13 | ||||
| -rw-r--r-- | test/unittest_checker_base.py | 12 |
3 files changed, 49 insertions, 20 deletions
diff --git a/checkers/base.py b/checkers/base.py index 6e5804f..d6553ed 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -16,6 +16,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """basic checker for Python code""" +import collections +import itertools import sys import astroid from logilab.common.ureports import Table @@ -944,6 +946,7 @@ class NameChecker(_BasicChecker): _BasicChecker.__init__(self, linter) self._name_category = {} self._name_group = {} + self._bad_names = {} def open(self): self.stats = self.linter.add_stats(badname_module=0, @@ -961,6 +964,25 @@ class NameChecker(_BasicChecker): @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): self._check_name('module', node.name.split('.')[-1], node) + self._bad_names = {} + + def leave_module(self, node): + for category, all_groups in self._bad_names.iteritems(): + if len(all_groups) < 2: + continue + groups = collections.defaultdict(list) + min_warnings = sys.maxint + for group in all_groups.itervalues(): + groups[len(group)].append(group) + min_warnings = min(len(group), min_warnings) + if len(groups[min_warnings]) > 1: + by_line = sorted(groups[min_warnings], + key=lambda group: min(warning[0].lineno for warning in group)) + warnings = itertools.chain(*by_line[1:]) + else: + warnings = groups[min_warnings][0] + for args in warnings: + self._raise_name_warning(*args) @check_messages('blacklisted-name', 'invalid-name') def visit_class(self, node): @@ -1030,6 +1052,14 @@ class NameChecker(_BasicChecker): match.lastgroup is not None and match.lastgroup not in EXEMPT_NAME_CATEGORIES) + def _raise_name_warning(self, node, node_type, name): + 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.stats['badname_' + node_type] += 1 + def _check_name(self, node_type, name, node): """check for a name using the type's regexp""" if is_inside_except(node): @@ -1047,18 +1077,12 @@ class NameChecker(_BasicChecker): if self._is_multi_naming_match(match): name_group = self._find_name_group(node_type) - if name_group not in self._name_category: - self._name_category[name_group] = match.lastgroup - elif self._name_category[name_group] != match.lastgroup: - match = None + bad_name_group = self._bad_names.setdefault(name_group, {}) + warnings = bad_name_group.setdefault(match.lastgroup, []) + warnings.append((node, node_type, name)) if match is None: - 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.stats['badname_' + node_type] += 1 + self._raise_name_warning(node, node_type, name) class DocStringChecker(_BasicChecker): diff --git a/doc/options.rst b/doc/options.rst index 4a159f2..702d93c 100644 --- a/doc/options.rst +++ b/doc/options.rst @@ -91,8 +91,8 @@ However, intra-module consistency should still be required, to make changes inside a single file easier. For this case, PyLint supports regular expression with several named capturing group. -The capturing group of the first valid match taints the module and enforces the -same group to be triggered on every subsequent occurrence of this name. +Rather than emitting name warnings immediately, PyLint will determine the +prevalent naming style inside each module and enforce it on all names. Consider the following (simplified) example:: @@ -101,16 +101,17 @@ Consider the following (simplified) example:: The regular expression defines two naming styles, ``snake`` for snake-case names, and ``camel`` for camel-case names. -In ``sample.py``, the function name on line 1 will taint the module and enforce -the match of named group ``snake`` for the remainder of the module:: +In ``sample.py``, the function name on line 1 and 7 will mark the module +and enforce the match of named group ``snake`` for the remaining names in +the module:: - def trigger_snake_case(arg): + def valid_snake_case(arg): ... def InvalidCamelCase(arg): ... - def valid_snake_case(arg): + def more_valid_snake_case(arg): ... Because of this, the name on line 4 will trigger an ``invalid-name`` warning, diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py index fe0dbca..4f16a77 100644 --- a/test/unittest_checker_base.py +++ b/test/unittest_checker_base.py @@ -161,18 +161,19 @@ class MultiNamingStyleTest(CheckerTestCase): MULTI_STYLE_RE = re.compile('(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$') @set_config(class_rgx=MULTI_STYLE_RE) - def test_multi_name_detection_first(self): + def test_multi_name_detection_majority(self): classes = test_utils.extract_node(""" - class CLASSA(object): #@ - pass class classb(object): #@ pass + class CLASSA(object): #@ + pass class CLASSC(object): #@ pass """) - with self.assertAddsMessages(Message('invalid-name', node=classes[1], args=('class', 'classb', ''))): + with self.assertAddsMessages(Message('invalid-name', node=classes[0], args=('class', 'classb', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(class_rgx=MULTI_STYLE_RE) def test_multi_name_detection_first_invalid(self): @@ -188,6 +189,7 @@ class MultiNamingStyleTest(CheckerTestCase): Message('invalid-name', node=classes[2], args=('class', 'CLASSC', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(method_rgx=MULTI_STYLE_RE, function_rgx=MULTI_STYLE_RE, @@ -204,6 +206,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[1], args=('function', 'FUNC', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) @set_config(function_rgx=re.compile('(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$')) def test_multi_name_detection_exempt(self): @@ -220,6 +223,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[3], args=('function', 'UPPER', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) if __name__ == '__main__': |
