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:
Andrew Halberstadt 2016-03-16 14:55:21 -04:00
Родитель 70ba843b03
Коммит bb96d51342
26 изменённых файлов: 1041 добавлений и 0 удалений

Просмотреть файл

@ -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."""

26
python/mozlint/setup.py Normal file
Просмотреть файл

@ -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()