tools/lint/flake8.lint
author Jonathan Hao <jhao@mozilla.com>
Thu, 12 Jan 2017 14:58:04 +0800
changeset 462919 24c57a372151b229c5b5fcd976a86e999cc768e4
parent 449910 801e9e6efb1742fd3e086685cec6698c8d05a995
child 484822 868a4dc22d7a40d78a620c2939be59392b7822be
permissions -rw-r--r--
Bug 1115712 - make DataStorage for HPKP and HSTS enumerable via xpcom. r=keeler,Cykesiopka MozReview-Commit-ID: GEOtuTAiPIX

# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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 json
import os
import signal
import subprocess

import which
from mozprocess import ProcessHandler

from mozlint import result


here = os.path.abspath(os.path.dirname(__file__))
FLAKE8_REQUIREMENTS_PATH = os.path.join(here, 'flake8', 'flake8_requirements.txt')

FLAKE8_NOT_FOUND = """
Could not find flake8! Install flake8 and try again.

    $ pip install -U --require-hashes -r {}
""".strip().format(FLAKE8_REQUIREMENTS_PATH)


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

LINE_OFFSETS = {
    # continuation line under-indented for hanging indent
    'E121': (-1, 2),
    # continuation line missing indentation or outdented
    'E122': (-1, 2),
    # continuation line over-indented for hanging indent
    'E126': (-1, 2),
    # continuation line over-indented for visual indent
    'E127': (-1, 2),
    # continuation line under-indented for visual indent
    'E128': (-1, 2),
    # continuation line unaligned for hanging indend
    'E131': (-1, 2),
    # expected 1 blank line, found 0
    'E301': (-1, 2),
    # expected 2 blank lines, found 1
    'E302': (-2, 3),
}
"""Maps a flake8 error to a lineoffset tuple.

The offset is of the form (lineno_offset, num_lines) and is passed
to the lineoffset property of `ResultContainer`.
"""

EXTENSIONS = ['.py', '.lint']
results = []


def process_line(line):
    # Escape slashes otherwise JSON conversion will not work
    line = line.replace('\\', '\\\\')
    try:
        res = json.loads(line)
    except ValueError:
        print('Non JSON output from linter, will not be processed: {}'.format(line))
        return

    if 'code' in res:
        if res['code'].startswith('W'):
            res['level'] = 'warning'

        if res['code'] in LINE_OFFSETS:
            res['lineoffset'] = LINE_OFFSETS[res['code']]

    results.append(result.from_linter(LINTER, **res))


def run_process(cmdargs):
    # flake8 seems to handle SIGINT poorly. Handle it here instead
    # so we can kill the process without a cryptic traceback.
    orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
    proc = ProcessHandler(cmdargs, env=os.environ,
                          processOutputLine=process_line)
    proc.run()
    signal.signal(signal.SIGINT, orig)

    try:
        proc.wait()
    except KeyboardInterrupt:
        proc.kill()


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

    try:
        return which.which('flake8')
    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_flake8():
    """
    Try to install flake8 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',
                FLAKE8_REQUIREMENTS_PATH):
        return True

    return False


def lint(files, **lintargs):

    if not reinstall_flake8():
        print(FLAKE8_INSTALL_ERROR)
        return 1

    binary = get_flake8_binary()

    cmdargs = [
        binary,
        '--format', '{"path":"%(path)s","lineno":%(row)s,'
                    '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
    ]

    # Run any paths with a .flake8 file in the directory separately so
    # it gets picked up. This means only .flake8 files that live in
    # directories that are explicitly included will be considered.
    # See bug 1277851
    no_config = []
    for f in files:
        if not os.path.isfile(os.path.join(f, '.flake8')):
            no_config.append(f)
            continue
        run_process(cmdargs+[f])

    # XXX For some reason passing in --exclude results in flake8 not using
    # the local .flake8 file. So for now only pass in --exclude if there
    # is no local config.
    exclude = lintargs.get('exclude')
    if exclude:
        cmdargs += ['--exclude', ','.join(lintargs['exclude'])]

    if no_config:
        run_process(cmdargs+no_config)

    return results


LINTER = {
    'name': "flake8",
    'description': "Python linter",
    'include': [
        'python/mozlint',
        'security/manager',
        'taskcluster',
        'testing/firefox-ui',
        'testing/marionette/client',
        'testing/marionette/harness',
        'testing/marionette/puppeteer',
        'testing/mozbase',
        'testing/mochitest',
        'testing/talos/',
        'tools/lint',
    ],
    'exclude': ["testing/mozbase/mozdevice/mozdevice/Zeroconf.py",
                'testing/mochitest/pywebsocket'],
    'extensions': EXTENSIONS,
    'type': 'external',
    'payload': lint,
}