Skip to content

Commit

Permalink
Merge pull request #956 from fsfe/feature/lint-lines-output
Browse files Browse the repository at this point in the history
feat: lint output per line
  • Loading branch information
carmenbianca authored May 28, 2024
2 parents f2118a4 + da08a1c commit 78a5719
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ CLI command and its behaviour. There are no guarantees of stability for the
- npm `.npmrc` files (#985)
- Added comment styles:
- `man` for UNIX Man pages (`.man`) (#954)
- Added `--lines` output option for `lint`. (#956)

### Changed

Expand Down
10 changes: 7 additions & 3 deletions docs/man/reuse-lint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,26 @@ associated with it, then the project is not compliant.
Options
-------

.. option:: --quiet
.. option:: -q, --quiet

Do not print anything to STDOUT.

..
TODO: specify the JSON output.
.. option:: --json
.. option:: -j, --json

Output the results of the lint as JSON.

.. option:: --plain
.. option:: -p, --plain

Output the results of the lint as descriptive text. The text is valid
Markdown.

.. option:: -l, --lines

Output one line per error, prefixed by the file path.

.. option:: -h, --help

Display help and exit.
83 changes: 82 additions & 1 deletion src/reuse/lint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2022 Florian Snow <[email protected]>
# SPDX-FileCopyrightText: 2023 DB Systel GmbH
# SPDX-FileCopyrightText: 2024 Nico Rikken <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -15,7 +16,7 @@
from io import StringIO
from pathlib import Path
from textwrap import TextWrapper
from typing import IO, Any
from typing import IO, Any, Optional

from . import __REUSE_version__
from .project import Project
Expand All @@ -37,6 +38,12 @@ def add_arguments(parser: ArgumentParser) -> None:
action="store_true",
help=_("formats output as plain text"),
)
mutex_group.add_argument(
"-l",
"--lines",
action="store_true",
help=_("formats output as errors per line"),
)


# pylint: disable=too-many-branches,too-many-statements,too-many-locals
Expand Down Expand Up @@ -257,6 +264,78 @@ def custom_serializer(obj: Any) -> Any:
)


def format_lines(report: ProjectReport) -> str:
"""Formats data dictionary as plaintext strings to be printed to sys.stdout
Sorting of output is not guaranteed.
Symbolic links can result in multiple entries per file.
Args:
report: ProjectReport data
Returns:
String (in plaintext) that can be output to sys.stdout
"""
output = StringIO()

def license_path(lic: str) -> Optional[Path]:
"""Resolve a license identifier to a license path."""
return report.licenses.get(lic)

if not report.is_compliant:
# Bad licenses
for lic, files in sorted(report.bad_licenses.items()):
for path in sorted(files):
output.write(
_("{path}: bad license {lic}\n").format(path=path, lic=lic)
)

# Deprecated licenses
for lic in sorted(report.deprecated_licenses):
lic_path = license_path(lic)
output.write(
_("{lic_path}: deprecated license\n").format(lic_path=lic_path)
)

# Licenses without extension
for lic in sorted(report.licenses_without_extension):
lic_path = license_path(lic)
output.write(
_("{lic_path}: license without file extension\n").format(
lic_path=lic_path
)
)

# Unused licenses
for lic in sorted(report.unused_licenses):
lic_path = license_path(lic)
output.write(
_("{lic_path}: unused license\n").format(lic_path=lic_path)
)

# Missing licenses
for lic, files in sorted(report.missing_licenses.items()):
for path in sorted(files):
output.write(
_("{path}: missing license {lic}\n").format(
path=path, lic=lic
)
)

# Read errors
for path in sorted(report.read_errors):
output.write(_("{path}: read error\n").format(path=path))

# Without licenses
for path in report.files_without_licenses:
output.write(_("{path}: no license identifier\n").format(path=path))

# Without copyright
for path in report.files_without_copyright:
output.write(_("{path}: no copyright notice\n").format(path=path))

return output.getvalue()


def run(args: Namespace, project: Project, out: IO[str] = sys.stdout) -> int:
"""List all non-compliant files."""
report = ProjectReport.generate(
Expand All @@ -267,6 +346,8 @@ def run(args: Namespace, project: Project, out: IO[str] = sys.stdout) -> int:
pass
elif args.json:
out.write(format_json(report))
elif args.lines:
out.write(format_lines(report))
else:
out.write(format_plain(report))

Expand Down
61 changes: 60 additions & 1 deletion tests/test_lint.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# SPDX-FileCopyrightText: 2019 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2022 Florian Snow <[email protected]>
# SPDX-FileCopyrightText: 2024 Nico Rikken <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""All tests for reuse.lint"""

import re
import shutil

from conftest import cpython, posix

from reuse.lint import format_plain
from reuse.lint import format_lines, format_plain
from reuse.project import Project
from reuse.report import ProjectReport

Expand Down Expand Up @@ -212,4 +214,61 @@ def test_lint_json_output(fake_repository):
)


def test_lint_lines_output(fake_repository):
"""Complete test for lint with lines output."""
# Prepare a repository that includes all types of situations:
# missing_licenses, unused_licenses, bad_licenses, deprecated_licenses,
# licenses_without_extension, files_without_copyright,
# files_without_licenses, read_errors
(fake_repository / "invalid-license.py").write_text(
"SPDX-License-Identifier: invalid"
)
(fake_repository / "no-license.py").write_text(
"SPDX-FileCopyrightText: Jane Doe"
)
(fake_repository / "LICENSES" / "invalid-license-text").write_text(
"An invalid license text"
)
(fake_repository / "LICENSES" / "Nokia-Qt-exception-1.1.txt").write_text(
"Deprecated"
)
(fake_repository / "LICENSES" / "MIT").write_text("foo")
(fake_repository / "file with spaces.py").write_text("foo")

project = Project.from_directory(fake_repository)
report = ProjectReport.generate(project)

lines_result = format_lines(report)
lines_result_lines = lines_result.splitlines()

assert len(lines_result_lines) == 12

for line in lines_result_lines:
assert re.match(".+: [^:]+", line)

assert lines_result.count("invalid-license.py") == 3
assert lines_result.count("no-license.py") == 1
assert lines_result.count("LICENSES") == 6
assert lines_result.count("invalid-license-text") == 3
assert lines_result.count("Nokia-Qt-exception-1.1.txt") == 2
assert lines_result.count("MIT") == 2
assert lines_result.count("file with spaces.py") == 2


@cpython
@posix
def test_lint_lines_read_errors(fake_repository):
"""Check read error output"""
(fake_repository / "restricted.py").write_text("foo")
(fake_repository / "restricted.py").chmod(0o000)
project = Project.from_directory(fake_repository)
report = ProjectReport.generate(project)
result = format_lines(report)
print(result)

assert len(result.splitlines()) == 1
assert "restricted.py" in result
assert "read error" in result


# REUSE-IgnoreEnd

0 comments on commit 78a5719

Please sign in to comment.