Files
Magnus Melin 3381809b31 Bug 2069988 - Add a mozlint linter_paths lintarg for payloads outside the linter directory r=linter-reviewers,sylvestre
Bug 2069661 made findobject load a linter's payload module from the file next to
its .yml rather than importing it by name. That assumes the definition and the
module it names sit in the same directory, which does not hold for comm-central.

Take a linter_paths lintarg, listing extra directories to search after the
definition's own. A payload is still loaded from a known linter directory and
not from sys.path.

Differential Revision: https://phabricator.services.mozilla.com/D324197
2026-09-08 07:13:02 +00:00

274 lines
7.9 KiB
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 os
import sys
import types
from fnmatch import fnmatch
import mozunit
import pytest
from mozlint import pathutils
here = os.path.abspath(os.path.dirname(__file__))
root = os.path.join(here, "filter")
definition = os.path.join(here, "linters", "external.yml")
def assert_paths(a, b):
def normalize(p):
if not os.path.isabs(p):
p = os.path.join(root, p)
return os.path.normpath(p)
assert set(map(normalize, a)) == set(map(normalize, b))
@pytest.mark.parametrize(
"test",
(
{
"paths": ["a.js", "subdir1/subdir3/d.js"],
"include": ["."],
"exclude": ["subdir1"],
"expected": ["a.js"],
},
{
"paths": ["a.js", "subdir1/subdir3/d.js"],
"include": ["subdir1/subdir3"],
"exclude": ["subdir1"],
"expected": ["subdir1/subdir3/d.js"],
},
{
"paths": ["."],
"include": ["."],
"exclude": ["**/c.py", "subdir1/subdir3"],
"extensions": ["py"],
"expected": ["."],
"expected_exclude": ["subdir2/c.py", "subdir1/subdir3"],
},
{
"paths": [
"a.py",
"a.js",
"subdir1/b.py",
"subdir2/c.py",
"subdir1/subdir3/d.py",
],
"include": ["."],
"exclude": ["**/c.py", "subdir1/subdir3"],
"extensions": ["py"],
"expected": ["a.py", "subdir1/b.py"],
},
pytest.param(
{
"paths": [
"a.py",
"a.js",
"subdir1/b.py",
"subdir2/c.py",
"subdir1/subdir3/d.py",
],
"include": ["."],
"exclude": [],
"exclude_extensions": ["py"],
"expected": ["a.js"],
},
id="Excluding .py should only return .js file.",
),
pytest.param(
{
"paths": [
"a.py",
"a.js",
"subdir1/b.py",
"subdir2/c.py",
"subdir1/subdir3/d.py",
],
"include": ["."],
"exclude": [],
"exclude_extensions": ["js"],
"expected": [
"a.py",
"subdir1/b.py",
"subdir2/c.py",
"subdir1/subdir3/d.py",
],
},
id="Excluding .js should only return .py files.",
),
{
"paths": ["a.py", "a.js", "subdir2"],
"include": ["."],
"exclude": [],
"extensions": ["py"],
"expected": ["a.py", "subdir2"],
},
{
"paths": ["subdir1"],
"include": ["."],
"exclude": ["subdir1/subdir3"],
"extensions": ["py"],
"expected": ["subdir1"],
"expected_exclude": ["subdir1/subdir3"],
},
{
"paths": ["docshell"],
"include": ["docs"],
"exclude": [],
"expected": [],
},
{
"paths": ["does/not/exist"],
"include": ["."],
"exclude": [],
"expected": [],
},
{
"paths": ["a.py"],
"include": ["a.js"],
"exclude": [],
"expected": [],
},
),
)
def test_filterpaths(test):
expected = test.pop("expected")
expected_exclude = test.pop("expected_exclude", [])
paths, exclude = pathutils.filterpaths(root, **test)
assert_paths(paths, expected)
assert_paths(exclude, expected_exclude)
@pytest.mark.parametrize(
"test",
(
{
"paths": ["subdir1/b.js"],
"config": {
"exclude": ["subdir1"],
"extensions": ["js"],
},
"expected": [],
},
{
"paths": ["subdir1"],
"config": {
"exclude": ["subdir1"],
"extensions": ["js"],
},
"expected": [],
},
pytest.param(
{
"paths": ["a.py", "subdir1"],
"config": {
"exclude": ["subdir1"],
"exclude_extensions": ["gob"],
},
"expected": ["a.py"],
},
id="Excluding both subdirs and nonsense extensions returns other files.",
),
pytest.param(
{
"paths": ["a.py", "a.js", "subdir1"],
"config": {
"exclude": [],
"exclude_extensions": ["py"],
},
"expected": ["a.js", "subdir1/subdir3/d.js", "subdir1/b.js"],
},
id="Excluding .py files returns only non-.py files, also from subdirs.",
),
),
)
def test_expand_exclusions(test):
expected = test.pop("expected", [])
input_paths = [os.path.join(root, p) for p in test["paths"]]
paths = list(pathutils.expand_exclusions(input_paths, test["config"], root))
assert_paths(paths, expected)
@pytest.mark.parametrize(
"paths,expected",
[
(["subdir1/*"], ["subdir1"]),
(["subdir2/*"], ["subdir2"]),
(["subdir1/*.*", "subdir1/subdir3/*", "subdir2/*"], ["subdir1", "subdir2"]),
([root + "/*", "subdir1/*.*", "subdir1/subdir3/*", "subdir2/*"], [root]),
(["subdir1/b.py", "subdir1/subdir3"], ["subdir1/b.py", "subdir1/subdir3"]),
(["subdir1/b.py", "subdir1/b.js"], ["subdir1/b.py", "subdir1/b.js"]),
(["subdir1/subdir3"], ["subdir1/subdir3"]),
(
[
"foo",
"foobar",
],
["foo", "foobar"],
),
],
)
def test_collapse(paths, expected):
os.chdir(root)
inputs = []
for path in paths:
base, name = os.path.split(path)
if "*" in name:
for n in os.listdir(base):
if not fnmatch(n, name):
continue
inputs.append(os.path.join(base, n))
else:
inputs.append(path)
print(f"inputs: {inputs}")
assert_paths(pathutils.collapse(inputs), expected)
def test_findobject_loads_from_root(monkeypatch):
monkeypatch.setitem(sys.modules, "external", types.ModuleType("external"))
monkeypatch.delitem(sys.modules, "mozlint.linters.external", raising=False)
func = pathutils.findobject("external:external", definition)
assert func.__module__ == "mozlint.linters.external"
assert os.path.samefile(
func.__code__.co_filename, os.path.join(here, "linters", "external.py")
)
assert pathutils.findobject("external:external", definition) is func
def test_findobject_loads_from_linter_paths(monkeypatch):
monkeypatch.setitem(sys.modules, "external", types.ModuleType("external"))
monkeypatch.delitem(sys.modules, "mozlint.linters.external", raising=False)
elsewhere = os.path.join(here, "filter", "external.yml")
func = pathutils.findobject(
"external:external", elsewhere, [os.path.join(here, "linters")]
)
assert os.path.samefile(
func.__code__.co_filename, os.path.join(here, "linters", "external.py")
)
def test_findobject_missing_module():
with pytest.raises(ImportError):
pathutils.findobject("does_not_exist:lint", definition)
with pytest.raises(ImportError):
pathutils.findobject("does_not_exist:lint", definition, [here])
def test_findobject_invalid_path():
with pytest.raises(ValueError):
pathutils.findobject("external.external", definition)
if __name__ == "__main__":
mozunit.main()