зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1230962 - Add python/mozlint for running several linters at once, r=smacleod
Mozlint provides two main benefits: 1. A common system for defining lints across multiple languages 2. A common interface and result format for running them This commit only adds the core library, it does not add any consumers of mozlint just yet. MozReview-Commit-ID: CSQzq5del5k --HG-- extra : rebase_source : b520b96177281a1b1770edf53a01cbc2196f494f
This commit is contained in:
Родитель
70ba843b03
Коммит
bb96d51342
|
@ -0,0 +1,7 @@
|
|||
# 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/.
|
||||
# flake8: noqa
|
||||
|
||||
from .roller import LintRoller
|
||||
from .result import ResultContainer
|
|
@ -0,0 +1,25 @@
|
|||
# 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 os
|
||||
|
||||
|
||||
class LintException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LinterNotFound(LintException):
|
||||
def __init__(self, path):
|
||||
LintException.__init__(self, "Could not find lint file '{}'".format(path))
|
||||
|
||||
|
||||
class LinterParseError(LintException):
|
||||
def __init__(self, path, message):
|
||||
LintException.__init__(self, "{}: {}".format(os.path.basename(path), message))
|
||||
|
||||
|
||||
class LintersNotConfigured(LintException):
|
||||
def __init__(self):
|
||||
LintException.__init__(self, "No linters registered! Use `LintRoller.read` "
|
||||
"to register a linter.")
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
||||
|
||||
from ..result import ResultEncoder
|
||||
from .stylish import StylishFormatter
|
||||
|
||||
|
||||
class JSONFormatter(object):
|
||||
def __call__(self, results):
|
||||
return json.dumps(results, cls=ResultEncoder)
|
||||
|
||||
|
||||
all_formatters = {
|
||||
'json': JSONFormatter,
|
||||
'stylish': StylishFormatter,
|
||||
}
|
||||
|
||||
|
||||
def get(name, **fmtargs):
|
||||
return all_formatters[name](**fmtargs)
|
|
@ -0,0 +1,104 @@
|
|||
# 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/.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..result import ResultContainer
|
||||
|
||||
try:
|
||||
import blessings
|
||||
except ImportError:
|
||||
blessings = None
|
||||
|
||||
|
||||
class NullTerminal(object):
|
||||
"""Replacement for `blessings.Terminal()` that does no formatting."""
|
||||
class NullCallableString(unicode):
|
||||
"""A dummy callable Unicode stolen from blessings"""
|
||||
def __new__(cls):
|
||||
new = unicode.__new__(cls, u'')
|
||||
return new
|
||||
|
||||
def __call__(self, *args):
|
||||
if len(args) != 1 or isinstance(args[0], int):
|
||||
return u''
|
||||
return args[0]
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return self.NullCallableString()
|
||||
|
||||
|
||||
class StylishFormatter(object):
|
||||
"""Formatter based on the eslint default."""
|
||||
|
||||
fmt = " {c1}{lineno}:{column} {c2}{level}{normal} {message} {c1}{rule}({linter}){normal}"
|
||||
fmt_summary = "{t.bold}{c}\u2716 {problem} ({error}, {warning}){t.normal}"
|
||||
|
||||
def __init__(self, disable_colors=None):
|
||||
if disable_colors or not blessings:
|
||||
self.term = NullTerminal()
|
||||
else:
|
||||
self.term = blessings.Terminal()
|
||||
|
||||
def _reset_max(self):
|
||||
self.max_lineno = 0
|
||||
self.max_column = 0
|
||||
self.max_level = 0
|
||||
self.max_message = 0
|
||||
|
||||
def _update_max(self, err):
|
||||
"""Calculates the longest length of each token for spacing."""
|
||||
self.max_lineno = max(self.max_lineno, len(str(err.lineno)))
|
||||
self.max_column = max(self.max_column, len(str(err.column)))
|
||||
self.max_level = max(self.max_level, len(str(err.level)))
|
||||
self.max_message = max(self.max_message, len(err.message))
|
||||
|
||||
def _pluralize(self, s, num):
|
||||
if num != 1:
|
||||
s += 's'
|
||||
return str(num) + ' ' + s
|
||||
|
||||
def __call__(self, result):
|
||||
message = []
|
||||
|
||||
num_errors = 0
|
||||
num_warnings = 0
|
||||
for path, errors in sorted(result.iteritems()):
|
||||
self._reset_max()
|
||||
|
||||
message.append(self.term.underline(path))
|
||||
# Do a first pass to calculate required padding
|
||||
for err in errors:
|
||||
assert isinstance(err, ResultContainer)
|
||||
self._update_max(err)
|
||||
if err.level == 'error':
|
||||
num_errors += 1
|
||||
else:
|
||||
num_warnings += 1
|
||||
|
||||
for err in errors:
|
||||
message.append(self.fmt.format(
|
||||
normal=self.term.normal,
|
||||
c1=self.term.color(8),
|
||||
c2=self.term.color(1) if err.level == 'error' else self.term.color(3),
|
||||
lineno=str(err.lineno).rjust(self.max_lineno),
|
||||
column=str(err.column).ljust(self.max_column),
|
||||
level=err.level.ljust(self.max_level),
|
||||
message=err.message.ljust(self.max_message),
|
||||
rule='{} '.format(err.rule) if err.rule else '',
|
||||
linter=err.linter.lower(),
|
||||
))
|
||||
|
||||
message.append('') # newline
|
||||
|
||||
# Print a summary
|
||||
message.append(self.fmt_summary.format(
|
||||
t=self.term,
|
||||
c=self.term.color(9) if num_errors else self.term.color(11),
|
||||
problem=self._pluralize('problem', num_errors + num_warnings),
|
||||
error=self._pluralize('error', num_errors),
|
||||
warning=self._pluralize('warning', num_warnings),
|
||||
))
|
||||
|
||||
return '\n'.join(message)
|
|
@ -0,0 +1,85 @@
|
|||
# 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 imp
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from .types import supported_types
|
||||
from .errors import LinterNotFound, LinterParseError
|
||||
|
||||
|
||||
class Parser(object):
|
||||
"""Reads and validates `.lint` files."""
|
||||
required_attributes = (
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'payload',
|
||||
)
|
||||
|
||||
def __call__(self, path):
|
||||
return self.parse(path)
|
||||
|
||||
def _load_linter(self, path):
|
||||
# Ensure parent module is present otherwise we'll (likely) get
|
||||
# an error due to unknown parent.
|
||||
parent_module = 'mozlint.linters'
|
||||
if parent_module not in sys.modules:
|
||||
mod = imp.new_module(parent_module)
|
||||
sys.modules[parent_module] = mod
|
||||
|
||||
write_bytecode = sys.dont_write_bytecode
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
module_name = '{}.{}'.format(parent_module, uuid.uuid1().get_hex())
|
||||
imp.load_source(module_name, path)
|
||||
|
||||
sys.dont_write_bytecode = write_bytecode
|
||||
|
||||
mod = sys.modules[module_name]
|
||||
|
||||
if not hasattr(mod, 'LINTER'):
|
||||
raise LinterParseError(path, "No LINTER definition found!")
|
||||
|
||||
definition = mod.LINTER
|
||||
definition['path'] = path
|
||||
return definition
|
||||
|
||||
def _validate(self, linter):
|
||||
missing_attrs = []
|
||||
for attr in self.required_attributes:
|
||||
if attr not in linter:
|
||||
missing_attrs.append(attr)
|
||||
|
||||
if missing_attrs:
|
||||
raise LinterParseError(linter['path'], "Missing required attribute(s): "
|
||||
"{}".format(','.join(missing_attrs)))
|
||||
|
||||
if linter['type'] not in supported_types:
|
||||
raise LinterParseError(linter['path'], "Invalid type '{}'".format(linter['type']))
|
||||
|
||||
for attr in ('include', 'exclude'):
|
||||
if attr in linter and (not isinstance(linter[attr], list) or
|
||||
not all(isinstance(a, basestring) for a in linter[attr])):
|
||||
raise LinterParseError(linter['path'], "The {} directive must be a "
|
||||
"list of strings!".format(attr))
|
||||
|
||||
def parse(self, path):
|
||||
"""Read a linter and return its LINTER definition.
|
||||
|
||||
:param path: Path to the linter.
|
||||
:returns: Linter definition (dict)
|
||||
:raises: LinterNotFound, LinterParseError
|
||||
"""
|
||||
if not os.path.isfile(path):
|
||||
raise LinterNotFound(path)
|
||||
|
||||
if not path.endswith('.lint'):
|
||||
raise LinterParseError(path, "Invalid filename, linters must end with '.lint'!")
|
||||
|
||||
linter = self._load_linter(path)
|
||||
self._validate(linter)
|
||||
return linter
|
|
@ -0,0 +1,88 @@
|
|||
# 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/.
|
||||
|
||||
from json import dumps, JSONEncoder
|
||||
|
||||
|
||||
class ResultContainer(object):
|
||||
"""Represents a single lint error and its related metadata.
|
||||
|
||||
:param linter: name of the linter that flagged this error
|
||||
:param path: path to the file containing the error
|
||||
:param message: text describing the error
|
||||
:param lineno: line number that contains the error
|
||||
:param column: column containing the error (default 1)
|
||||
:param level: severity of the error, either 'warning' or 'error' (default 'error')
|
||||
:param hint: suggestion for fixing the error (optional)
|
||||
:param source: source code context of the error (optional)
|
||||
:param rule: name of the rule that was violated (optional)
|
||||
:param lineoffset: denotes an error spans multiple lines, of the form
|
||||
(<lineno offset>, <num lines>) (optional)
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'linter',
|
||||
'path',
|
||||
'message',
|
||||
'lineno',
|
||||
'column',
|
||||
'hint',
|
||||
'source',
|
||||
'level',
|
||||
'rule',
|
||||
'lineoffset',
|
||||
)
|
||||
|
||||
def __init__(self, linter, path, message, lineno, column=1, hint=None,
|
||||
source=None, level='error', rule=None, lineoffset=None):
|
||||
self.path = path
|
||||
self.message = message
|
||||
self.lineno = lineno
|
||||
self.column = column
|
||||
self.hint = hint
|
||||
self.source = source
|
||||
self.level = level
|
||||
self.linter = linter
|
||||
self.rule = rule
|
||||
self.lineoffset = lineoffset
|
||||
|
||||
def __repr__(self):
|
||||
s = dumps(self, cls=ResultEncoder, indent=2)
|
||||
return "ResultContainer({})".format(s)
|
||||
|
||||
|
||||
class ResultEncoder(JSONEncoder):
|
||||
"""Class for encoding :class:`~result.ResultContainer`s to json.
|
||||
|
||||
Usage:
|
||||
|
||||
json.dumps(results, cls=ResultEncoder)
|
||||
"""
|
||||
def default(self, o):
|
||||
if isinstance(o, ResultContainer):
|
||||
return {a: getattr(o, a) for a in o.__slots__}
|
||||
return JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
def from_linter(lintobj, **kwargs):
|
||||
"""Create a :class:`~result.ResultContainer` from a LINTER definition.
|
||||
|
||||
Convenience method that pulls defaults from a LINTER
|
||||
definition and forwards them.
|
||||
|
||||
:param lintobj: LINTER obj as defined in a .lint file
|
||||
:param kwargs: same as :class:`~result.ResultContainer`
|
||||
:returns: :class:`~result.ResultContainer` object
|
||||
"""
|
||||
attrs = {}
|
||||
for attr in ResultContainer.__slots__:
|
||||
attrs[attr] = kwargs.get(attr, lintobj.get(attr))
|
||||
|
||||
if not attrs['linter']:
|
||||
attrs['linter'] = lintobj.get('name')
|
||||
|
||||
if not attrs['message']:
|
||||
attrs['message'] = lintobj.get('description')
|
||||
|
||||
return ResultContainer(**attrs)
|
|
@ -0,0 +1,111 @@
|
|||
# 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 signal
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from Queue import Empty
|
||||
from multiprocessing import (
|
||||
Manager,
|
||||
Pool,
|
||||
cpu_count,
|
||||
)
|
||||
|
||||
from .errors import LintersNotConfigured
|
||||
from .types import supported_types
|
||||
from .parser import Parser
|
||||
|
||||
|
||||
def _run_linters(queue, paths, **lintargs):
|
||||
parse = Parser()
|
||||
results = defaultdict(list)
|
||||
|
||||
while True:
|
||||
try:
|
||||
linter_path = queue.get(False)
|
||||
except Empty:
|
||||
return results
|
||||
|
||||
# Ideally we would pass the entire LINTER definition as an argument
|
||||
# to the worker instead of re-parsing it. But passing a function from
|
||||
# a dynamically created module (with imp) does not seem to be possible
|
||||
# with multiprocessing on Windows.
|
||||
linter = parse(linter_path)
|
||||
func = supported_types[linter['type']]
|
||||
res = func(paths, linter, **lintargs) or []
|
||||
|
||||
for r in res:
|
||||
results[r.path].append(r)
|
||||
|
||||
|
||||
def _run_worker(*args, **lintargs):
|
||||
try:
|
||||
return _run_linters(*args, **lintargs)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
|
||||
class LintRoller(object):
|
||||
"""Registers and runs linters.
|
||||
|
||||
:param lintargs: Arguments to pass to the underlying linter(s).
|
||||
"""
|
||||
|
||||
def __init__(self, **lintargs):
|
||||
self.parse = Parser()
|
||||
self.linters = []
|
||||
self.lintargs = lintargs
|
||||
|
||||
def read(self, paths):
|
||||
"""Parse one or more linters and add them to the registry.
|
||||
|
||||
:param paths: A path or iterable of paths to linter definitions.
|
||||
"""
|
||||
if isinstance(paths, basestring):
|
||||
paths = (paths,)
|
||||
|
||||
for path in paths:
|
||||
self.linters.append(self.parse(path))
|
||||
|
||||
def roll(self, paths, num_procs=None):
|
||||
"""Run all of the registered linters against the specified file paths.
|
||||
|
||||
:param paths: An iterable of files and/or directories to lint.
|
||||
:param num_procs: The number of processes to use. Default: cpu count
|
||||
:return: A dictionary with file names as the key, and a list of
|
||||
:class:`~result.ResultContainer`s as the value.
|
||||
"""
|
||||
if not self.linters:
|
||||
raise LintersNotConfigured
|
||||
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
|
||||
m = Manager()
|
||||
queue = m.Queue()
|
||||
|
||||
for linter in self.linters:
|
||||
queue.put(linter['path'])
|
||||
|
||||
num_procs = num_procs or cpu_count()
|
||||
num_procs = min(num_procs, len(self.linters))
|
||||
|
||||
# ensure child processes ignore SIGINT so it reaches parent
|
||||
orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
pool = Pool(num_procs)
|
||||
signal.signal(signal.SIGINT, orig)
|
||||
|
||||
all_results = defaultdict(list)
|
||||
results = []
|
||||
for i in range(num_procs):
|
||||
results.append(
|
||||
pool.apply_async(_run_worker, args=(queue, paths), kwds=self.lintargs))
|
||||
|
||||
for res in results:
|
||||
# parent process blocks on res.get()
|
||||
for k, v in res.get().iteritems():
|
||||
all_results[k].extend(v)
|
||||
|
||||
return all_results
|
|
@ -0,0 +1,146 @@
|
|||
# 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 os
|
||||
import re
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from mozpack import path as mozpath
|
||||
from mozpack.files import FileFinder
|
||||
|
||||
from . import result
|
||||
|
||||
|
||||
class BaseType(object):
|
||||
"""Abstract base class for all types of linters."""
|
||||
__metaclass__ = ABCMeta
|
||||
batch = False
|
||||
|
||||
def __call__(self, paths, linter, **lintargs):
|
||||
"""Run `linter` against `paths` with `lintargs`.
|
||||
|
||||
:param paths: Paths to lint. Can be a file or directory.
|
||||
:param linter: Linter definition paths are being linted against.
|
||||
:param lintargs: External arguments to the linter not defined in
|
||||
the definition, but passed in by a consumer.
|
||||
:returns: A list of :class:`~result.ResultContainer` objects.
|
||||
"""
|
||||
exclude = lintargs.get('exclude', [])
|
||||
exclude.extend(linter.get('exclude', []))
|
||||
|
||||
paths = self._filter(paths, linter.get('include'), exclude)
|
||||
if not paths:
|
||||
return
|
||||
|
||||
if self.batch:
|
||||
return self._lint(paths, linter, **lintargs)
|
||||
|
||||
errors = []
|
||||
for p in paths:
|
||||
result = self._lint(p, linter, **lintargs)
|
||||
if result:
|
||||
errors.extend(result)
|
||||
return errors
|
||||
|
||||
def _filter(self, paths, include=None, exclude=None):
|
||||
if not include and not exclude:
|
||||
return paths
|
||||
|
||||
if include:
|
||||
include = map(os.path.normpath, include)
|
||||
|
||||
if exclude:
|
||||
exclude = map(os.path.normpath, exclude)
|
||||
|
||||
def match(path, patterns):
|
||||
return any(mozpath.match(path, pattern) for pattern in patterns)
|
||||
|
||||
filtered = []
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
if include and not match(path, include):
|
||||
continue
|
||||
elif exclude and match(path, exclude):
|
||||
continue
|
||||
filtered.append(path)
|
||||
elif os.path.isdir(path):
|
||||
finder = FileFinder(path, find_executables=False, ignore=exclude)
|
||||
if self.batch:
|
||||
# Batch means the underlying linter will be responsible for finding
|
||||
# matching files in the directory. Return the path as is if there
|
||||
# exists at least one matching file.
|
||||
if any(finder.contains(pattern) for pattern in include):
|
||||
filtered.append(path)
|
||||
else:
|
||||
# Convert the directory to a list of matching files.
|
||||
for pattern in include:
|
||||
filtered.extend([os.path.join(path, p)
|
||||
for p, f in finder.find(pattern)])
|
||||
|
||||
return filtered
|
||||
|
||||
@abstractmethod
|
||||
def _lint(self, path):
|
||||
pass
|
||||
|
||||
|
||||
class LineType(BaseType):
|
||||
"""Abstract base class for linter types that check each line individually.
|
||||
|
||||
Subclasses of this linter type will read each file and check the provided
|
||||
payload against each line one by one.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def condition(payload, line):
|
||||
pass
|
||||
|
||||
def _lint(self, path, linter, **lintargs):
|
||||
payload = linter['payload']
|
||||
|
||||
with open(path, 'r') as fh:
|
||||
lines = fh.readlines()
|
||||
|
||||
errors = []
|
||||
for i, line in enumerate(lines):
|
||||
if self.condition(payload, line):
|
||||
errors.append(result.from_linter(linter, path=path, lineno=i+1))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
class StringType(LineType):
|
||||
"""Linter type that checks whether a substring is found."""
|
||||
|
||||
def condition(self, payload, line):
|
||||
return payload in line
|
||||
|
||||
|
||||
class RegexType(LineType):
|
||||
"""Linter type that checks whether a regex match is found."""
|
||||
|
||||
def condition(self, payload, line):
|
||||
return re.search(payload, line)
|
||||
|
||||
|
||||
class ExternalType(BaseType):
|
||||
"""Linter type that runs an external function.
|
||||
|
||||
The function is responsible for properly formatting the results
|
||||
into a list of :class:`~result.ResultContainer` objects.
|
||||
"""
|
||||
batch = True
|
||||
|
||||
def _lint(self, files, linter, **lintargs):
|
||||
payload = linter['payload']
|
||||
return payload(files, **lintargs)
|
||||
|
||||
|
||||
supported_types = {
|
||||
'string': StringType(),
|
||||
'regex': RegexType(),
|
||||
'external': ExternalType(),
|
||||
}
|
||||
"""Mapping of type string to an associated instance."""
|
|
@ -0,0 +1,26 @@
|
|||
# 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/.
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
VERSION = 0.1
|
||||
DEPS = []
|
||||
|
||||
setup(
|
||||
name='mozlint',
|
||||
description='Framework for registering and running micro lints',
|
||||
license='MPL 2.0',
|
||||
author='Andrew Halberstadt',
|
||||
author_email='ahalberstadt@mozilla.com',
|
||||
url='',
|
||||
packages=['mozlint'],
|
||||
version=VERSION,
|
||||
classifiers=[
|
||||
'Environment :: Console',
|
||||
'Development Status :: 3 - Alpha',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
'Natural Language :: English',
|
||||
],
|
||||
install_requires=DEPS,
|
||||
)
|
|
@ -0,0 +1,2 @@
|
|||
// Oh no.. we called this variable foobar, bad!
|
||||
var foobar = "a string";
|
|
@ -0,0 +1,2 @@
|
|||
// What a relief
|
||||
var properlyNamed = "a string";
|
|
@ -0,0 +1,30 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; 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/.
|
||||
|
||||
from mozlint import result
|
||||
|
||||
|
||||
def lint(files, **lintargs):
|
||||
results = []
|
||||
for path in files:
|
||||
with open(path, 'r') as fh:
|
||||
for i, line in enumerate(fh.readlines()):
|
||||
if 'foobar' in line:
|
||||
results.append(result.from_linter(
|
||||
LINTER, path=path, lineno=i+1, column=1, rule="no-foobar"))
|
||||
return results
|
||||
|
||||
|
||||
LINTER = {
|
||||
'name': "ExternalLinter",
|
||||
'description': "It's bad to have the string foobar in js files.",
|
||||
'include': [
|
||||
'**/*.js',
|
||||
'**/*.jsm',
|
||||
],
|
||||
'type': 'external',
|
||||
'payload': lint,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "BadExcludeLinter",
|
||||
'description': "Has an invalid exclude directive.",
|
||||
'exclude': [0, 1], # should be a list of strings
|
||||
'type': 'string',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "BadExtensionLinter",
|
||||
'description': "Has an invalid file extension.",
|
||||
'type': 'string',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "BadIncludeLinter",
|
||||
'description': "Has an invalid include directive.",
|
||||
'include': 'should be a list',
|
||||
'type': 'string',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "BadTypeLinter",
|
||||
'description': "Has an invalid type.",
|
||||
'type': 'invalid',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "MissingAttrsLinter",
|
||||
'description': "Missing type and payload",
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
# No LINTER variable
|
|
@ -0,0 +1,19 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; 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/.
|
||||
|
||||
from mozlint.errors import LintException
|
||||
|
||||
|
||||
def lint(files, **lintargs):
|
||||
raise LintException("Oh no something bad happened!")
|
||||
|
||||
|
||||
LINTER = {
|
||||
'name': "RaisesLinter",
|
||||
'description': "Raises an exception",
|
||||
'type': 'external',
|
||||
'payload': lint,
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "RegexLinter",
|
||||
'description': "Make sure the string 'foobar' never appears in a js variable files because it is bad.",
|
||||
'rule': 'no-foobar',
|
||||
'include': [
|
||||
'**/*.js',
|
||||
'**/*.jsm',
|
||||
],
|
||||
'type': 'regex',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
|
||||
LINTER = {
|
||||
'name': "StringLinter",
|
||||
'description': "Make sure the string 'foobar' never appears in browser js files because it is bad.",
|
||||
'rule': 'no-foobar',
|
||||
'include': [
|
||||
'**/*.js',
|
||||
'**/*.jsm',
|
||||
],
|
||||
'type': 'string',
|
||||
'payload': 'foobar',
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
# 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/.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from unittest import TestCase
|
||||
|
||||
from mozunit import main
|
||||
|
||||
from mozlint import ResultContainer
|
||||
from mozlint import formatters
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class TestFormatters(TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
containers = (
|
||||
ResultContainer(
|
||||
linter='foo',
|
||||
path='a/b/c.txt',
|
||||
message="oh no foo",
|
||||
lineno=1,
|
||||
),
|
||||
ResultContainer(
|
||||
linter='bar',
|
||||
path='d/e/f.txt',
|
||||
message="oh no bar",
|
||||
hint="try baz instead",
|
||||
level='warning',
|
||||
lineno=4,
|
||||
column=2,
|
||||
rule="bar-not-allowed",
|
||||
),
|
||||
ResultContainer(
|
||||
linter='baz',
|
||||
path='a/b/c.txt',
|
||||
message="oh no baz",
|
||||
lineno=4,
|
||||
source="if baz:",
|
||||
),
|
||||
)
|
||||
|
||||
self.results = defaultdict(list)
|
||||
for c in containers:
|
||||
self.results[c.path].append(c)
|
||||
|
||||
def test_stylish_formatter(self):
|
||||
expected = """
|
||||
a/b/c.txt
|
||||
1:1 error oh no foo (foo)
|
||||
4:1 error oh no baz (baz)
|
||||
|
||||
d/e/f.txt
|
||||
4:2 warning oh no bar bar-not-allowed (bar)
|
||||
|
||||
\u2716 3 problems (2 errors, 1 warning)
|
||||
""".strip()
|
||||
|
||||
fmt = formatters.get('stylish', disable_colors=True)
|
||||
self.assertEqual(expected, fmt(self.results))
|
||||
|
||||
def test_json_formatter(self):
|
||||
fmt = formatters.get('json')
|
||||
formatted = json.loads(fmt(self.results))
|
||||
|
||||
self.assertEqual(set(formatted.keys()), set(self.results.keys()))
|
||||
|
||||
slots = ResultContainer.__slots__
|
||||
for errors in formatted.values():
|
||||
for err in errors:
|
||||
self.assertTrue(all(s in err for s in slots))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,68 @@
|
|||
# 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 os
|
||||
from unittest import TestCase
|
||||
|
||||
from mozunit import main
|
||||
|
||||
from mozlint.parser import Parser
|
||||
from mozlint.errors import (
|
||||
LinterNotFound,
|
||||
LinterParseError,
|
||||
)
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class TestParser(TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self._lintdir = os.path.join(here, 'linters')
|
||||
self._parse = Parser()
|
||||
|
||||
def parse(self, name):
|
||||
return self._parse(os.path.join(self._lintdir, name))
|
||||
|
||||
def test_parse_valid_linter(self):
|
||||
linter = self.parse('string.lint')
|
||||
self.assertIsInstance(linter, dict)
|
||||
self.assertIn('name', linter)
|
||||
self.assertIn('description', linter)
|
||||
self.assertIn('type', linter)
|
||||
self.assertIn('payload', linter)
|
||||
|
||||
def test_parse_invalid_type(self):
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('invalid_type.lint')
|
||||
|
||||
def test_parse_invalid_extension(self):
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('invalid_extension.lnt')
|
||||
|
||||
def test_parse_invalid_include_exclude(self):
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('invalid_include.lint')
|
||||
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('invalid_exclude.lint')
|
||||
|
||||
def test_parse_missing_attributes(self):
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('missing_attrs.lint')
|
||||
|
||||
def test_parse_missing_definition(self):
|
||||
with self.assertRaises(LinterParseError):
|
||||
self.parse('missing_definition.lint')
|
||||
|
||||
def test_parse_non_existent_linter(self):
|
||||
with self.assertRaises(LinterNotFound):
|
||||
self.parse('missing_file.lint')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,75 @@
|
|||
# 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 os
|
||||
import sys
|
||||
from unittest import TestCase
|
||||
|
||||
from mozunit import main
|
||||
|
||||
from mozlint import LintRoller, ResultContainer
|
||||
from mozlint.errors import LintersNotConfigured, LintException
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class TestLintRoller(TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
filedir = os.path.join(here, 'files')
|
||||
self.files = [os.path.join(filedir, f) for f in os.listdir(filedir)]
|
||||
self.lintdir = os.path.join(here, 'linters')
|
||||
|
||||
names = ('string.lint', 'regex.lint', 'external.lint')
|
||||
self.linters = [os.path.join(self.lintdir, n) for n in names]
|
||||
|
||||
def setUp(self):
|
||||
TestCase.setUp(self)
|
||||
self.lint = LintRoller()
|
||||
|
||||
def test_roll_no_linters_configured(self):
|
||||
with self.assertRaises(LintersNotConfigured):
|
||||
self.lint.roll(self.files)
|
||||
|
||||
def test_roll_successful(self):
|
||||
self.lint.read(self.linters)
|
||||
|
||||
result = self.lint.roll(self.files)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
path = result.keys()[0]
|
||||
self.assertEqual(os.path.basename(path), 'foobar.js')
|
||||
|
||||
errors = result[path]
|
||||
self.assertIsInstance(errors, list)
|
||||
self.assertEqual(len(errors), 6)
|
||||
|
||||
container = errors[0]
|
||||
self.assertIsInstance(container, ResultContainer)
|
||||
self.assertEqual(container.rule, 'no-foobar')
|
||||
|
||||
def test_roll_catch_exception(self):
|
||||
self.lint.read(os.path.join(self.lintdir, 'raises.lint'))
|
||||
|
||||
# suppress printed traceback from test output
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
with self.assertRaises(LintException):
|
||||
self.lint.roll(self.files)
|
||||
sys.stderr = old_stderr
|
||||
|
||||
def test_roll_with_excluded_path(self):
|
||||
self.lint.lintargs = {'exclude': ['**/foobar.js']}
|
||||
|
||||
self.lint.read(self.linters)
|
||||
result = self.lint.roll(self.files)
|
||||
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,69 @@
|
|||
# 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 os
|
||||
from unittest import TestCase
|
||||
|
||||
from mozunit import main
|
||||
|
||||
from mozlint import LintRoller
|
||||
from mozlint.result import ResultContainer
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class TestLinterTypes(TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self.lintdir = os.path.join(here, 'linters')
|
||||
self.filedir = os.path.join(here, 'files')
|
||||
self.files = [os.path.join(self.filedir, f) for f in os.listdir(self.filedir)]
|
||||
|
||||
def setUp(self):
|
||||
TestCase.setUp(self)
|
||||
self.lint = LintRoller()
|
||||
|
||||
def path(self, name):
|
||||
return os.path.join(self.filedir, name)
|
||||
|
||||
def test_string_linter(self):
|
||||
self.lint.read(os.path.join(self.lintdir, 'string.lint'))
|
||||
result = self.lint.roll(self.files)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
self.assertIn(self.path('foobar.js'), result.keys())
|
||||
self.assertNotIn(self.path('no_foobar.js'), result.keys())
|
||||
|
||||
result = result[self.path('foobar.js')][0]
|
||||
self.assertIsInstance(result, ResultContainer)
|
||||
self.assertEqual(result.linter, 'StringLinter')
|
||||
|
||||
def test_regex_linter(self):
|
||||
self.lint.read(os.path.join(self.lintdir, 'regex.lint'))
|
||||
result = self.lint.roll(self.files)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn(self.path('foobar.js'), result.keys())
|
||||
self.assertNotIn(self.path('no_foobar.js'), result.keys())
|
||||
|
||||
result = result[self.path('foobar.js')][0]
|
||||
self.assertIsInstance(result, ResultContainer)
|
||||
self.assertEqual(result.linter, 'RegexLinter')
|
||||
|
||||
def test_external_linter(self):
|
||||
self.lint.read(os.path.join(self.lintdir, 'external.lint'))
|
||||
result = self.lint.roll(self.files)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn(self.path('foobar.js'), result.keys())
|
||||
self.assertNotIn(self.path('no_foobar.js'), result.keys())
|
||||
|
||||
result = result[self.path('foobar.js')][0]
|
||||
self.assertIsInstance(result, ResultContainer)
|
||||
self.assertEqual(result.linter, 'ExternalLinter')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Загрузка…
Ссылка в новой задаче