Bug 1392795 - [yamllint] Group paths to lint by their closest config and run each config group separately, r?Callek draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 23 Aug 2017 11:28:28 -0400
changeset 651587 bb39bc9942ac85ed4b13fa4230a82fe079e830eb
parent 651330 446cd9f4b0f5684adaf275a999e0361988a95cfe
child 727826 dd84809ecbdbccb6496e98e58c363a36a9f2a800
push id75770
push userahalberstadt@mozilla.com
push dateWed, 23 Aug 2017 21:24:31 +0000
reviewersCallek
bugs1392795
milestone57.0a1
Bug 1392795 - [yamllint] Group paths to lint by their closest config and run each config group separately, r?Callek This makes configuration files for yamllint work a bit better. It's still not perfect, but it's an improvement on the current situation. MozReview-Commit-ID: IKxgQm1a7bP
.yamllint
taskcluster/.yamllint
tools/lint/yamllint_/__init__.py
new file mode 100644
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,5 @@
+---
+ignore: |
+  *node_modules*
+
+extends: default
--- a/taskcluster/.yamllint
+++ b/taskcluster/.yamllint
@@ -1,9 +1,11 @@
 ---
+ignore: |
+  *node_modules*
 
 extends: default
 
 rules:
   document-end:
      present: false
   # Checks currently failing
   brackets: disable
--- a/tools/lint/yamllint_/__init__.py
+++ b/tools/lint/yamllint_/__init__.py
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import re
 import os
 import signal
 import subprocess
+from collections import defaultdict
 
 import which
 from mozprocess import ProcessHandlerMixin
 
 from mozlint import result
 
 
 here = os.path.abspath(os.path.dirname(__file__))
@@ -109,23 +110,45 @@ def run_process(config, cmd):
     except KeyboardInterrupt:
         proc.kill()
 
 
 def gen_yamllint_args(cmdargs, paths=None, conf_file=None):
     args = cmdargs[:]
     if isinstance(paths, basestring):
         paths = [paths]
-    if conf_file:
+    if conf_file and conf_file != 'default':
         return args + ['-c', conf_file] + paths
     return args + paths
 
 
+def ancestors(path):
+    while path:
+        yield path
+        (path, child) = os.path.split(path)
+        if child == "":
+            break
+
+
+def get_relevant_configs(name, path, root):
+    """Returns a list of configuration files that exist in `path`'s ancestors,
+    sorted from closest->furthest.
+    """
+    configs = []
+    for path in ancestors(path):
+        if path == root:
+            break
+
+        config = os.path.join(path, name)
+        if os.path.isfile(config):
+            configs.append(config)
+    return configs
+
+
 def lint(files, config, **lintargs):
-
     if not reinstall_yamllint():
         print(YAMLLINT_INSTALL_ERROR)
         return 1
 
     binary = get_yamllint_binary()
 
     cmdargs = [
         binary,
@@ -133,22 +156,17 @@ def lint(files, config, **lintargs):
     ]
 
     config = config.copy()
     config['root'] = lintargs['root']
 
     # Run any paths with a .yamllint file in the directory separately so
     # it gets picked up. This means only .yamllint files that live in
     # directories that are explicitly included will be considered.
-    no_config = []
+    paths_by_config = defaultdict(list)
     for f in files:
-        yamllint_config = os.path.join(f, '.yamllint')
-        if not os.path.isfile(yamllint_config):
-            no_config.append(f)
-            continue
-        run_process(config,
-                    gen_yamllint_args(cmdargs, conf_file=yamllint_config, paths=f))
+        conf_files = get_relevant_configs('.yamllint', f, config['root'])
+        paths_by_config[conf_files[0] if conf_files else 'default'].append(f)
 
-    if no_config:
-        run_process(config,
-                    gen_yamllint_args(cmdargs, paths=no_config))
+    for conf_file, paths in paths_by_config.items():
+        run_process(config, gen_yamllint_args(cmdargs, conf_file=conf_file, paths=paths))
 
     return results