Tree Sitter

Tree Sitter: Metriken aus Ihrem Code

vg

Die Technologie, aber vor allem die Software-Entwicklung ist ein sich ständig veränderndes Feld. Diese Änderungen können organisch innerhalb des Teams erfolgen, wenn es mehr über das Problem erfährt, das es löst, und seine Lösung iteriert, oder aus externen Quellen stammen, wie z. B. neuen Paradigmen, die von den Frameworks empfohlen werden, die das Team verwendet. Damit ein Entwicklungsteam den Aufwand für diese Änderungen gering halten kann, muss es eine Möglichkeit geben, den Zustand der Codebasis zu messen. Diese werden oft als Entwicklungsmetriken bezeichnet und werden durch die Extraktion von „Fakten“ über den Quellcode zusammengestellt, die quantifiziert und verwendet werden können, um das Team dazu zu bringen, diese Menge zu erhöhen oder zu verringern. Dies läßt sich zum Beispiel mit Tree Sitter realisieren.

Ein Beispiel dafür wäre ein Team, das vor einigen Jahren ein React-basiertes Produkt entwickelt und das Zustandsverwaltungsparadigma verwendet hat, das damals als Best Practice galt – Redux und Higher Order Components. In den folgenden Jahren änderte sich das, was als Best Practice galt, wobei Hooks and Contexts diese älteren Paradigmen ersetzten.

Die Zeit

Wenn das Team mit dieser Veränderung konfrontiert wird, kann es auf verschiedene Weise reagieren:

  1. Sie können sich darauf konzentrieren, die Codebasis an die neuen Muster anzupassen, auf Kosten anderer Änderungen am Code, wie z. B. der Arbeit an Funktionen
  2. Sie können die neuen Muster anprangern und sich an das Paradigma halten, das sie kennen, und akzeptieren, dass es einen Punkt geben könnte, an dem sie die Bibliotheken, auf die sie angewiesen sind, aufgrund dieser Wahl nicht mehr aktualisieren können
  3. Sie können einen Plan erstellen, um die Codebasis neu auszurichten, während sie an anderen Arbeiten arbeiten, und den vorhandenen Code, den sie berühren, neu gestalten, während sie andere Dinge erstellen

Aus meiner Erfahrung sind es oft die beiden letztgenannten Ansätze, die verwendet werden, wobei der zweite häufiger vorkommt, als ich hoffen würde. Teams, die diesen Ansatz verfolgen, stecken im Wesentlichen den Kopf in den Sand, bis sie gezwungen sind, die Änderungen zu übernehmen, oder ihre Lösung komplett neu geschrieben werden muss, da sie im Vergleich zum Rest der Branche als Legacy-Code gilt.

Teams, die einen gesünderen Umgang mit ihren „technischen Schulden“ haben, wählen den dritten Ansatz. Diese Teams verstehen, dass ihr Code nie „perfekt“ sein wird, und entwickeln Tools, um die Schwankungen in Übereinstimmung mit internen und externen Mustern zu bewältigen.

Eines dieser Tools sind Entwicklungsmetriken.

Metriken für die Entwicklung von Gebäuden

Um eine Dev Metrics zu erstellen, müssen Sie in der Lage sein, Ihren Code zu quantifizieren, aber wie verwandeln Sie einen Haufen magischer Wörter, die einem Computer sagen, dass er Dinge tun soll, in Zahlen, die Sie in einem Diagramm darstellen können? – Sie definieren Muster und sehen, wie viel von Ihrem Code diesen entspricht.

Ein Muster kann etwas Einfaches sein, z. B. das Sicherstellen, dass Sie Semikolons am Ende Ihrer Codezeilen haben, oder es kann komplex sein, indem Sie überprüfen, ob Ihr Code dieselben Werte wie eine externe Ressource verwendet, um sicherzustellen, dass beide synchron sind.

Um ein Muster zu erstellen, müssen Sie in der Lage sein, Ihren Code zu analysieren. Es gibt verschiedene Möglichkeiten, dies zu tun:

