This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Check LF whitespace invariants for maintained, tracked text files."""
from __future__ import annotations
import argparse
import logging
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
LOGGER = logging.getLogger(__name__)
EXCLUDED_PREFIXES = ("test/googletest-1.16.0/",)
TRAILING_WHITESPACE = re.compile(rb"[ \t]+(?=\r?\n|\r|$)")
@dataclass(frozen=True)
class Violation:
path: str
line: int | str
message: str
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Check whitespace in tracked maintained text files."
)
parser.add_argument(
"--root",
type=Path,
help="repository root (defaults to the root containing this script)",
)
return parser.parse_args()
def repository_root(argument: Path | None) -> Path:
if argument is not None:
return argument.resolve()
return Path(__file__).resolve().parents[1]
def tracked_paths(root: Path) -> tuple[str, ...]:
result = subprocess.run(
("git", "-C", root, "ls-files", "-z"),
check=True,
stdout=subprocess.PIPE,
)
return tuple(
path.decode("utf-8")
for path in result.stdout.split(b"\0")
if path
)
def is_excluded(path: str) -> bool:
return any(
path == prefix.rstrip("/") or path.startswith(prefix)
for prefix in EXCLUDED_PREFIXES
)
def is_binary(content: bytes) -> bool:
return b"\0" in content
def line_number(content: bytes, position: int) -> int:
return content.count(b"\n", 0, position) + 1
def check_content(path: str, content: bytes) -> tuple[Violation, ...]:
violations: list[Violation] = []
for match in TRAILING_WHITESPACE.finditer(content):
violations.append(
Violation(
path,
line_number(content, match.start()),
"trailing whitespace",
)
)
if b"\r" in content:
violations.append(
Violation(
path,
"EOL",
"carriage return found; use LF line endings",
)
)
if content:
if not content.endswith(b"\n"):
violations.append(Violation(path, "EOF", "missing final newline"))
elif content[:-1].endswith(b"\n"):
violations.append(
Violation(path, "EOF", "multiple final newlines")
)
return tuple(violations)
def check_file(root: Path, path: str) -> tuple[Violation, ...]:
if is_excluded(path):
return ()
file_path = root / path
if not file_path.is_file():
return ()
content = file_path.read_bytes()
if is_binary(content):
return ()
return check_content(path, content)
def main() -> int:
arguments = parse_arguments()
root = repository_root(arguments.root)
violations: list[Violation] = []
try:
paths = tracked_paths(root)
except (OSError, subprocess.CalledProcessError) as error:
LOGGER.error("unable to list tracked files in %s: %s", root, error)
return 2
for path in paths:
violations.extend(check_file(root, path))
for violation in violations:
LOGGER.error(
"%s:%s: %s",
violation.path,
violation.line,
violation.message,
)
if violations:
LOGGER.error("%d whitespace violation(s) found", len(violations))
return 1
LOGGER.info("Whitespace check passed")
return 0
if __name__ == "__main__":
logging.basicConfig(format="%(message)s", level=logging.INFO)
raise SystemExit(main())