Bug 1811850 - [lint] Replace pylint linter with ruff, r=linter-reviewers,marco

This is enabling the PLE and PLC rules across a much larger swatch of the code
base. The PLR and PLW are additionally enabled at the warning level (introduces
~2500 new warnings).

To avoid extraneous changes in people's patches, rules at the warning level are
excluded from --fix.

Differential Revision: https://phabricator.services.mozilla.com/D172358
This commit is contained in:
Andrew Halberstadt
2023-03-20 13:06:28 +00:00
parent 83bc2ca934
commit 4e51653b98
15 changed files with 14 additions and 388 deletions

View File

@@ -100,11 +100,6 @@ In this document, we try to list these all tools.
- `bug 1555560 <https://bugzilla.mozilla.org/show_bug.cgi?id=1555560>`__
- :ref:`black`
- https://black.readthedocs.io/en/stable
* - pylint
-
- `bug 1623024 <https://bugzilla.mozilla.org/show_bug.cgi?id=1623024>`__
- :ref:`pylint`
- https://www.pylint.org/
.. list-table:: Rust
:widths: 20 20 20 20 20

View File

@@ -1,33 +0,0 @@
pylint
======
`pylint <https://www.pylint.org/>`__ is a popular linter for python. It is now the default python
linter in VS Code.
Please note that we also have :ref:`Flake8` available as a linter.
Run Locally
-----------
The mozlint integration of pylint can be run using mach:
.. parsed-literal::
$ mach lint --linter pylint <file paths>
Configuration
-------------
To enable pylint on new directory, add the path to the include
section in the `pylint.yml <https://searchfox.org/mozilla-central/source/tools/lint/pylint.yml>`_ file.
We enabled the same Pylint rules as `VS Code <https://code.visualstudio.com/docs/python/linting#_pylint>`_.
See in `pylint.py <https://searchfox.org/mozilla-central/source/tools/lint/python/pylint.py>`_ for the full list
Sources
-------
* `Configuration (YAML) <https://searchfox.org/mozilla-central/source/tools/lint/pylint.yml>`_
* `Source <https://searchfox.org/mozilla-central/source/tools/lint/python/pylint.py>`_

View File

