Files
Mike Hommey 2dbda43bf7 Bug 2060817 - Expose lint hints in their output. r=linter-reviewers,sylvestre
Several linters (gecko-trace, python-sites, clippy, ruff, eslint,
libpref) populate Issue.hint with a suggested remedy, but of the
mozlint's formatters actually render it. The hint is only ever visible
in the raw JSON output.

Render it in the default "stylish" formatter, while deduplicating
identical hint text across a whole run (not just within one file), since
a lint that shares one generic hint across many findings would otherwise
repeat it verbatim after every single issue.

Differential Revision: https://phabricator.services.mozilla.com/D316509
2026-08-06 08:48:06 +00:00

178 lines
4.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 json
from textwrap import dedent
import attr
import mozpack.path as mozpath
import mozunit
import pytest
from mozlint import formatters
from mozlint.result import Issue, ResultSummary
NORMALISED_PATHS = {
"abc": mozpath.normpath("a/b/c.txt"),
"def": mozpath.normpath("d/e/f.txt"),
"root": mozpath.abspath("/fake/root"),
}
EXPECTED = {
"compact": {
"kwargs": {},
"format": """
/fake/root/a/b/c.txt: line 1, Error - oh no foo (foo)
/fake/root/a/b/c.txt: line 4, col 10, Error - oh no baz (baz)
/fake/root/a/b/c.txt: line 5, Error - oh no foo-diff (foo-diff)
/fake/root/d/e/f.txt: line 4, col 2, Warning - oh no bar (bar-not-allowed)
4 problems
""".strip(),
},
"stylish": {
"kwargs": {"disable_colors": True},
"format": """
/fake/root/a/b/c.txt
1 error oh no foo (foo)
4:10 error oh no baz (baz)
|
4 | if baz:
| ^^^
5 error oh no foo-diff (foo-diff)
diff 1
- hello
+ hello2
/fake/root/d/e/f.txt
4:2 warning oh no bar bar-not-allowed (bar)
Hint: try baz instead
\u2716 4 problems (3 errors, 1 warning, 0 fixed)
""".strip(),
},
"treeherder": {
"kwargs": {},
"format": """
TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:1 | oh no foo (foo)
TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:4:10 | oh no baz (baz)
TEST-UNEXPECTED-ERROR | /fake/root/a/b/c.txt:5 | oh no foo-diff (foo-diff)
TEST-UNEXPECTED-WARNING | /fake/root/d/e/f.txt:4:2 | oh no bar (bar-not-allowed)
""".strip(),
},
"unix": {
"kwargs": {},
"format": """
{abc}:1: foo error: oh no foo
{abc}:4:10: baz error: oh no baz
{abc}:5: foo-diff error: oh no foo-diff
{def}:4:2: bar-not-allowed warning: oh no bar
""".format(**NORMALISED_PATHS).strip(),
},
"summary": {
"kwargs": {},
"format": """
{root}/a: 3 errors
{root}/d: 0 errors, 1 warning
""".format(**NORMALISED_PATHS).strip(),
},
}
@pytest.fixture
def result(scope="module"):
result = ResultSummary("/fake/root")
containers = (
Issue(linter="foo", path="a/b/c.txt", message="oh no foo", lineno=1),
Issue(
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",
),
Issue(
linter="baz",
path="a/b/c.txt",
message="oh no baz",
lineno=4,
column=10,
source=dedent(
"""
|
4 | if baz:
| ^^^
"""
).lstrip("\n"),
),
Issue(
linter="foo-diff",
path="a/b/c.txt",
message="oh no foo-diff",
lineno=5,
diff="diff 1\n- hello\n+ hello2",
),
)
result = ResultSummary("/fake/root")
for c in containers:
result.issues[c.path].append(c)
return result
@pytest.mark.parametrize("name", EXPECTED.keys())
def test_formatters(result, name):
opts = EXPECTED[name]
fmt = formatters.get(name, **opts["kwargs"])
# encoding to str bypasses a UnicodeEncodeError in pytest
assert fmt(result) == opts["format"]
def test_json_formatter(result):
fmt = formatters.get("json")
formatted = json.loads(fmt(result))
assert set(formatted.keys()) == set(result.issues.keys())
attrs = attr.fields(Issue)
for errors in formatted.values():
for err in errors:
assert all(a.name in err for a in attrs)
def test_stylish_hint_shown_once_per_distinct_text():
# "fix it" is repeated on issues in both a.txt and b.txt: dedup must hold
# across files, not just within a single one, since seen hints are
# tracked for the whole formatter run.
result = ResultSummary("/fake/root")
containers = (
Issue(linter="foo", path="a.txt", message="oh no foo", lineno=1, hint="fix it"),
Issue(linter="bar", path="a.txt", message="oh no bar", lineno=2, hint="fix it"),
Issue(
linter="baz",
path="a.txt",
message="oh no baz",
lineno=3,
hint="fix it differently",
),
Issue(linter="qux", path="b.txt", message="oh no qux", lineno=1, hint="fix it"),
Issue(linter="norf", path="b.txt", message="oh no norf", lineno=2),
)
for c in containers:
result.issues[c.path].append(c)
fmt = formatters.get("stylish", disable_colors=True)
output = fmt(result)
assert output.count("Hint: fix it\n") == 1
assert "Hint: fix it differently" in output
assert "oh no qux" in output
assert "oh no norf" in output
if __name__ == "__main__":
mozunit.main()