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