Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137#!/usr/bin/env python3"""Refuse a stdout line that prints a raw DID at a person.
A DID is an identifier and not a name. It is the right thing to send aservice and the wrong thing to show somebody: two cannot be told apart at aglance, neither can be typed back into any command, and a reader who hasnever been told that an account *has* a DID meets the concept for the firsttime in a line of output.
atgc has been fixing these one sweep at a time and keeps finding more, alwaysthe same way — somebody reads the output during unrelated work and notices.The listings were fixed by linking the shared label code; the write verbswere missed by that sweep precisely because they never built a label, theyformatted the DID straight into the line. This is that sweep made mechanical.
**Not a type check, and it does not pretend to be one.** It matches on the*name* of the interpolated expression, so it is defeated by an alias(`let a = did; println!("{a}")`) and it cannot see through a function thatreturns a DID. What it does catch is the form every one of these bugs hasactually taken. The type-level version is a newtype with no `Display`; seeplan/output.md for why that is a larger change than it looks.
`--json` output is exempt by construction: this reads `println!` and`print!`, and JSON leaves through `jsonout::emit`. A DID in a `--json` fieldis the right answer."""
import reimport sysfrom pathlib import Path
# What counts as a raw identifier, and the distinction the whole check turns# on: **a struct field holds what the record holds; a local holds what the# code computed.** `issue.author` is a DID off a record and always was;# `author` is nearly always a label somebody already resolved, and flagging# it produced five false positives for every true one. So a field access# ending in an identifier name is suspect, and a bare local is suspect only# when its name says outright that it is a DID.SUSPECT = re.compile(r"\.(did|author|owner|actor|acting|[a-z_]*_did)$|^([a-z_]*_)?did$")# Anything that turns one into something a person can read.NAMED = re.compile( r"hyperlink::|account\(|linked\(\)|\.label\(\)|named\(|_label\b|\.display\(\)|handle\(")MACROS = re.compile(r"\b(println|print)!\s*\(")
def calls(text): """Every `println!`/`print!` invocation, as (offset, body).""" for m in MACROS.search(text) and MACROS.finditer(text) or []: start = m.end() depth, i = 1, start while i < len(text) and depth: if text[i] == '(': depth += 1 elif text[i] == ')': depth -= 1 i += 1 yield m.start(), text[start : i - 1]
def interpolations(body): """The expressions this call interpolates: inline `{name}` captures and the arguments after the format string.""" fmt = re.match(r'\s*"((?:[^"\\]|\\.)*)"', body) if not fmt: return [] names = [n for n in re.findall(r"\{([a-zA-Z_][\w.]*)[^}]*\}", fmt.group(1))] rest = body[fmt.end() :].lstrip().lstrip(",") # Arguments split on top-level commas. args, depth, cur = [], 0, "" for ch in rest: if ch in "([{": depth += 1 elif ch in ")]}": depth -= 1 if ch == "," and depth == 0: args.append(cur) cur = "" else: cur += ch args.append(cur) return names + [a.strip() for a in args if a.strip()]
# A line may say out loud that its DID is deliberate. Written next to the# code rather than kept in a list here, so the reason is in front of whoever# is reading the line and so deleting the line deletes its exception.ALLOW = re.compile(r"raw-did-ok:")
def allowed_above(text, offset): """Whether the contiguous comment block above `offset` carries the marker.""" for line in reversed(text[:offset].split("\n")[:-1]): stripped = line.strip() if not stripped: continue if not stripped.startswith("//"): return False if ALLOW.search(stripped): return True return False
def main(): root = Path(__file__).resolve().parent.parent bad = [] for path in sorted((root / "src" / "cmd").rglob("*.rs")): text = path.read_text(encoding="utf-8") for offset, body in calls(text): line = text.count("\n", 0, offset) + 1 # The marker may sit on the call, or anywhere in the comment # block immediately above it — walked rather than windowed, # because an exception worth writing is usually worth several # lines and a fixed window silently stops honouring the long ones. if ALLOW.search(body) or allowed_above(text, offset): continue for expr in interpolations(body): if NAMED.search(expr): continue if SUSPECT.search(expr.strip().lstrip("&")): bad.append((path.relative_to(root), line, expr.strip())) for path, line, expr in bad: print(f"{path}:{line}: prints `{expr}` at a person, which is a raw DID") if bad: print() print( "A DID is for a service; a person gets `@handle`. Resolve it with\n" "`crate::clients::atproto::handles::handle(did).await` and name it with\n" "`crate::term::hyperlink::account(handle, did)`, which links the handle\n" "and falls back to the DID only when none resolves." ) return 1 if bad else 0
if __name__ == "__main__": sys.exit(main())