summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Marek <shlomme@gmail.com>2014-07-24 14:13:09 +0200
committerTorsten Marek <shlomme@gmail.com>2014-07-24 14:13:09 +0200
commitfaae5267f9541ffbce013f3505e26295698a43b9 (patch)
tree9f309e21d8f3184da5d80cde0253d4abb62c53a0
parent54e8ee1d68f66b5a9f89d48209c24494a56d68d9 (diff)
downloadpylint-faae5267f9541ffbce013f3505e26295698a43b9.tar.gz
Change the multi-style name checker from first-style-wins to majority-style-wins.
-rw-r--r--checkers/base.py44
-rw-r--r--doc/options.rst13
-rw-r--r--test/unittest_checker_base.py12
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__':