xref: /aosp_15_r20/external/pytorch/tools/linter/adapters/newlines_linter.py (revision da0073e96a02ea20f0ac840b70461e3646d07c45)
1"""
2NEWLINE: Checks files to make sure there are no trailing newlines.
3"""
4
5from __future__ import annotations
6
7import argparse
8import json
9import logging
10import sys
11from enum import Enum
12from typing import NamedTuple
13
14
15NEWLINE = 10  # ASCII "\n"
16CARRIAGE_RETURN = 13  # ASCII "\r"
17LINTER_CODE = "NEWLINE"
18
19
20class LintSeverity(str, Enum):
21    ERROR = "error"
22    WARNING = "warning"
23    ADVICE = "advice"
24    DISABLED = "disabled"
25
26
27class LintMessage(NamedTuple):
28    path: str | None
29    line: int | None
30    char: int | None
31    code: str
32    severity: LintSeverity
33    name: str
34    original: str | None
35    replacement: str | None
36    description: str | None
37
38
39def check_file(filename: str) -> LintMessage | None:
40    logging.debug("Checking file %s", filename)
41
42    with open(filename, "rb") as f:
43        lines = f.readlines()
44
45    if len(lines) == 0:
46        # File is empty, just leave it alone.
47        return None
48
49    if len(lines) == 1 and len(lines[0]) == 1:
50        # file is wrong whether or not the only byte is a newline
51        return LintMessage(
52            path=filename,
53            line=None,
54            char=None,
55            code=LINTER_CODE,
56            severity=LintSeverity.ERROR,
57            name="testestTrailing newline",
58            original=None,
59            replacement=None,
60            description="Trailing newline found. Run `lintrunner --take NEWLINE -a` to apply changes.",
61        )
62
63    if len(lines[-1]) == 1 and lines[-1][0] == NEWLINE:
64        try:
65            original = b"".join(lines).decode("utf-8")
66        except Exception as err:
67            return LintMessage(
68                path=filename,
69                line=None,
70                char=None,
71                code=LINTER_CODE,
72                severity=LintSeverity.ERROR,
73                name="Decoding failure",
74                original=None,
75                replacement=None,
76                description=f"utf-8 decoding failed due to {err.__class__.__name__}:\n{err}",
77            )
78
79        return LintMessage(
80            path=filename,
81            line=None,
82            char=None,
83            code=LINTER_CODE,
84            severity=LintSeverity.ERROR,
85            name="Trailing newline",
86            original=original,
87            replacement=original.rstrip("\n") + "\n",
88            description="Trailing newline found. Run `lintrunner --take NEWLINE -a` to apply changes.",
89        )
90    has_changes = False
91    original_lines: list[bytes] | None = None
92    for idx, line in enumerate(lines):
93        if len(line) >= 2 and line[-1] == NEWLINE and line[-2] == CARRIAGE_RETURN:
94            if not has_changes:
95                original_lines = list(lines)
96                has_changes = True
97            lines[idx] = line[:-2] + b"\n"
98
99    if has_changes:
100        try:
101            assert original_lines is not None
102            original = b"".join(original_lines).decode("utf-8")
103            replacement = b"".join(lines).decode("utf-8")
104        except Exception as err:
105            return LintMessage(
106                path=filename,
107                line=None,
108                char=None,
109                code=LINTER_CODE,
110                severity=LintSeverity.ERROR,
111                name="Decoding failure",
112                original=None,
113                replacement=None,
114                description=f"utf-8 decoding failed due to {err.__class__.__name__}:\n{err}",
115            )
116        return LintMessage(
117            path=filename,
118            line=None,
119            char=None,
120            code=LINTER_CODE,
121            severity=LintSeverity.ERROR,
122            name="DOS newline",
123            original=original,
124            replacement=replacement,
125            description="DOS newline found. Run `lintrunner --take NEWLINE -a` to apply changes.",
126        )
127
128    return None
129
130
131if __name__ == "__main__":
132    parser = argparse.ArgumentParser(
133        description="native functions linter",
134        fromfile_prefix_chars="@",
135    )
136    parser.add_argument(
137        "--verbose",
138        action="store_true",
139        help="location of native_functions.yaml",
140    )
141    parser.add_argument(
142        "filenames",
143        nargs="+",
144        help="paths to lint",
145    )
146
147    args = parser.parse_args()
148
149    logging.basicConfig(
150        format="<%(threadName)s:%(levelname)s> %(message)s",
151        level=logging.NOTSET
152        if args.verbose
153        else logging.DEBUG
154        if len(args.filenames) < 1000
155        else logging.INFO,
156        stream=sys.stderr,
157    )
158
159    lint_messages = []
160    for filename in args.filenames:
161        lint_message = check_file(filename)
162        if lint_message is not None:
163            lint_messages.append(lint_message)
164
165    for lint_message in lint_messages:
166        print(json.dumps(lint_message._asdict()), flush=True)
167