@@ -5,6 +5,7 @@ select = [
"E", "W", # pycodestyle
"F", # pyflakes
"I", # isort
"PL", # pylint
]
ignore = [
# These should be triaged and either fixed or moved to the list below.
@@ -100,6 +101,7 @@ exclude = [
[tool.ruff.per-file-ignores]
# These paths are intentionally excluded.
"dom/bindings/Configuration.py" = ["PLC3002"]
"ipc/ipdl/*" = ["F403", "F405"]
"layout/tools/reftest/selftest/conftest.py" = ["F811"]
# cpp_eclipse has a lot of multi-line embedded XML which exceeds line length
@@ -108,6 +110,7 @@ exclude = [
"testing/marionette/**/__init__.py" = ["F401"]
"testing/mochitest/tests/python/conftest.py" = ["F811"]
"testing/mozbase/manifestparser/tests/test_filters.py" = ["E731"]
"testing/mozbase/mozinfo/mozinfo/mozinfo.py" = ["PLE0605"]
"testing/mozbase/mozlog/tests/test_formatters.py" = ["E501"]
"testing/mozharness/configs/*" = ["E501"]
"**/*.configure" = ["F821"]

View File

@@ -274,19 +274,6 @@ py-ruff:
- 'tools/lint/python/ruff.py'
- 'tools/lint/python/ruff_requirements.txt'
py-pylint:
description: pylint run over the gecko codebase
treeherder:
symbol: py(pylint)
run:
mach: lint -v -l pylint -f treeherder -f json:/builds/worker/mozlint.json *
when:
files-changed:
- '**/*.py'
- 'tools/lint/pylint.yml'
# moz.configure files are also Python files
# However, pylint has some hard time dealing with it
test-manifest:
description: lint test manifests
treeherder:

View File

@@ -3,7 +3,6 @@ file-whitespace:
description: File content sanity check
include:
- .
- tools/lint/python/pylint_requirements.txt
- tools/lint/python/black_requirements.txt
- tools/lint/python/ruff_requirements.txt
- tools/lint/rst/requirements.txt

View File

@@ -1,24 +0,0 @@
---
pylint:
description: A second Python linter
include:
- configure.py
- client.py
- security/
- accessible/
- docs/
- dom/base/
- dom/websocket/
- mozglue/
- toolkit/components/telemetry/
exclude:
- dom/bindings/Codegen.py
- security/manager/ssl/tests/unit/test_content_signing/pysign.py
- security/ct/tests/gtest/createSTHTestData.py
extensions: ['py']
support-files:
- '**/.pylint'
- 'tools/lint/python/pylint*'
type: external
payload: python.pylint:lint
setup: python.pylint:setup

View File

@@ -1,133 +0,0 @@
# 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
from mach.site import InstallPipRequirementsException
from mozlint import result
from mozlint.pathutils import expand_exclusions
from mozprocess import ProcessHandler
here = os.path.abspath(os.path.dirname(__file__))
PYLINT_REQUIREMENTS_PATH = os.path.join(here, "pylint_requirements.txt")
PYLINT_NOT_FOUND = """
Could not find pylint! Install pylint and try again.
$ pip install -U --require-hashes -r {}
""".strip().format(
PYLINT_REQUIREMENTS_PATH
)
PYLINT_INSTALL_ERROR = """
Unable to install correct version of pylint
Try to install it manually with:
$ pip install -U --require-hashes -r {}
""".strip().format(
PYLINT_REQUIREMENTS_PATH
)
class PylintProcess(ProcessHandler):
def __init__(self, config, *args, **kwargs):
self.config = config
kwargs["stream"] = False
kwargs["universal_newlines"] = True
ProcessHandler.__init__(self, *args, **kwargs)
def run(self, *args, **kwargs):
orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
ProcessHandler.run(self, *args, **kwargs)
signal.signal(signal.SIGINT, orig)
def setup(root, **lintargs):
virtualenv_manager = lintargs["virtualenv_manager"]
try:
virtualenv_manager.install_pip_requirements(
PYLINT_REQUIREMENTS_PATH,
quiet=True,
)
except (subprocess.CalledProcessError, InstallPipRequirementsException):
print(PYLINT_INSTALL_ERROR)
return 1
def get_pylint_binary():
return "pylint"
def run_process(config, cmd):
proc = PylintProcess(config, cmd)
proc.run()
try:
proc.wait()
except KeyboardInterrupt:
proc.kill()
return proc.output
def parse_issues(log, config, issues_json, path):
results = []
try:
issues = json.loads(issues_json)
except json.decoder.JSONDecodeError:
log.debug("Could not parse the output:")
log.debug("pylint output: {}".format(issues_json))
return []
for issue in issues:
res = {
"path": issue["path"],
"level": issue["type"],
"lineno": issue["line"],
"column": issue["column"],
"message": issue["message"],
"rule": issue["message-id"],
}
results.append(result.from_config(config, **res))
return results
def get_pylint_version(binary):
return subprocess.check_output(
[binary, "--version"],
universal_newlines=True,
stderr=subprocess.STDOUT,
)
def lint(paths, config, **lintargs):
log = lintargs["log"]
binary = get_pylint_binary()
log = lintargs["log"]
paths = list(expand_exclusions(paths, config, lintargs["root"]))
cmd_args = [binary]
results = []
# list from https://code.visualstudio.com/docs/python/linting#_pylint
# And ignore a bit more elements
cmd_args += [
"-fjson",
"--disable=all",
"--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,no-else-return", # NOQA: E501
"--disable=import-error,no-member",
]
base_command = cmd_args + paths
log.debug("Command: {}".format(" ".join(cmd_args)))
log.debug("pylint version: {}".format(get_pylint_version(binary)))
output = " ".join(run_process(config, base_command))
results = parse_issues(log, config, str(output), [])
return results

View File

@@ -1,5 +0,0 @@
pylint==2.15.8
dill==0.3.4
tomli==1.2.2
typing-extensions==3.10.0.2
tomlkit==0.10.1

View File

@@ -1,136 +0,0 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --generate-hashes tools/lint/python/pylint_requirements.in
#
astroid==2.12.13 \
--hash=sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907 \
--hash=sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7
# via pylint
dill==0.3.4 \
--hash=sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f \
--hash=sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675
# via
# -r tools/lint/python/pylint_requirements.in
# pylint
isort==5.11.2 \
--hash=sha256:dd8bbc5c0990f2a095d754e50360915f73b4c26fc82733eb5bfc6b48396af4d2 \
--hash=sha256:e486966fba83f25b8045f8dd7455b0a0d1e4de481e1d7ce4669902d9fb85e622
# via pylint
lazy-object-proxy==1.8.0 \
--hash=sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada \
--hash=sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d \
--hash=sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7 \
--hash=sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe \
--hash=sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd \
--hash=sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c \
--hash=sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858 \
--hash=sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288 \
--hash=sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec \
--hash=sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f \
--hash=sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891 \
--hash=sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c \
--hash=sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25 \
--hash=sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156 \
--hash=sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8 \
--hash=sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f \
--hash=sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e \
--hash=sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0 \
--hash=sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b
# via astroid
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via pylint
platformdirs==2.6.0 \
--hash=sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca \
--hash=sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e
# via pylint
pylint==2.15.8 \
--hash=sha256:ea82cd6a1e11062dc86d555d07c021b0fb65afe39becbe6fe692efd6c4a67443 \
--hash=sha256:ec4a87c33da054ab86a6c79afa6771dc8765cb5631620053e727fcf3ef8cbed7
# via -r tools/lint/python/pylint_requirements.in
tomli==1.2.2 \
--hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee \
--hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade
# via
# -r tools/lint/python/pylint_requirements.in
# pylint
tomlkit==0.10.1 \
--hash=sha256:3c517894eadef53e9072d343d37e4427b8f0b6200a70b7c9a19b2ebd1f53b951 \
--hash=sha256:3eba517439dcb2f84cf39f4f85fd2c3398309823a3c75ac3e73003638daf7915
# via
# -r tools/lint/python/pylint_requirements.in
# pylint
typing-extensions==3.10.0.2 \
--hash=sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e \
--hash=sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7 \
--hash=sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34
# via -r tools/lint/python/pylint_requirements.in
wrapt==1.14.1 \
--hash=sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3 \
--hash=sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b \
--hash=sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4 \
--hash=sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2 \
--hash=sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656 \
--hash=sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3 \
--hash=sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff \
--hash=sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310 \
--hash=sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a \
--hash=sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57 \
--hash=sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069 \
--hash=sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383 \
--hash=sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe \
--hash=sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87 \
--hash=sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d \
--hash=sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b \
--hash=sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907 \
--hash=sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f \
--hash=sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0 \
--hash=sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28 \
--hash=sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1 \
--hash=sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853 \
--hash=sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc \
--hash=sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3 \
--hash=sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3 \
--hash=sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164 \
--hash=sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1 \
--hash=sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c \
--hash=sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1 \
--hash=sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7 \
--hash=sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1 \
--hash=sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320 \
--hash=sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed \
--hash=sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1 \
--hash=sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248 \
--hash=sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c \
--hash=sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456 \
--hash=sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77 \
--hash=sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef \
--hash=sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1 \
--hash=sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7 \
--hash=sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86 \
--hash=sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4 \
--hash=sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d \
--hash=sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d \
--hash=sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8 \
--hash=sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5 \
--hash=sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471 \
--hash=sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00 \
--hash=sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68 \
--hash=sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3 \
--hash=sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d \
--hash=sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735 \
--hash=sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d \
--hash=sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569 \
--hash=sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7 \
--hash=sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59 \
--hash=sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5 \
--hash=sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb \
--hash=sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b \
--hash=sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f \
--hash=sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462 \
--hash=sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015 \
--hash=sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af
# via astroid

View File

@@ -141,10 +141,15 @@ def lint(paths, config, log, **lintargs):
process_kwargs = {"processStderrLine": lambda line: log.debug(line)}
warning_rules = set(config.get("warning-rules", []))
if lintargs.get("fix"):
# Do a first pass with --fix-only as the json format doesn't return the
# number of fixed issues.
output = run_process(config, args + ["--fix-only"], **process_kwargs)
fix_args = args + ["--fix-only"]
# Don't fix warnings to limit unrelated changes sneaking into patches.
fix_args.append(f"--extend-ignore={','.join(warning_rules)}")
output = run_process(config, fix_args, **process_kwargs)
matches = re.match(r"Fixed (\d+) errors?.", output)
if matches:
fixed = int(matches[1])
@@ -159,7 +164,6 @@ def lint(paths, config, log, **lintargs):
log.error(f"could not parse output: {output}")
return []
warning_rules = set(config.get("warning-rules", []))
for issue in issues:
res = {
"path": issue["filename"],
@@ -168,8 +172,11 @@ def lint(paths, config, log, **lintargs):
"lineoffset": issue["end_location"]["row"] - issue["location"]["row"],
"message": issue["message"],
"rule": issue["code"],
"level": "warning" if issue["code"] in warning_rules else "error",
"level": "error",
}
if any(issue["code"].startswith(w) for w in warning_rules):
res["level"] = "warning"
if issue["fix"]:
res["hint"] = issue["fix"]["message"]

View File

@@ -11,7 +11,7 @@ ruff:
- "**/pyproject.toml"
- "tools/lint/python/ruff.py"
# Rules that should result in warnings rather than errors.
warning-rules: []
warning-rules: [PLR, PLW]
type: external
payload: python.ruff:lint
setup: python.ruff:setup

View File

@@ -1,5 +0,0 @@
def foo():
useless_var = 1
useless_var = true
return "true"
print("unreachable")

View File

@@ -1,3 +0,0 @@
def foo():
a = 1 + 1
return a

View File

@@ -20,8 +20,6 @@ skip-if = os == "win"
[test_perfdocs_generation.py]
[test_updatebot.py]
[test_perfdocs_helpers.py]
[test_pylint.py]
requirements = tools/lint/python/pylint_requirements.txt
[test_rst.py]
requirements = tools/lint/rst/requirements.txt
[test_ruff.py]

View File

@@ -1,24 +0,0 @@
import mozunit
LINTER = "pylint"
def test_lint_single_file(lint, paths):
results = lint(paths("bad.py"))
assert len(results) == 3
assert results[1].rule == "E0602"
assert results[2].rule == "W0101"
assert results[2].lineno == 5
# run lint again to make sure the previous results aren't counted twice
results = lint(paths("bad.py"))
assert len(results) == 3
def test_lint_single_file_good(lint, paths):
results = lint(paths("good.py"))
assert len(results) == 0
if __name__ == "__main__":
mozunit.main()