Textabgleich

Ein Ansatz besteht darin, einen Abgleich mit einem Textmuster in Ihrem Code durchzuführen, häufig unter Verwendung eines regulären Ausdrucks (Regular Expression, Regex). Nützlich zum Überprüfen oder Extrahieren einfacher Zeichenfolgenwerte, kann aber falsch positive/negative Ergebnisse liefern, wenn es Inkonsistenzen in der Art und Weise gibt, wie der Code geschrieben wird.

Diese Art der Prüfung hat den Vorteil, dass sie schnell ist, und wenn Sie mit einem gewissen Maß an Ungenauigkeit umgehen können, kann sie schnell einen großen Mehrwert bieten. Es gibt jedoch einen Kompromiss, denn je genauer Sie versuchen, den Regex zu erstellen, desto komplexer wird er und desto weniger lesbar und wartbar wird er.

Nehmen Sie zum Beispiel die folgende Überprüfung, um eine bestimmte aufgerufene Methode abzugleichen.

^.*\(.*\).*=>.*cy\..*\(.*$

Er stimmt perfekt mit dem folgenden Code überein:

const someUseOfCypress = () => cy.find(".selector")

Aber wenn wir zu einem späteren Zeitpunkt eine Zeile vor diesem Code hinzufügen müssen, um etwas zu protokollieren, dann brechen wir die Prüfung.

const someUseOfCypress = () => {
cy.log("This is going to check something")
cy.find(".selector")
}

Und dann müssten wir den Regex so ändern, dass er sowohl die Inline-Pfeilfunktion als auch die mehrzeilige Pfeilfunktion behandelt, die sehr schwer zu lesen ist.

Syntax-Linting

Ein anderer Ansatz besteht darin, ein Linting-Tool wie eslint oder Grit zu verwenden, um ein Muster basierend auf der Syntax des Codes zu definieren. Diese Linting-Tools enthalten eine Darstellung des Codes, die abgefragt und verwendet werden kann, um eine Ausgabe für das Linting-Tool zum Melden zu erzeugen.

Um die Methodenprüfung von oben wiederzuverwenden, würde dies in Grit wie ein Abgleich mit dem Body einer const-, var- oder Funktionsdeklaration aussehen, um zu sehen, ob eine Methode der „cy“-Instanz darin verwendet wird.

engine marzano(0.1)
language js

or {
lexical_declaration($declarations) where $declarations <: contains member_expression(object="cy"),
variable_declaration($declarations) where $declarations <: contains member_expression(object="cy"),
function_declaration($body) where $body <: contains member_expression(object="cy")
}

Mit Linting-Tools können Sie in der Regel einen Schweregrad für Übereinstimmungen mit den Regeln definieren, sodass Sie die INFO-Stufe als Mittel verwenden können, um die Übereinstimmungen zu melden, ohne dass Code, der diesem Muster entspricht, als Fehler behandelt wird.

Abhängig von den Optionen, die als Teil des Linting-Tools verfügbar sind, müssen Sie möglicherweise zusätzliche Schritte ausführen, um die Anzahl für Ihre Entwicklungsmetrik zu erhalten. Dies kann das Lesen der Anzahl der Zeilen umfassen, die beim Ausführen des Tools in die Ausgabe des Tools ausgespuckt werden, oder das Analysieren einer serialisierten Ausgabe.

Grit bietet eine JSON-Ausgabe, was es zu einem guten Tool zum Sammeln von Entwicklungsmetriken macht, da Sie diese in ein Tool wie JQ leiten oder als Unterprozess ausführen und den stderr lesen und diesen JSON für die weitere Berechnung analysieren können.

# get the count of items that match a certain GritQL check
grit check --json 2>&1 | jq '.results[] | select(.localname == "CHECK_NAME") | length'

Syntax-linting ist genauer als textueller Abgleich, da es widerstandsfähiger gegenüber Änderungen der Struktur des Codes ist, sodass ein Zeilenumbruch oder zusätzliche Codezeilen in einer Funktion die Übereinstimmungen nicht beeinflussen.

Der Nachteil bei der Verwendung eines strukturellen Linting-Tools besteht darin, dass Sie nur Metriken darüber sammeln können, wie gut die Syntax Ihres Codes einem definierten Muster entspricht, Sie können keine Werte aus dem Code extrahieren, es sei denn, Sie führen eine zusätzliche Verarbeitung durch.

Abstrakte Syntax Baum Traversal

Ein anderer Ansatz besteht darin, die zugrunde liegende Technik zu verwenden, die von den Linting-Tools verwendet wird – Abstract Syntax Tree (AST) Traversal, um Werte aus Ihrem Code abzufragen und zu extrahieren. Das Syntax-linting-Tool Grit baut auf Tree Sitter auf.

Im Gegensatz zum Linting, bei dem Sie eine Abfrage definieren und entweder eine Anzahl oder eine Liste von Übereinstimmungen abrufen müssen, können Sie beim Durchlaufen des AST die Knoten extrahieren, die einem Prädikat entsprechen, und weitere Berechnungen für sie durchführen.

Wenn wir das Beispiel für die Methodenprüfung erneut verwenden, können wir einen Tree Sitter verwenden, um mit der Methode abzugleichen, aber auch den Knoten verwenden, um den Wert zu erhalten, der an die Argumente übergeben wird ( ).@selector

(
(call_expression
function: (
member_expression
object: (identifier) @instance
property: (property_identifier) @method
)
arguments: (
arguments (string (string_fragment) @selector)
)
)
(#eq? @instance "cy")
)

Wir können diese Werte dann verwenden, um weitere Analysen durchzuführen, z. B. möchten wir überprüfen, ob die an die Methode übergebenen Werte in einer externen Wertetabelle vorhanden sind, wir können dies tun und dann einen Bericht über alle fehlenden Werte erstellen.

Das war genau der Anwendungsfall, mit dem ich mich konfrontiert sah.

Verwenden von Tree Sitter zum Extrahieren von Werten aus Code

Ich habe Python verwendet, um mein Skript zu schreiben, aber ich denke, der Prozess wird in allen Sprachen ähnlich sein.

Der erste Schritt bestand darin, Tree Sitter und die Tree Sitter Grammatik für die Sprache zu installieren, in der der Code war. Der Code, den ich analysierte, war in TypeScript, also musste ich tree-sitter und tree-sitter-typescript installieren.

Nachdem ich die Bibliotheken installiert hatte, musste ich herausfinden, wie ich eine Abfrage gegen meinen Quellcode ausführen konnte. Die Tree Sitter-Dokumentation hilft in dieser Hinsicht wirklich nicht viel, aber zum Glück gibt es einige Beispiele in den tree-sitter Bibliotheken GithGit, GitHub und GitLabub, die zeigen, wie die Python-Bibliothek gegen einen Byte-String abfragen kann, also habe ich das angepasst, um eine Datei von der Festplatte als Byte-String zu lesen, und das hat funktioniert.

# import library
from tree_sitter import Language, Parser
import tree_sitter_typescript

# setup parser
language = Language(tree_sitter_typescript.language_typescript())
parser = Parser(language)
query = language.query("""
(
(call_expression
function: (
member_expression
object: (identifier) @instance
property: (property_identifier) @method
)
arguments: (
arguments (string (string_fragment) @selectors)
)
)
(#eq? @instance "cy")
)
""")

# either read file as byte-string or create a byte-string
source_code = b"""
const someUseOfCypress = () => {
cy.log("This is going to check something")
cy.find(".selector")
}
"""

# run query
source_code_tree = parser.parse(source_code)
captures = query.captures(source_code_tree)

Sobald ich eine Möglichkeit hatte, die Abfrage gegen den Quellcode auszuführen, musste ich die Abfrage schreiben. Dies war ein großer Lernschritt, da die S-Expression-Abfragesyntax von Tree Sitter die polnische Notation verwendet, was mich immer wieder überrascht.

Im Wesentlichen erstellen Sie beim Erstellen einer Abfrage einen Ausdruck, der mit einem Knoten übereinstimmt, und Sie können geschachtelte Ausdrücke verwenden, um sicherzustellen, dass Sie nur auf Knoten übereinstimmen, die untergeordnete Elemente haben, die ein Prädikat erfüllen. Sie können diese Knoten dann erfassen und außerhalb dieses Ausdrucks weiter auswerten.

Die Ausgabe dieser Abfrage ist ein Schlüssel-Wert-Paar aus der Erfassung und der Liste der Knoten im AST, die mit der Abfrage übereinstimmen.

Sie können dann den Baum des Knotens durchlaufen, um Werte aus dem AST zu extrahieren, in meinem Fall ging es darum, die Argumente zu extrahieren, die an die aufgerufene Methode übergeben wurden.

# get the captures
captures = query.captures(source_code_tree)
selector_nodes = captures["selectors"]

# for each capture walk the tree and get the value
selectors = []
for selector_node in selector_nodes:
cursor = selector_node.walk()
if cursor.node:
selector_name_node = cursor.node.child(1)
if selector_name_node:
selector_name_text_node = selector_name_node.child(1)
if selector_name_text_node and selector_name_text_node.text:
selectors.append(select_name_text_node.text.decode("utf-8"))

Ich konnte dann Werte in einem Satz speichern und diesen Satz mit einem anderen vergleichen, um zu analysieren, welche Werte fehlten.

selectors_in_code = set(selectors)
selectors_in_external = set(selector_strings_from_external_resource)
missing_from_external = (selectors_in_code - selectors_in_external)
missing_from_source_code = (selectors_in_external - selectors_in_code)

Einige Fallstricke

Der AST-Traversal ist nicht perfekt, wenn Sie eine Datei analysieren, die Konstanten oder Typen aus anderen Dateien importiert, können Sie ohne zusätzliche statische Verarbeitung oder dynamische Laufzeitverarbeitung nicht auf diese Definitionen zugreifen.

// Tree sitter won't know what SomeFancyType is so can't determine what
// properties are on "typedArg" but can tell you that it's calling
// cy.find with the selector property of typedArg
import SomeFancyType from './types'

const someUseOfCypress = (typedArg: SomeFancyType) => {
cy.log("This is going to check something", typedArg)
cy.find(typedArg.selector)
}

Es gibt Systeme, die dies ermöglichen, diese Systeme analysieren Ihre gesamte Codebasis und erstellen „Fakten“ über den Code. Um leistungsfähig zu bleiben, speichern diese Systeme in der Regel nicht den AST selbst, sondern die am häufigsten bewerteten Fakten über den Code.

Sie müssen also Ihre Bedürfnisse einschätzen, um zu entscheiden, ob nur der AST die Aufgabe erfüllt oder ob Sie etwas Größeres benötigen.

Fazit

Mit Tree Sitter war ich in der Lage, die in meinem Code verwendeten Werte zu extrahieren, um sie mit einer Tabelle abzugleichen, die von meinen nicht-technischen Kollegen verwendet wird, und wertvolles Feedback zu geben, um sicherzustellen, dass alle Parteien über neue Analyseereignisnamen informiert sind, die entweder dem Code oder dieser Tabelle hinzugefügt wurden.

Ich war in der Lage, dies auch zu einer Entwicklungsmetrik zusammenzustellen, damit wir die Vorfälle identifizieren können, bei denen Dinge aus dem Takt geraten, damit wir Wege finden können, die Anzahl der Fälle, in denen dies passiert, zu verringern.

Indem ich mehr darüber lerne, wie Tree Sitter funktioniert, und es verwende, um „Fakten“ aus dem Code zu extrahieren, beginne ich, ein grundlegendes Wissen darüber aufzubauen, wie ich als Entwickler Suchanfragen erstellen kann, die Fragen darüber beantworten, was der Code für nicht-technische Personen tut.

Weiterer interessanter Beitrag: Warum ich IDEs aufgegeben habe

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)