- 2026-10-15
- 3 minuti
tokensave internals: modello dati e pipeline di estrazione del grafo

Questo è il secondo articolo della serie sull’architettura interna di tokensave. Nel primo articolo abbiamo visto cos’è un grafo semantico del codice e come è strutturata la pipeline ad alto livello. Qui entriamo nel codice Rust: come sono definiti i dati, come vengono estratti dai file sorgente e come viene mantenuta la sincronizzazione con il filesystem.
Il modello dei dati: nodi, archi e file
Il grafo ha tre entità principali, ognuna persistita in una tabella SQLite distinta.
Nodi (nodes)
Un nodo rappresenta una entità simbolica del codice. Ogni nodo ha:
id: hash deterministico content-addressed (SHA-256 di path + nome + tipo), che garantisce stabilità e deduplicazionekind: il tipo del simbolo, modellato dall’enumNodeKind- Posizione sorgente: file, riga di inizio, riga di fine, colonna
- Metadati: firma (
signature), docstring, visibilità, flagis_async - Metriche di complessità: branch, loop, nesting massimo, blocchi unsafe, chiamate non verificate
L’enum NodeKind conta oltre 65 varianti per coprire i costrutti di tutti i linguaggi supportati:
pub enum NodeKind {
// Universali
File, Module, Function, Method, Field, Const, Static, TypeAlias,
// OOP (Java, Kotlin, C#, Scala…)
Class, Interface, Constructor, Annotation, InnerClass, AbstractMethod,
// Rust-specifici
Struct, Enum, EnumVariant, Trait, Impl, Macro,
// Go-specifici
InterfaceType, StructMethod, GoPackage, StructTag,
// TypeScript/JavaScript
ArrowFunction, Decorator, Export, Namespace,
// C/C++
Union, Typedef, Include, PreprocessorDef, Template,
// … e molti altri (Dart, Pascal, Protobuf, GDScript…)
}
Lo schema SQL della tabella nodes riflette esattamente questa struttura:
CREATE TABLE nodes (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
name TEXT NOT NULL,
qualified_name TEXT NOT NULL,
file_path TEXT NOT NULL,
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
start_column INTEGER NOT NULL,
end_column INTEGER NOT NULL,
docstring TEXT,
signature TEXT,
visibility TEXT NOT NULL DEFAULT 'private',
is_async INTEGER NOT NULL DEFAULT 0,
branches INTEGER NOT NULL DEFAULT 0,
loops INTEGER NOT NULL DEFAULT 0,
returns INTEGER NOT NULL DEFAULT 0,
max_nesting INTEGER NOT NULL DEFAULT 0,
unsafe_blocks INTEGER NOT NULL DEFAULT 0,
unchecked_calls INTEGER NOT NULL DEFAULT 0,
assertions INTEGER NOT NULL DEFAULT 0,
cognitive_complexity INTEGER NOT NULL DEFAULT 0,
parent_id TEXT,
-- metriche Halstead
distinct_operators INTEGER NOT NULL DEFAULT 0,
distinct_operands INTEGER NOT NULL DEFAULT 0,
total_operators INTEGER NOT NULL DEFAULT 0,
total_operands INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
Archi (edges)
Un arco rappresenta una relazione tra due nodi. Il tipo di relazione è modellato dall’enum EdgeKind:
pub enum EdgeKind {
Contains, // un file/modulo contiene una funzione/classe
Calls, // una funzione chiama un'altra
Uses, // un simbolo referenzia un altro
Implements, // un tipo implementa un trait/interfaccia
TypeOf, // un campo/parametro ha un tipo
Returns, // una funzione restituisce un tipo
DerivesMacro, // una struct usa una macro derive (Rust)
Extends, // una classe eredita da un'altra
Annotates, // un'annotazione è applicata a un simbolo
Receives, // un metodo riceve un tipo come parametro
}
Lo schema SQL è semplice e ottimizzato per le traversate in entrambe le direzioni:
CREATE TABLE edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
target TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
line INTEGER -- riga della call site (opzionale)
);
-- Indici covering per traversata forward e backward
CREATE INDEX idx_edges_source_kind ON edges(source, kind);
CREATE INDEX idx_edges_target_kind ON edges(target, kind);
File (files)
La tabella files tiene traccia dello stato di indicizzazione di ogni file sorgente:
CREATE TABLE files (
path TEXT PRIMARY KEY,
content_hash TEXT NOT NULL, -- SHA-256 del contenuto
size INTEGER NOT NULL,
modified_at INTEGER NOT NULL,
indexed_at INTEGER NOT NULL,
node_count INTEGER NOT NULL DEFAULT 0
);
Il content_hash è il meccanismo chiave per la sincronizzazione incrementale: al prossimo tokensave sync, solo i file il cui hash è cambiato vengono ri-estratti.
Il parsing: tree-sitter come fondamenta
Il primo stadio della pipeline è il parsing del codice sorgente. tokensave delega questo lavoro a tree-sitter, una libreria di parsing incrementale che genera un Concrete Syntax Tree (CST) per oltre 100 linguaggi.
tree-sitter è stato scelto per diverse ragioni:
- Velocità: parser C generati a partire da grammatiche formali, compilati nell’eseguibile
- Robustezza agli errori: produce alberi parziali anche su codice sintaticamente incompleto
- Copertura linguistica: grammatiche disponibili per praticamente ogni linguaggio moderno
- Incrementalità: può aggiornare solo le parti dell’albero modificate tra una versione e l’altra del file
tokensave mantiene un registry degli estrattori linguistici, selezionando il parser corretto in base all’estensione del file:
src/extraction/
rust_extractor.rs
typescript_extractor.rs
python_extractor.rs
java_extractor.rs
go_extractor.rs
cpp_extractor.rs
… (50+ estrattori)
Alcune grammatiche (COBOL e Protobuf) sono vendorate direttamente come sorgente C nella directory vendor/, compilate tramite build.rs, per via di incompatibilità di versione con i crate disponibili su crates.io.
Gli estrattori linguistici
Ogni estrattore ha la stessa firma:
XxxExtractor::extract_source(path: &str, source: &str) -> ExtractionResult
Internamente, l’estrattore:
- Costruisce il CST dal sorgente tramite tree-sitter
- Visita i nodi dell’albero con un walker ricorsivo
- Accumula simboli, relazioni e riferimenti non risolti in un
ExtractionState - Restituisce un
ExtractionResultcon nodi, archi e riferimenti da risolvere
L’architettura degli estrattori è stateless: ogni chiamata è indipendente e non ha effetti collaterali sul database, il che li rende facilmente parallelizzabili.
Durante l’estrazione vengono calcolate anche le metriche di complessità:
- Complessità ciclomatica:
branches + 1 - Complessità cognitiva (stile SonarSource): pesata per profondità di annidamento
- Metriche di Halstead: operatori e operandi distinti/totali, da cui si derivano lunghezza, vocabolario e sforzo del programma
La risoluzione dei riferimenti cross-file
Un estrattore può rilevare che la funzione foo() chiama bar(), ma se bar è definita in un altro file, l’arco Calls non può essere creato immediatamente: non si conosce ancora l’ID del nodo target.
Questo è il problema che risolve il modulo resolution/. Dopo che tutti i file sono stati estratti, il ReferenceResolver:
- Carica tutti i nodi in memoria
- Costruisce un indice
nome → nodo - Itera su tutti i riferimenti non risolti (
unresolved_refs) - Per ciascuno, cerca il nodo corrispondente nell’indice
- Se trovato, crea il corretto arco tipizzato (
Calls,Uses,TypeOf, ecc.)
Questo secondo passaggio crea le connessioni cross-file che rendono il grafo davvero utile per analisi a livello di progetto.
La sincronizzazione incrementale
tokensave sync è ottimizzato per non ri-estrarre file non modificati. Il flusso è:
- Scansiona il filesystem (rispettando
.gitignore) - Calcola SHA-256 di ogni file trovato
- Confronta con gli hash in
files - Classifica ogni file in uno di tre insiemi:
- New: presenti su disco ma non nel DB → estrazione completa
- Modified: hash diverso → re-estrazione e sostituzione
- Removed: nel DB ma non su disco → eliminazione di nodi e archi in cascade
Grazie al ON DELETE CASCADE sulle foreign key degli archi, eliminare un nodo rimuove automaticamente tutti i suoi archi.
Prossimo articolo
Ora che il grafo è costruito e popolato, il passo successivo è interrogarlo. Nel terzo articolo della serie vediamo come tokensave implementa gli algoritmi di traversata (BFS, Tarjan SCC), espone i tool via server MCP e costruisce il contesto per gli agenti AI.
Riferimenti
- Sorgente tokensave 7.4.0: crates.io/crates/tokensave
- Repository ufficiale: github.com/aovestdipaperino/tokensave
- tree-sitter: tree-sitter.github.io
- Algoritmo di Tarjan (SCC): wikipedia.org/wiki/Tarjan’s_strongly_connected_components_algorithm













