Blog
Strukturelle Navigation mit ehrlichen Lücken
Ich nutze strukturelle Navigation, um meine Lektüre einzugrenzen, nicht um sie zu ersetzen.
Mit einer Karte beginnen
In unbekanntem Code kann ein Index schneller auf Definitionen und mögliche Aufrufstellen hinweisen als wahlloses Lesen. Das spart Aufmerksamkeit, besonders bei häufigen Namen. Ich betrachte das Ergebnis als Wegweiser, nicht als Urteil über das Verhalten. Vor einer Änderung lese ich den betreffenden Quelltext und prüfe, ob Symbol, Signatur und Bedingungen wirklich zur gestellten Frage passen.
Meine erste Abfrage ist meist ein Symbolname. Findet der Index eine Definition, öffne ich die Datei und lese die umgebende Funktion; findet er mehrere, vergleiche ich die Modulpfade, bevor ich wähle. Auch die Liste der Aufrufstellen glaube ich nicht blind: Ich suche nach Zeichenketten und Konfigurationsschlüsseln, die denselben Ort auf einem anderen Weg erreichen könnten.
Gerade häufige Namen machen diese Disziplin wertvoll. Eine Abfrage nach einem allgemeinen Handler kann ein Dutzend Kandidaten in unzusammenhängenden Modulen liefern, und der kürzeste Weg ist, den ersten zu nehmen und aufzuhören. Ich verwende einen Moment auf die Modulgrenzen, denn die falsche Definition bleibt falsch, auch wenn ich sie sorgfältig lese.
Unsichere Verbindungen bleiben unsicher
Namen können mehrdeutig sein, Aufrufe indirekt erfolgen und manche Verbindungen erst zur Laufzeit entstehen. Eine fehlende Kante in einer strukturellen Ansicht beweist daher nicht, dass keine Beziehung existiert. Eine ungelöste Verbindung ist mir lieber als eine selbstbewusst erratene. Wo die Karte endet, folge ich den Hinweisen im Code: Woher kommt ein Wert, wohin wird er weitergegeben und was lässt sich statisch nicht entscheiden?
Zwei Fehlerarten sind möglich: eine fehlende Kante, wenn die Weitergabe dynamisch erfolgt, und eine falsche Kante, wenn zwei Symbole denselben Namen tragen. Beides sind Gründe, die Karte als Vermutung zu behandeln. Ich prüfe den Import am Dateianfang und den tatsächlichen Aufruf, bevor ich einer Verbindung glaube, und notiere die Fälle, die ich nicht entscheiden konnte.
Eine ehrliche Notiz ist nützlicher als ein vollständig wirkendes Bild. In einer Prüfung lese ich lieber, dass ein Pfad statisch nicht bestätigt werden konnte, damit ihn jemand beobachten kann, als einen selbstbewussten Pfeil zu sehen, den niemand geprüft hat. Die Notiz altert auch besser, weil die nächste Person genau weiß, wo die Karte endete.
{
"query": "handleRequest",
"resolved": false,
"reason": "three same-named symbols in different modules",
"candidates": [
{ "file": "src/api/handler.ts", "line": 42 },
{ "file": "src/jobs/handler.ts", "line": 17 },
{ "file": "src/tools/handler.ts", "line": 8 }
],
"callers": []
}Den wichtigen Pfad prüfen
Bei einer geplanten Änderung benenne ich ein konkretes gefährdetes Verhalten und lese genug Quelltext, um den Pfad erklären zu können. Danach wähle ich einen gezielten Test oder eine direkte Beobachtung, die meiner Deutung widersprechen könnte. Bleibt wegen dynamischen Verhaltens eine Lücke, halte ich sie fest, statt die Karte vollständig zu nennen. Strukturwerkzeuge sind stark, wenn sie Nachforschung erleichtern und Unsicherheit sichtbar lassen.
Für eine Änderung wähle ich ein Verhalten, das brechen könnte, und verfolge es von der Eingabe bis zur Wirkung, indem ich jeden Schritt im Quelltext lese. Dann wähle ich die kleinste Prüfung, die meiner Deutung widersprechen könnte. Lässt sich die Prüfung nicht isoliert ausführen, nenne ich den Grund und weiche auf eine festgehaltene Beobachtung aus. Entscheidend ist, dass die Prüfung genau den verfolgten Pfad trifft.
#!/usr/bin/env bash
set -euo pipefail
result=$(code-index query --symbol handleRequest)
if [ "$(echo "$result" | jq -r .resolved)" != "true" ]; then
# The map stopped here: read every candidate instead of guessing.
echo "$result" | jq -r '.candidates[].file' | sort -u | while read -r file; do
echo "read: $file"
done
exit 0
fi
echo "$result" | jq -r '.callers[].file' | sort -u