Files
pytorch/tools/linter/adapters/grep_linter.py

275 lines
7.8 KiB
Python

"""
Generic linter that greps for a pattern and optionally suggests replacements.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import subprocess
import sys
import time
from enum import Enum
from typing import Any, NamedTuple
IS_WINDOWS: bool = os.name == "nt"
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, file=sys.stderr, flush=True, **kwargs)
class LintSeverity(str, Enum):
ERROR = "error"
WARNING = "warning"
ADVICE = "advice"
DISABLED = "disabled"
class LintMessage(NamedTuple):
path: str | None
line: int | None
char: int | None
code: str
severity: LintSeverity
name: str
original: str | None
replacement: str | None
description: str | None
def as_posix(name: str) -> str:
return name.replace("\\", "/") if IS_WINDOWS else name
def run_command(
args: list[str],
) -> subprocess.CompletedProcess[bytes]:
logging.debug("$ %s", " ".join(args))
start_time = time.monotonic()
try:
return subprocess.run(
args,
capture_output=True,
)
finally:
end_time = time.monotonic()
logging.debug("took %dms", (end_time - start_time) * 1000)
def lint_file(
matching_line: str,
allowlist_pattern: str,
replace_pattern: str,
linter_name: str,
error_name: str,
error_description: str,
) -> LintMessage | None:
# matching_line looks like:
# tools/linter/clangtidy_linter.py:13:import foo.bar.baz
split = matching_line.split(":")
filename = split[0]
if allowlist_pattern:
try:
proc = run_command(["grep", "-nEHI", allowlist_pattern, filename])
except Exception as err:
return LintMessage(
path=None,
line=None,
char=None,
code=linter_name,
severity=LintSeverity.ERROR,
name="command-failed",
original=None,
replacement=None,
description=(
f"Failed due to {err.__class__.__name__}:\n{err}"
if not isinstance(err, subprocess.CalledProcessError)
else (
"COMMAND (exit code {returncode})\n"
"{command}\n\n"
"STDERR\n{stderr}\n\n"
"STDOUT\n{stdout}"
).format(
returncode=err.returncode,
command=" ".join(as_posix(x) for x in err.cmd),
stderr=err.stderr.decode("utf-8").strip() or "(empty)",
stdout=err.stdout.decode("utf-8").strip() or "(empty)",
)
),
)
# allowlist pattern was found, abort lint
if proc.returncode == 0:
return None
original = None
replacement = None
if replace_pattern:
with open(filename) as f:
original = f.read()
try:
proc = run_command(["sed", "-r", replace_pattern, filename])
replacement = proc.stdout.decode("utf-8")
except Exception as err:
return LintMessage(
path=None,
line=None,
char=None,
code=linter_name,
severity=LintSeverity.ERROR,
name="command-failed",
original=None,
replacement=None,
description=(
f"Failed due to {err.__class__.__name__}:\n{err}"
if not isinstance(err, subprocess.CalledProcessError)
else (
"COMMAND (exit code {returncode})\n"
"{command}\n\n"
"STDERR\n{stderr}\n\n"
"STDOUT\n{stdout}"
).format(
returncode=err.returncode,
command=" ".join(as_posix(x) for x in err.cmd),
stderr=err.stderr.decode("utf-8").strip() or "(empty)",
stdout=err.stdout.decode("utf-8").strip() or "(empty)",
)
),
)
return LintMessage(
path=split[0],
line=int(split[1]) if len(split) > 1 else None,
char=None,
code=linter_name,
severity=LintSeverity.ERROR,
name=error_name,
original=original,
replacement=replacement,
description=error_description,
)
def main() -> None:
parser = argparse.ArgumentParser(
description="grep wrapper linter.",
fromfile_prefix_chars="@",
)
parser.add_argument(
"--pattern",
required=True,
help="pattern to grep for",
)
parser.add_argument(
"--allowlist-pattern",
help="if this pattern is true in the file, we don't grep for pattern",
)
parser.add_argument(
"--linter-name",
required=True,
help="name of the linter",
)
parser.add_argument(
"--match-first-only",
action="store_true",
help="only match the first hit in the file",
)
parser.add_argument(
"--error-name",
required=True,
help="human-readable description of what the error is",
)
parser.add_argument(
"--error-description",
required=True,
help="message to display when the pattern is found",
)
parser.add_argument(
"--replace-pattern",
help=(
"the form of a pattern passed to `sed -r`. "
"If specified, this will become proposed replacement text."
),
)
parser.add_argument(
"--verbose",
action="store_true",
help="verbose logging",
)
parser.add_argument(
"filenames",
nargs="+",
help="paths to lint",
)
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,
)
files_with_matches = []
if args.match_first_only:
files_with_matches = ["--files-with-matches"]
try:
proc = run_command(
["grep", "-nEHI", *files_with_matches, args.pattern, *args.filenames]
)
except Exception as err:
err_msg = LintMessage(
path=None,
line=None,
char=None,
code=args.linter_name,
severity=LintSeverity.ERROR,
name="command-failed",
original=None,
replacement=None,
description=(
f"Failed due to {err.__class__.__name__}:\n{err}"
if not isinstance(err, subprocess.CalledProcessError)
else (
"COMMAND (exit code {returncode})\n"
"{command}\n\n"
"STDERR\n{stderr}\n\n"
"STDOUT\n{stdout}"
).format(
returncode=err.returncode,
command=" ".join(as_posix(x) for x in err.cmd),
stderr=err.stderr.decode("utf-8").strip() or "(empty)",
stdout=err.stdout.decode("utf-8").strip() or "(empty)",
)
),
)
print(json.dumps(err_msg._asdict()), flush=True)
sys.exit(0)
lines = proc.stdout.decode().splitlines()
for line in lines:
lint_message = lint_file(
line,
args.allowlist_pattern,
args.replace_pattern,
args.linter_name,
args.error_name,
args.error_description,
)
if lint_message is not None:
print(json.dumps(lint_message._asdict()), flush=True)
if __name__ == "__main__":
main()