DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Mercurial (1aeaa33a64f9)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
# 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
from mozlint.pathutils import get_ancestors_by_name


here = os.path.abspath(os.path.dirname(__file__))
YAMLLINT_REQUIREMENTS_PATH = os.path.join(here, 'yamllint_requirements.txt')


YAMLLINT_INSTALL_ERROR = """
Unable to install correct version of yamllint
Try to install it manually with:
    $ pip install -U --require-hashes -r {}
""".strip().format(YAMLLINT_REQUIREMENTS_PATH)

YAMLLINT_FORMAT_REGEX = re.compile(r'(.*):(.*):(.*): \[(error|warning)\] (.*) \((.*)\)$')

results = []


class YAMLLintProcess(ProcessHandlerMixin):
    def __init__(self, config, *args, **kwargs):
        self.config = config
        kwargs['processOutputLine'] = [self.process_line]
        ProcessHandlerMixin.__init__(self, *args, **kwargs)

    def process_line(self, line):
        try:
            match = YAMLLINT_FORMAT_REGEX.match(line)
            abspath, line, col, level, message, code = match.groups()
        except AttributeError:
            print('Unable to match yaml regex against output: {}'.format(line))
            return

        res = {'path': os.path.relpath(abspath, self.config['root']),
               'message': message,
               'level': level,
               'lineno': line,
               'column': col,
               'rule': code,
               }

        results.append(result.from_config(self.config, **res))

    def run(self, *args, **kwargs):
        # protect against poor SIGINT handling. Handle it here instead
        # so we can kill the process without a cryptic traceback.
        orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
        ProcessHandlerMixin.run(self, *args, **kwargs)
        signal.signal(signal.SIGINT, orig)


def get_yamllint_binary():
    """
    Returns the path of the first yamllint binary available
    if not found returns None
    """
    binary = os.environ.get('YAMLLINT')
    if binary:
        return binary

    try:
        return which.which('yamllint')
    except which.WhichError:
        return None


def _run_pip(*args):
    """
    Helper function that runs pip with subprocess
    """
    try:
        subprocess.check_output(['pip'] + list(args),
                                stderr=subprocess.STDOUT)
        return True
    except subprocess.CalledProcessError as e:
        print(e.output)
        return False


def reinstall_yamllint():
    """
    Try to install yamllint at the target version, returns True on success
    otherwise prints the otuput of the pip command and returns False
    """
    if _run_pip('install', '-U',
                '--require-hashes', '-r',
                YAMLLINT_REQUIREMENTS_PATH):
        return True

    return False


def run_process(config, cmd):
    proc = YAMLLintProcess(config, cmd)
    proc.run()
    try:
        proc.wait()
    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 and conf_file != 'default':
        return args + ['-c', conf_file] + paths
    return args + paths


def lint(files, config, **lintargs):
    if not reinstall_yamllint():
        print(YAMLLINT_INSTALL_ERROR)
        return 1

    binary = get_yamllint_binary()

    cmdargs = [
        binary,
        '-f', 'parsable'
    ]

    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.
    paths_by_config = defaultdict(list)
    for f in files:
        conf_files = get_ancestors_by_name('.yamllint', f, config['root'])
        paths_by_config[conf_files[0] if conf_files else 'default'].append(f)

    for conf_file, paths in paths_by_config.items():
        run_process(config, gen_yamllint_args(cmdargs, conf_file=conf_file, paths=paths))

    return results