Skip to content

Commit

Permalink
feat: Add refurb linter
Browse files Browse the repository at this point in the history
  • Loading branch information
q0w committed Jan 20, 2023
1 parent 1fb39d2 commit 7e02d9d
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 0 deletions.
28 changes: 28 additions & 0 deletions examples/adapters/refurb/.lintrunner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[[linter]]
code = 'REFURB'
include_patterns = [
'**/*.py',
]
exclude_patterns = []
command = [
'python',
'-m',
'lintrunner_adapters',
'run',
'refurb_linter',
'--config=pyproject.toml',
'--show-disable',
'--severity=FURB101:advice',
'--severity=FURB102:warning',
'--',
'@{{PATHSFILE}}'
]
init_command = [
'python',
'-m',
'lintrunner_adapters',
'run',
'pip_init',
'--dry-run={{DRYRUN}}',
'refurb==1.10.0',
]
164 changes: 164 additions & 0 deletions lintrunner_adapters/adapters/refurb_linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Refurbish and modernize Python code"""

from __future__ import annotations

import argparse
import logging
import re
import sys
from textwrap import dedent

from lintrunner_adapters import LintMessage, LintSeverity, add_default_options
from lintrunner_adapters._common.lintrunner_common import run_command

LINTER_CODE = "REFURB"

RESULTS_RE = re.compile(
r"""(?mx)
^
(?P<file>.*?):
(?P<line>\d+):
(?:(?P<column>-?\d+))?
(?:\s\[(?P<code>.*)\]:)?
\s(?P<message>.*)
$
"""
)


def _test_results_re() -> None:
"""Doctests.
>>> def t(s): return RESULTS_RE.search(s).groupdict()
>>> t(r"main.py:3:17 [FURB109]: Use `in (x, y, z)` instead of `in [x, y, z]`")
... # doctest: +NORMALIZE_WHITESPACE
{'file': 'main.py', 'line': '3', 'column': '17', 'code': 'FURB109',
'message': 'Use `in (x, y, z)` instead of `in [x, y, z]`'}
>>> t(r"main.py:4:5 [FURB101]: Use `y = Path(x).read_text()` instead of `with open(x, ...) as f: y = f.read()`")
... # doctest: +NORMALIZE_WHITESPACE
{'file': 'main.py', 'line': '4', 'column': '5', 'code': 'FURB101',
'message': 'Use `y = Path(x).read_text()` instead of `with open(x, ...) as f: y = f.read()`'}
"""
pass


def format_lint_message(message: str, code: str, show_disable: bool) -> str:
formatted = f"{message}\n"
if show_disable:
formatted += dedent(
f"""
To disable, use
[tool.refurb]
ignore = [{code}]
or
disable = [{code}]
"""
)
return formatted


def check_files(
filenames: list[str],
severities: dict[str, LintSeverity],
*,
config_file: str,
retries: int,
show_disable: bool,
) -> list[LintMessage]:
try:
proc = run_command(
[sys.executable, "-mrefurb", "--config-file", config_file] + filenames,
retries=retries,
)
except OSError as err:
return [
LintMessage(
path=None,
line=None,
char=None,
code=LINTER_CODE,
severity=LintSeverity.ERROR,
name="command-failed",
original=None,
replacement=None,
description=(f"Failed due to {err.__class__.__name__}:\n{err}"),
)
]
stdout = str(proc.stdout, "utf-8").strip()
return [
LintMessage(
path=match["file"],
name=match["code"] or "note",
description=format_lint_message(
match["message"],
match["code"],
show_disable,
),
line=int(match["line"]),
char=int(match["column"])
if match["column"] is not None and not match["column"].startswith("-")
else None,
code=LINTER_CODE,
severity=severities.get(match["code"], LintSeverity.ERROR),
original=None,
replacement=None,
)
for match in RESULTS_RE.finditer(stdout)
]


def main() -> None:
parser = argparse.ArgumentParser(
description=f"refurb wrapper linter. Linter code: {LINTER_CODE}",
fromfile_prefix_chars="@",
)
parser.add_argument(
"--config-file",
required=True,
help="path to pyproject.toml config file",
)
parser.add_argument(
"--severity",
action="append",
help="map code to severity (e.g. `FURB109:advice`)",
)
parser.add_argument(
"--show-disable",
action="store_true",
help="show how to disable a lint message",
)
add_default_options(parser)
args = parser.parse_args()

logging.basicConfig(
format="<%(threadName)s:%(levelname)s> %(message)s",
level=logging.NOTSET
if args.verbose
else logging.DEBUG
if len(args.filenames) < 1000
else logging.INFO,
stream=sys.stderr,
)

severities: dict[str, LintSeverity] = {}
if args.severity:
for severity in args.severity:
parts = severity.split(":", 1)
assert len(parts) == 2, f"invalid severity `{severity}`"
severities[parts[0]] = LintSeverity(parts[1])

lint_messages = check_files(
args.filenames,
severities,
config_file=args.config_file,
retries=args.retries,
show_disable=args.show_disable,
)
for lint_message in lint_messages:
lint_message.display()


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Test dependencies to avoid doctest import errors
add-trailing-comma==2.4.0
pyupgrade==3.3.1
refurb==1.10.0;python_version>=3.10
ufmt==2.0.1

0 comments on commit 7e02d9d

Please sign in to comment.