Eine Sprache hinzufügen
Eine Sprache der generischen Stufe ist eine deklarative Spezifikation, kein Parser. Eine der vollen Stufe ist ein kleines Interface. Beide brauchen keine neue Abhängigkeit.
Handbooks unterstützt 18 Sprachen über zwei Mechanismen. Eine zur generischen Stufe
hinzuzufügen ist meist ein einziges Objektliteral und braucht keine neue Abhängigkeit —
die Grammatiken werden bereits mit tree-sitter-wasms ausgeliefert.
Generische Stufe — eine deklarative Spezifikation
Füge einen Eintrag zu GENERIC_LANGUAGES in packages/analyzer/src/generic.ts hinzu:
{
name: 'elixir',
grammar: 'elixir', // the tree-sitter-wasms grammar name
extensions: ['.ex', '.exs'],
functionNodes: ['call'], // node types that define a function
classNodes: ['module'], // node types that define a container
callNodes: ['call'], // node types that are a call site
nameField: 'target', // where the name lives on those nodes
// …plus whatever the spec type asks for
}Danach registrieren — die Schleife am Ende von packages/analyzer/src/register.ts greift
jeden Eintrag in GENERIC_LANGUAGES automatisch ab, dort ist also nichts zu ergänzen.
Prüfe, ob die Grammatik mitgeliefert wird
ls node_modules/tree-sitter-wasms/out/ | grep elixirIst sie nicht da, braucht die Sprache eine neue Abhängigkeit, und das ist ein größeres Gespräch.
Finde die Knotentypen
node -e "
import('web-tree-sitter').then(async ({Parser, Language}) => {
await Parser.init();
const lang = await Language.load(require('fs').readFileSync(
require.resolve('tree-sitter-wasms/out/tree-sitter-elixir.wasm')));
const p = new Parser(); p.setLanguage(lang);
console.log(p.parse('defmodule Foo do\n def bar(x), do: x\nend').rootNode.toString());
});
"Der ausgegebene S-Ausdruck ist das Knotenvokabular, gegen das du die Spezifikation schreibst.
Schreibe die Spezifikation und einen Test
Jede Sprache bekommt einen Test, der ein echtes Mini-Repository in einem temporären
Verzeichnis baut und echte Knoten und Kanten prüft. Übernimm die Form eines bestehenden
Tests in packages/analyzer/src/generic.test.ts.
Ein gemockter Parse-Baum beweist nichts über eine Grammatik. Parse echte Quellen.
Deklariere ehrliche Fähigkeiten
Die generische Engine setzt diese für dich:
{ tier: 'generic', callTypes: GENERIC_CALL_TYPES, selfAttrs: false, statementSpans: false }Blase sie nicht auf. Der ganze Sinn der Deklaration ist, dass ein Leser eine Kante der generischen Stufe von einer in Python-Qualität unterscheiden kann. Siehe Analysetreue.
Aktualisiere die Doku, sonst scheitert der Build
Ein Drift-Test prüft, dass jede registrierte Sprache in beiden READMEs auftaucht. Füge
den Anzeigenamen zu DISPLAY in packages/cli/src/docs-drift.test.ts hinzu, danach zu:
README.mdundREADME.zh-CN.md— den Sprachtabellendocs/content/docs/reference/languages.mdxpackages/analyzer/README.mdund seinem chinesischen Zwilling
Diesen Test gibt es, weil die Liste bereits sechs Sprachen hinterherhinkte, bevor es jemandem auffiel.
Volle Stufe — den Adapter implementieren
Lohnt sich, wenn die Aufrufauflösung einer Sprache wirklich Typinformationen braucht: Attributtypen, Parameter-Annotationen, Vererbung.
export class ElixirAdapter implements LanguageAdapter {
readonly name = 'elixir';
readonly extensions = ['.ex', '.exs'] as const;
readonly capabilities: AdapterCapabilities = {
tier: 'full',
callTypes: ['self_method', 'internal_func', 'boundary', 'unresolved'],
selfAttrs: false,
statementSpans: true,
};
discover(sourceRoot: string): string[] {
return discoverByExtension(sourceRoot, this.extensions);
}
async analyze(files: readonly string[], sourceRoot: string, options?: { logger?: Logger }) {
// pass 1: declarations, imports, containers, per-function facts
// pass 2: resolve every call site into a typed edge
return { functions, edges };
}
async statementSpans(filePath: string, qualname: string) {
// 1-based inclusive spans — legal snap boundaries for resync
}
}Registriere ihn in packages/analyzer/src/register.ts:
registerAdapter('elixir', () => new ElixirAdapter());Das ist der gesamte Vertrag. Jede nachgelagerte Phase funktioniert unverändert.
Die Zwei-Durchgang-Regel
Durchgang 1 sammelt Deklarationen und baut Typindizes. Durchgang 2 läuft die Aufrufstellen ab, mit diesen Indizes in der Hand.
self.attr.method() oder param.method() in einem Durchgang aufzulösen ist unmöglich,
weil der Typ von attr aus einer Konstruktor-Zuweisung gelernt wird, die nach dem Aufruf
stehen kann. Jeder Adapter der vollen Stufe folgt dieser Form.
Die Auflösungsregel
Ein Aufruf, den du nicht festnageln kannst, wird unresolved, und der Graph-Builder stellt
ihn mit einer Kategorie und seinem Rohtext in dropped-calls.json unter Quarantäne.
Nie raten. Eine geratene Kante ist für alles Nachgelagerte von einer echten nicht zu unterscheiden, was Gruppierung, Co-Change-Hinweise und den Agent-Locator-Index auf einen Schlag vergiftet.
Die Offenlegungsregel
Dasselbe gilt für ganze Dateien. Eine Datei, die dein Adapter nicht lesen, nicht parsen
oder nur teilweise parsen kann, muss in ModuleAnalysis.unparsedFiles zurückkommen — und
darf nie stillschweigend übersprungen werden:
unreadable— das Lesen hat geworfen.detailist die errno-Meldung.unparsable— die Grammatik hat geworfen oder keinen Baum geliefert.partial—tree.rootNode.hasError. Die Fakten, die du extrahiert hast, sind echt; was im Fehlerknoten steckte, fehlt.
Das gemeinsame Rückgrat hält alle drei für dich fest, ein spezifikationsgetriebener
Adapter bekommt das also geschenkt. Die Pipeline wirft die ersten beiden aus
scannedFiles und schreibt jeden Eintrag nach phase1/scan-coverage.json. Eine Datei,
die still aus der Analyse verschwindet, ist der eine Fehlerfall, den nichts Nachgelagertes
bemerken kann.
Einen Adapter von außen registrieren
Die Registry ist öffentlich, du kannst also eine Sprache hinzufügen, ohne zu forken:
import { registerAdapter, registerBuiltinAdapters } from '@handbooks/analyzer';
import { MyAdapter } from './my-adapter.js';
registerBuiltinAdapters();
registerAdapter('mylang', () => new MyAdapter());Ein Adapter, der keine Fähigkeiten deklariert oder Unsinn deklariert, wird schlicht aus den Graph-Metadaten herausgelassen, statt dass eine Treue-Behauptung für ihn erfunden wird.
Checkliste
- Die Grammatik wird mit
tree-sitter-wasmsausgeliefert - Spezifikation oder Adapter geschrieben
- In
register.tsregistriert (Einträge der generischen Stufe werden automatisch abgegriffen) - Ein Test, der echte Quellen in einem temporären Verzeichnis parst
- Fähigkeiten ehrlich deklariert
- Unlesbare, nicht parsebare und teilweise geparste Dateien gemeldet, nie stillschweigend übersprungen
- Anzeigename zur
DISPLAY-Map des Drift-Tests hinzugefügt - Beide READMEs, die Analyzer-READMEs und die Sprachreferenz aktualisiert
-
pnpm checkläuft durch - Ein Changeset hinzugefügt
Entwicklung
Der Build, die Gates, die Konventionen, die das Werkzeug erzwingt, und warum die Tests nie einen API-Schlüssel brauchen.
Veröffentlichen
Changesets steuern Versionen und Changelogs. Das Veröffentlichen bleibt untätig, bis ein Token konfiguriert ist, sodass die Buchführung in jedem Fall stimmt.