mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
216 lines
7.6 KiB
Python
216 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Refuse what the house rules ban: as an edit hook, or --staged."""
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.dirname(os.path.dirname(HERE))
|
|
SOURCES = (".cpp", ".h", ".mm", ".cxx")
|
|
LICENSE = "This file is part of "
|
|
HARNESS = "/SourceFiles/test/"
|
|
MARKER = "// WHY:"
|
|
MARKED_LINES = 3
|
|
PLAIN_LINES = 2
|
|
MARKED_BLOCKS = 1
|
|
|
|
|
|
def banned():
|
|
"""Name and replacement of every entry in CustomFunctions."""
|
|
with open(os.path.join(ROOT, ".clang-tidy"), encoding="utf-8") as stream:
|
|
return re.findall(r"\^::(\w+)\$,([^,;]+),", stream.read())
|
|
|
|
|
|
def hollow(match):
|
|
"""The literal's quotes around filler, so offsets stay put."""
|
|
quoted = match.group()
|
|
return quoted[0] + "_" * (len(quoted) - 2) + quoted[-1]
|
|
|
|
|
|
def blanked(line):
|
|
line = re.sub(r'"(?:[^"\\]|\\.)*"', hollow, line)
|
|
return re.sub(r"'(?:[^'\\]|\\.)*'", hollow, line)
|
|
|
|
|
|
def without_comments(text):
|
|
text = "\n".join(blanked(line) for line in text.split("\n"))
|
|
text = re.sub(r"//[^\n]*", "", text)
|
|
return re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
|
|
|
|
def comment_start(line):
|
|
"""Offset of the first comment marker outside literals, or -1."""
|
|
body = blanked(line)
|
|
found = [at for at in (body.find("//"), body.find("/*")) if at >= 0]
|
|
return min(found) if found else -1
|
|
|
|
|
|
def comment_text(line):
|
|
"""What a moved comment keeps: its text, not the code beside it."""
|
|
at = comment_start(line)
|
|
return line[at:].strip() if at >= 0 else line.strip()
|
|
|
|
|
|
def classify(line, inside):
|
|
"""("full" | "tail" | None, inside a /* */ block after this line)."""
|
|
body = blanked(line)
|
|
if inside:
|
|
return "full", "*/" not in body
|
|
at = comment_start(line)
|
|
if at < 0:
|
|
return None, False
|
|
opens = body[at:].startswith("/*") and "*/" not in body[at + 2:]
|
|
code = body[:at].strip()
|
|
if code == "}" or code.startswith("#"):
|
|
return None, opens # a closer or directive may carry its label
|
|
return ("tail" if code else "full"), opens
|
|
|
|
|
|
def blocks(numbered):
|
|
"""Comment blocks as (line number, lines): runs of whole-line
|
|
comments, and every trailing comment on its own."""
|
|
out, run, first, previous, inside = [], [], 0, None, False
|
|
for number, line in numbered:
|
|
gap = previous is not None and number != previous + 1
|
|
was = inside and not gap
|
|
kind, inside = classify(line, was)
|
|
if run and (gap or kind != "full"):
|
|
out.append((first, run))
|
|
run = []
|
|
if kind == "full" or (kind == "tail" and inside):
|
|
first = first if run else number
|
|
run.append(line)
|
|
if was and not inside:
|
|
out.append((first, run)) # a closed /* */ ends its block
|
|
run = []
|
|
elif kind == "tail":
|
|
out.append((number, [line]))
|
|
previous = number
|
|
if run:
|
|
out.append((first, run))
|
|
return out
|
|
|
|
|
|
def fault(block):
|
|
"""Why this comment block breaks the rule, or None."""
|
|
if any(LICENSE in line for line in block):
|
|
return None
|
|
if block[0].strip().startswith(MARKER):
|
|
return (f"a {MARKER} block runs {MARKED_LINES} lines at most"
|
|
if len(block) > MARKED_LINES else None)
|
|
return (f"a comment is one line; up to {MARKED_LINES} only when it opens"
|
|
f" with {MARKER}" if len(block) > 1 else None)
|
|
|
|
|
|
def written(payload):
|
|
"""The text this call put into the file, across all the edit shapes."""
|
|
tool = payload.get("tool_input") or {}
|
|
parts = [tool.get("new_string") or "", tool.get("content") or ""]
|
|
parts += [edit.get("new_string") or ""
|
|
for edit in tool.get("edits") or []]
|
|
return "\n".join(parts)
|
|
|
|
|
|
def replaced(payload, path):
|
|
"""What was there before, so moved code is not read as freshly written."""
|
|
tool = payload.get("tool_input") or {}
|
|
parts = [tool.get("old_string") or ""]
|
|
parts += [edit.get("old_string") or ""
|
|
for edit in tool.get("edits") or []]
|
|
if tool.get("content"):
|
|
# Relative to the file, so a submodule answers for its own files.
|
|
parts.append(subprocess.run(
|
|
["git", "show", "HEAD:./" + os.path.basename(path)],
|
|
cwd=os.path.dirname(path) or ".", capture_output=True,
|
|
text=True).stdout)
|
|
return "\n".join(parts)
|
|
|
|
|
|
def check_edit(payload, path):
|
|
text = written(payload)
|
|
found = [(name, into) for name, into in banned()
|
|
if re.search(r"\b" + name + r"\s*\(", without_comments(text))]
|
|
for name, into in found:
|
|
print(f"{name} is banned in this repository -- write {into} instead.",
|
|
file=sys.stderr)
|
|
was = replaced(payload, path)
|
|
for _, block in ([] if HARNESS in path
|
|
else blocks(list(enumerate(text.split("\n"))))):
|
|
why = fault(block)
|
|
if why and "\n".join(block) not in was:
|
|
found.append(True)
|
|
print(f"{len(block)}-line comment: {why}.", file=sys.stderr)
|
|
if not found:
|
|
return 0
|
|
print(f"The rules live in .clang-tidy and AGENTS.md. Fix"
|
|
f" {os.path.basename(path)} now, before anything else.",
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
def staged():
|
|
# No cwd: the hook answers for the repository it was invoked in.
|
|
diff = subprocess.run(["git", "diff", "--cached", "-U0", "-M"],
|
|
capture_output=True, text=True).stdout
|
|
path, number, added, dropped, bad = "", 0, {}, set(), 0
|
|
for line in diff.split("\n"):
|
|
if line.startswith("+++ b/"):
|
|
path, number = line[6:], 0
|
|
elif line.startswith("@@"):
|
|
number = int(re.search(r"\+(\d+)", line).group(1)) - 1
|
|
elif line.startswith("-") and not line.startswith("---"):
|
|
dropped.add(comment_text(line[1:]))
|
|
elif line.startswith("+") and not line.startswith("+++"):
|
|
number += 1
|
|
if path.endswith(SOURCES):
|
|
added.setdefault(path, []).append((number, line[1:]))
|
|
plain, marked = 0, 0
|
|
for path, lines in added.items():
|
|
for name, into in banned():
|
|
for number, line in lines:
|
|
if re.search(r"\b" + name + r"\s*\(", without_comments(line)):
|
|
print(f"{path}:{number}: {name} is banned -- write"
|
|
f" {into} instead.", file=sys.stderr)
|
|
bad += 1
|
|
for number, block in ([] if HARNESS in "/" + path
|
|
else blocks(lines)):
|
|
if any(LICENSE in one for one in block):
|
|
continue
|
|
if all(comment_text(one) in dropped for one in block):
|
|
continue
|
|
why = fault(block)
|
|
if why:
|
|
print(f"{path}:{number}: {why}.", file=sys.stderr)
|
|
bad += 1
|
|
elif block[0].strip().startswith(MARKER):
|
|
marked += 1
|
|
else:
|
|
plain += len(block)
|
|
if plain > PLAIN_LINES:
|
|
print(f"this commit adds {plain} comment lines; {PLAIN_LINES} is the"
|
|
" budget, and one marked block on top of it.", file=sys.stderr)
|
|
bad += 1
|
|
if marked > MARKED_BLOCKS:
|
|
print(f"this commit takes {marked} {MARKER} exceptions;"
|
|
f" {MARKED_BLOCKS} is the budget.", file=sys.stderr)
|
|
bad += 1
|
|
return 1 if bad else 0
|
|
|
|
|
|
def main():
|
|
if "--staged" in sys.argv:
|
|
return staged()
|
|
try:
|
|
payload = json.load(sys.stdin)
|
|
except (ValueError, OSError):
|
|
return 0
|
|
path = ((payload.get("tool_response") or {}).get("filePath")
|
|
or (payload.get("tool_input") or {}).get("file_path") or "")
|
|
return check_edit(payload, path) if path.endswith(SOURCES) else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|