Commits

Torsten Marek committed 323c8c0

Change the multi-style name checker from first-style-wins to majority-style-wins.

Comments (0)

Files changed (3)

 # 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
         _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,
     @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):
                 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):
 
         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):
 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::
 
 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,

test/unittest_checker_base.py

     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 classb(object): #@
+            pass
         class CLASSA(object): #@
             pass
-        class classb(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):
                                      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,
         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):
         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__':