Funktionen

Genau ein Symbol lesen, genau ein Symbol ändern

Was du davon hast

Dein Agent liest eine Funktion, eine Methode oder einen Typ beim Namen, ohne Zeilennummern zu suchen. Für eine Datei bekommt er ihre Gliederung. Geändert wird genauso gezielt: ein Symbol ersetzen, überall umbenennen, mehrere Dateien in einem Patch. Jede Änderung meldet sich mit einer Quittung, und greppy undo nimmt sie in einem Schritt zurück.

Vorher und nachher

Erste Aufgabe an serde, zum Lesen: Zeig mir ContentDeserializer::deserialize_any.

Ohne greppy

In serde/src/private/de.rs gibt es zwölf Funktionen namens deserialize_any. Der Agent sucht die richtige über ihren impl-Block und schneidet sie dann mit sed aus.

$ grep -n 'impl.*for ContentDeserializer\|fn deserialize_any' serde/src/private/de.rs
38:        fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, E>
1120:    impl<'de, E> Deserializer<'de> for ContentDeserializer<'de, E>
1126:        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
1546:        fn deserialize_any<V>(mut self, visitor: V) -> Result<V::Value, Self::Error>
…
$ sed -n '1126,/^        }$/p' serde/src/private/de.rs
        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de>,
        {
            match self.content {
                Content::Bool(v) => visitor.visit_bool(v),
…
        }

Mit greppy

$ greppy read ContentDeserializer::deserialize_any
serde/src/private/de.rs:1126-1154  ContentDeserializer::deserialize_any
        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de>,
        {
            match self.content {
                Content::Bool(v) => visitor.visit_bool(v),
…
                Content::Map(v) => visit_content_map(v, visitor),
            }
        }
ohne greppy mit greppy
Aufrufe 2 1
Zeichen Ausgabe 2.737 1.639
Token 696 364

Zweite Aufgabe, zum Ändern: Benenne get_lit_str überall in get_str_lit um. Die Änderung lief in einer privaten Kopie von serde.

Ohne greppy

Der Agent prüft jede Fundstelle darauf, dass sie wirklich die Funktion meint, und ersetzt dann per Textersetzung.

$ grep -rnw get_lit_str --include='*.rs' .
./serde_derive/src/internals/attr.rs:416:                    if let Some(s) = get_lit_str(cx, TAG, &meta)? {
./serde_derive/src/internals/attr.rs:438:                    if let Some(s) = get_lit_str(cx, CONTENT, &meta)? {
…
./serde_derive/src/internals/attr.rs:1411:fn get_lit_str(
…
./serde_derive/src/internals/attr.rs:1545:    let Some(string) = get_lit_str(cx, BORROW, meta)? else {
$ perl -pi -e 's/\bget_lit_str\b/get_str_lit/g' serde_derive/src/internals/attr.rs

Mit greppy

$ greppy rename get_lit_str get_str_lit
applied serde_derive/src/internals/attr.rs:416,438,490,787,1086,1411,1460,1481,1523,1545  a6f0ae
ohne greppy mit greppy
Aufrufe 2 1
Zeichen Ausgabe 1.039 97
Token 316 44

Beide Wege ergeben eine byte-identische Datei: die Definition und neun Aufrufe in serde_derive/src/internals/attr.rs heißen jetzt get_str_lit. Die Quittung nennt jede geänderte Zeile und die Edit-ID. Ein Befehl nimmt die ganze Umbenennung zurück:

$ greppy undo
reversed a6f0ae serde_derive/src/internals/attr.rs

So funktioniert es

read holt die Definition über den Index und druckt ihre aktuellen Bytes samt Doc-Kommentaren und Attributen. Jeder Lesebefehl gibt auf Wunsch (--handle) einen Handle aus, der Datei, Byte-Bereich und Inhalts-Hash festhält. replace-span lehnt ab, sobald sich die Datei seitdem geändert hat. rename ändert die Definition und jede Referenz, die der Graph auflöst, und lehnt ab, wenn der alte Name noch als Bezeichner ohne aufgelöste Referenz vorkommt. patch landet einen Unified Diff atomar über alle Dateien: Entweder ändern sich alle, oder keine. --verify prüft die berührten Dateien mit dem passenden lokalen Checker und meldet dessen Diagnosen. Jede Änderung bekommt eine Edit-ID, undo macht sie als Ganzes rückgängig.

Die Befehle

Befehl was er tut
greppy read S die Definition eines Symbols, byte-genau mit Doc-Kommentaren und Attributen; für eine Datei ihre Gliederung
greppy read-file F --lines A:B genau den Zeilenbereich A bis B einer Datei, inklusiv und ab 1 gezählt
greppy replace S eine Definition ersetzen, mit --body ihren Rumpf; der neue Text kommt als Argument oder über stdin
greppy patch --verify einen Unified Diff als eine Transaktion über alle Dateien anwenden und die berührten Dateien prüfen
greppy rename S NAME eine Definition und jede vom Graph aufgelöste Referenz umbenennen
greppy undo die letzte Änderung zurücknehmen, mit ID eine bestimmte

Mehr in der Doku