Language Server Index Format Specification - 0.4.0
Language Server Index Format
Der Zweck des Language Server Index Format (LSIF) ist die Definition eines Standardformats für Language Server oder andere Programmierwerkzeuge, um ihr Wissen über einen Workspace zu exportieren. Dieser Export kann später verwendet werden, um Language Server LSP Anfragen für denselben Workspace zu beantworten, ohne den Language Server selbst ausführen zu müssen. Da ein Großteil der Informationen durch eine Änderung am Workspace ungültig würde, schließt der exportierte Dump typischerweise Anfragen aus, die beim Ändern eines Dokuments verwendet werden. Zum Beispiel ist das Ergebnis einer Autovervollständigungsanfrage typischerweise nicht Teil eines solchen Dumps.
Changelog
Version 0.4.0
Bis zur Version 0.4.0 lag der Fokus des LSIF-Formats darin, die Erstellung des Dumps für Language-Tool-Anbieter zu erleichtern. Dies machte es für Konsumenten des Dumps jedoch sehr schwierig, ihn effizient in eine Datenbank zu importieren, es sei denn, das Datenbankformat entsprach eins zu eins dem LSIF-Format. Diese Version der Spezifikation versucht, dies auszubalancieren, indem sie von Tool-Anbietern verlangt, zusätzliche Ereignisse auszugeben, wenn bestimmte Daten zum Konsum bereit sind. Sie fügt auch Unterstützung für die Partitionierung von Daten pro Dokument hinzu.
Da 0.4.0 einige LSIF-Aspekte tiefergreifend ändert, ist eine alte 0.3.x Version der Spezifikation hier verfügbar.
Motivation
Hauptentwicklungsziele
- Das Format sollte nicht die Verwendung einer bestimmten Persistenztechnologie implizieren.
- Die definierten Daten sollten so nah wie möglich am Language Server Protocol modelliert werden, um es zu ermöglichen, die Daten über LSP ohne weitere Transformation bereitzustellen.
- Die gespeicherten Daten sind Ergebnisdaten, die normalerweise von einer LSP-Anfrage zurückgegeben werden. Der Dump enthält keine Programmsymbolinformationen und LSIF definiert auch keine Symbolsemantik (z. B. wo ein Symbol definiert oder referenziert wird oder wann eine Methode eine andere überschreibt). LSIF definiert daher keine Symbol-Datenbank. Bitte beachten Sie, dass dies im Einklang mit LSP selbst steht, das ebenfalls keine Symbolsemantik definiert.
- Das Ausgabeformat basiert, wie bei LSP, auf JSON.
LSP-Anfragen, die sich gut für die Unterstützung in LSIF eignen, sind
textDocument/documentSymboltextDocument/foldingRangetextDocument/documentLinktextDocument/definitiontextDocument/declarationtextDocument/typeDefinitiontextDocument/hovertextDocument/referencestextDocument/implementation
Die entsprechenden LSP-Anfragen haben eine der folgenden beiden Formen
request(uri, method) -> result
request(uri, position, method) -> result
wobei `method` die JSON-RPC-Anfragemethode ist.
Konkrete Beispiele sind
request('file:///Users/dirkb/sample/test.ts', 'textDocument/foldingRange') -> FoldingRange[];
request('file:///Users/dirkb/sample/test.ts', { line: 10, character: 17 }, 'textDocument/hover') -> Hover;
Das Eingabetupel für eine Anfrage ist entweder [uri, method] oder [uri, position, method] und die Ausgabe ist eine Form von Ergebnis. Für dasselbe uri und Tupel [uri, position] gibt es viele verschiedene Anfragen, die ausgeführt werden müssen.
Das Dump-Format sollte daher die folgenden Funktionen unterstützen
- Eingabedaten müssen einfach abfragbar sein (z. B. das Dokument und die Position).
- Jedes Element hat eine eindeutige ID (die eine Zeichenkette oder eine Zahl sein kann).
- Es sollte möglich sein, Daten auszugeben, sobald sie verfügbar sind, um Streaming statt großer Speicheranforderungen zu ermöglichen. Beispielsweise sollten Daten basierend auf der Dokumentensyntax für jede Datei ausgegeben werden, während die Analyse fortschreitet.
- Es sollte einfach sein, später zusätzliche Anfragen hinzuzufügen.
- Es sollte für ein Werkzeug einfach sein, einen Dump zu konsumieren und ihn beispielsweise in eine Datenbank zu importieren, ohne den Dump im Speicher zu halten.
Wir sind zu dem Schluss gekommen, dass der flexibelste Weg zur Ausgabe dies ein Graph ist, bei dem Kanten die Methode darstellen und Knoten [uri], [uri, position] oder ein Anfrageergebnis sind. Diese Daten könnten dann als JSON gespeichert oder in eine Datenbank geladen werden, die diese Knoten und Beziehungen darstellen kann.
Angenommen, es gibt eine Datei /Users/dirkb/sample.ts und wir möchten die Faltungsbereichsinformationen dazu speichern, dann gibt der Indexer zwei Knoten aus: einen, der das Dokument mit seiner URI file:///Users/dirkb/sample.ts repräsentiert, und einen anderen, der das Faltungsergebnis repräsentiert. Zusätzlich wird eine Kante ausgegeben, die die Anfrage textDocument/foldingRange repräsentiert.
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 2, type: "vertex", label: "foldingRangeResult", result: [ { ... }, { ... }, ... ] }
{ id: 3, type: "edge", label: "textDocument/foldingRange", outV: 1, inV: 2 }
Der entsprechende Graph sieht wie folgt aus

Bereiche (Ranges)
Für Anfragen, die eine Position als Eingabe nehmen, müssen wir auch die Position speichern. Normalerweise geben LSP-Anfragen dasselbe Ergebnis für Positionen zurück, die auf dasselbe Wort/denselben Namen in einem Dokument zeigen. Nehmen wir das folgende TypeScript-Beispiel
function bar() {
}
Eine Hover-Anfrage für eine Position, die das b in bar bezeichnet, gibt dasselbe Ergebnis zurück wie eine Position, die das a oder r bezeichnet. Um den Dump kompakter zu gestalten, werden stattdessen Bereiche (Ranges) verwendet, um dies zu erfassen, anstatt einzelne Positionen. In diesem Fall werden die folgenden Knoten ausgegeben. Beachten Sie, dass Zeile und Spalte nullbasiert sind, wie in LSP
{ id: 4, type: "vertex", label: "range", start: { line: 0, character: 9}, end: { line: 0, character: 12 } }
Um den Bereich an ein Dokument zu binden, verwenden wir eine spezielle Kante mit der Bezeichnung contains, die von einem Dokument zu einer Menge von Bereichen zeigt.
{ id: 5, type: "edge", label: "contains", outV: 1, inVs: [4] }
LSIF unterstützt 1:n-Kanten für die contains-Beziehung, die in einem Graphen leicht auf n 1:1-Kanten abgebildet werden kann. LSIF unterstützt dies aus zwei Gründen: (a) um die Ausgabe kompakter zu gestalten, da ein Dokument normalerweise Hunderte dieser Bereiche enthält, und (b) um den Import und die Stapelverarbeitung für Konsumenten eines LSIF-Dumps zu erleichtern.
Um das Hover-Ergebnis an den Bereich zu binden, verwenden wir dasselbe Muster, das wir für Faltungsbereiche verwendet haben. Wir geben einen Knoten aus, der das Hover-Ergebnis repräsentiert, und eine Kante, die die Anfrage textDocument/hover repräsentiert.
{
id: 6,
type: "vertex",
label: "hoverResult",
result: {
contents: [
{ language: "typescript", value: "function bar(): void" }
]
}
}
{ id: 7, type: "edge", label: "textDocument/hover", outV: 4, inV: 6 }
Der entsprechende Graph sieht wie folgt aus

Die für ein Dokument im `contains`-Verhältnis ausgegebenen Bereiche müssen diese Regeln befolgen
- eine gegebene Bereichs-ID kann nur in einem Dokument enthalten sein, d. h. Bereiche dürfen nicht über Dokumente hinweg geteilt werden, auch wenn sie denselben Start-/Endwert haben.
- Keine zwei Bereiche können gleich sein.
- Keine zwei Bereiche dürfen sich überschneiden und dieselbe Position in einem Dokument beanspruchen, es sei denn, ein Bereich ist vollständig in einem anderen enthalten.
Wenn eine Position in einem Dokument einem Bereich zugeordnet wird und mehr als ein Bereich die Position abdeckt, sollte der folgende Algorithmus verwendet werden
- sortiere die Bereiche nach Einbettungstiefe mit der innersten zuerst
- für jeden Bereich in den Bereichen do
- prüfe, ob der Bereich eine ausgehende Kante
textDocument/${method}hat - wenn ja, verwende sie
- prüfe, ob der Bereich eine ausgehende Kante
- end
- gib
nullzurück
Ergebnis-Menge (Result Set)
Normalerweise ist das Hover-Ergebnis dasselbe, ob man über eine Definition einer Funktion oder über eine Referenz dieser Funktion schwebt. Dasselbe gilt tatsächlich für viele LSP-Anfragen wie textDocument/definition, textDocument/references oder textDocument/typeDefinition. In einem naiven Modell hätte jeder Bereich ausgehende Kanten für all diese LSP-Anfragen und würde auf die entsprechenden Ergebnisse zeigen. Um dies zu optimieren und den Graphen leichter verständlich zu machen, wird das Konzept einer ResultSet eingeführt. Eine Ergebnis-Menge dient als Drehscheibe, um Informationen zu speichern, die für viele Bereiche gemeinsam sind. Die ResultSet selbst trägt keine Informationen. Sie sieht also so aus
export interface ResultSet {
}
Die entsprechende Ausgabe des obigen Beispiels mit einem Hover, der eine Ergebnis-Menge verwendet, sieht so aus
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 2, type: "vertex", label: "resultSet" }
{ id: 3, type: "vertex", label: "range", start: { line: 0, character: 9}, end: { line: 0, character: 12 } }
{ id: 4, type: "edge", label: "contains", outV: 1, inVs: [3] }
{ id: 5, type: "edge", label: "next", outV: 3, inV: 2 }
{ id: 6, type: "vertex", label: "hoverResult", result: {"contents":[{"language":"typescript","value":"function bar(): void"},""] }
{ id: 7, type: "edge", label: "textDocument/hover", outV: 2, inV: 6 }

Ergebnis-Mengen werden mit einer next-Kante mit Bereichen verknüpft. Eine Ergebnis-Menge kann auch Informationen an eine andere Ergebnis-Menge weiterleiten, indem sie mit einer next-Kante darauf verweist.
Das Muster zum Speichern des Ergebnisses mit der ResultSet wird auch für andere Anfragen verwendet. Der Suchalgorithmus für eine Anfrage [document, position, method] ist daher wie folgt
- finde alle Bereiche für [document, position]. Wenn keine existieren, gib
nullals Ergebnis zurück - sortiere die Bereiche nach Einbettungstiefe, die innerste zuerst
- für jeden Bereich in den Bereichen do
- weise den Bereich `out` zu
- while out !==
null- prüfe, ob `out` eine ausgehende Kante
textDocument/${method}hat. Wenn ja, verwende sie und gib das entsprechende Ergebnis zurück - prüfe, ob `out` eine ausgehende
next-Kante hat. Wenn ja, setze `out` auf den Zielknoten. Andernfalls setze `out` aufnull
- prüfe, ob `out` eine ausgehende Kante
- end
- end
- andernfalls gib
nullzurück
Sprachfunktionen
Anfrage: textDocument/definition
Dasselbe Muster, eine Range, eine Ergebnis-Menge oder ein Dokument mit einer Anfragekante zu einem Methoden-Ergebnis zu verbinden, wird auch für andere Anfragen verwendet. Betrachten wir als Nächstes die Anfrage textDocument/definition unter Verwendung des folgenden TypeScript-Beispiels
function bar() {
}
function foo() {
bar();
}
Dies gibt die folgenden Knoten und Kanten aus, um die Anfrage textDocument/definition zu modellieren
// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// The bar declaration
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range", start: { line: 0, character: 9 }, end: { line: 0, character: 12 } }
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The bar reference
{ id: 20, type: "vertex", label: "range", start: { line: 4, character: 2 }, end: { line: 4, character: 5 } }
{ id: 21, type: "edge", label: "next", outV: 20, inV: 6}
// The definition result linked to the bar result set
{ id: 22, type: "vertex", label: "definitionResult" }
{ id: 23, type: "edge", label: "textDocument/definition", outV: 6, inV: 22 }
{ id: 24, type: "edge", label: "item", outV: 22, inVs: [9], document: 4 }

Das Definitions-Ergebnis oben hat nur einen Wert (die Range mit der ID ‘9’) und wir hätten es direkt ausgeben können. Wir haben jedoch den Knoten `definitionResult` aus zwei Gründen eingeführt
- Um Konsistenz mit allen anderen Anfragen zu gewährleisten, die auf ein Ergebnis verweisen.
- Um Unterstützung für Sprachen zu haben, bei denen eine Definition über mehrere Bereiche oder sogar mehrere Dokumente verteilt sein kann. Um mehrere Dokumente zu unterstützen, werden Bereiche zu einem Definitions-Ergebnis über eine 1:N
item-Kante hinzugefügt. Konzeptionell ist ein Definitions-Ergebnis ein Array, dem dieitem-Kante Elemente hinzufügt.
Betrachten Sie das folgende TypeScript-Beispiel
interface X {
foo();
}
interface X {
bar();
}
let x: X;
Wenn Sie in let x: X auf X klicken, wird ein Dialog angezeigt, der dem Benutzer die Auswahl zwischen den beiden Definitionen des interface X ermöglicht. Das ausgegebene JSON sieht in diesem Fall wie folgt aus
{ id : 38, type: "vertex", label: "definitionResult" }
{ id : 40, type: "edge", label: "item", outV: 38, inVs: [9, 13], document: 4 }
Die item-Kante hat als zusätzliche Eigenschaft `document`, die angibt, in welchem Dokument sich diese Deklarationen befinden. Wir haben diese Information hinzugefügt, um die einfache Ausgabe der Daten zu ermöglichen und gleichzeitig die einfache Verarbeitung der Daten zur Speicherung in einer Datenbank zu gewährleisten. Ohne diese Information müssten wir entweder eine Reihenfolge angeben, in der Daten ausgegeben werden müssen (z. B. eine Item-Kante, die nur auf eine Range verweist, die bereits über eine contains-Kante zu einem Dokument hinzugefügt wurde) oder wir zwingen die Verarbeitungstools, viele Knoten und Kanten im Speicher zu halten. Der Ansatz, diese document-Eigenschaft zu haben, scheint ein fairer Kompromiss zu sein.
Anfrage: textDocument/declaration
Es gibt Programmiersprachen, die das Konzept von Deklarationen und Definitionen kennen (wie C/C++). In diesem Fall kann der Dump einen entsprechenden declarationResult-Knoten und eine textDocument/declaration-Kante enthalten, um die Informationen zu speichern. Sie werden analog zu den Entitäten behandelt, die für die Anfrage textDocument/definition ausgegeben werden.
Mehr zur Anfrage: textDocument/hover
In LSP wird der Hover wie folgt definiert
export interface Hover {
/**
* The hover's content
*/
contents: MarkupContent | MarkedString | MarkedString[];
/**
* An optional range
*/
range?: Range;
}
wobei der optionale Bereich der Namensbereich des Wortes ist, über das gehovert wird.
Seitennotiz: Dies ist ein Muster, das auch für andere LSP-Anfragen verwendet wird, bei denen das Ergebnis den Wortbereich des Wortes enthält, auf das der Positions-Parameter gezeigt hat.
Dies macht den Hover für jede Position unterschiedlich, daher können wir ihn nicht wirklich mit der Ergebnis-Menge speichern. Aber warten Sie, der Bereich ist der Bereich einer der bar-Referenzen, die wir bereits ausgegeben und verwendet haben, um mit der Berechnung des Ergebnisses zu beginnen. Um den Hover wiederverwendbar zu machen, bitten wir den Indexserver, den Startbereich zu füllen, wenn im Ergebnis kein Bereich definiert ist. Bei einer Hover-Anfrage, die auf dem Bereich { line: 4, character: 2 }, end: { line: 4, character: 5 } ausgeführt wird, ist das Hover-Ergebnis
{ id: 6, type: "vertex", label: "hoverResult", result: { contents: [ { language: "typescript", value: "function bar(): void" } ], range: { line: 4, character: 2 }, end: { line: 4, character: 5 } } }
Anfrage: textDocument/references
Das Speichern von Referenzen erfolgt auf die gleiche Weise wie das Speichern von Hover- oder Go-to-Definition-Bereichen. Es verwendet einen `referenceResult`-Knoten und `item`-Kanten, um Bereiche zum Ergebnis hinzuzufügen.
Betrachten Sie das folgende Beispiel
function bar() {
}
function foo() {
bar();
}
Die relevante JSON-Ausgabe sieht so aus
// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// The bar declaration
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range", start: { line: 0, character: 9 }, end: { line: 0, character: 12 } }
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The bar reference range
{ id: 20, type: "vertex", label: "range", start: { line: 4, character: 2 }, end: { line: 4, character: 5 } }
{ id: 21, type: "edge", label: "next", outV: 20, inV: 6 }
// The reference result
{ id : 25, type: "vertex", label: "referenceResult" }
// Link it to the result set
{ id : 26, type: "edge", label: "textDocument/references", outV: 6, inV: 25 }
// Add the bar definition as a reference to the reference result
{ id: 27, type: "edge", label: "item", outV: 25, inVs: [9], document: 4, property : "definitions" }
// Add the bar reference as a reference to the reference result
{ id: 28, type: "edge", label: "item", outV: 25, inVs: [20], document:4, property: "references" }

Wir kennzeichnen die item-Kante mit der ID 27 als Definition, da das Referenzergebnis zwischen Definitionen, Deklarationen und Referenzen unterscheidet. Dies geschieht, da die Anfrage textDocument/references einen zusätzlichen Eingabeparameter includeDeclarations aufweist, der steuert, ob Deklarationen und Definitionen ebenfalls in das Ergebnis einbezogen werden. Drei separate Eigenschaften ermöglichen es dem Server, das Ergebnis entsprechend zu berechnen.
Die `item`-Kante unterstützt auch die Verknüpfung von Referenzergebnissen mit anderen Referenzergebnissen. Dies ist nützlich, wenn Referenzen auf Methoden berechnet werden, die in einer Typenhierarchie überschrieben werden.
Nehmen Sie das folgende Beispiel
interface I {
foo(): void;
}
class A implements I {
foo(): void {
}
}
class B implements I {
foo(): void {
}
}
let i: I;
i.foo();
let b: B;
b.foo();
Das Referenzergebnis für die Methode foo in TypeScript enthält alle drei Deklarationen und beide Referenzen. Während der Analyse des Dokuments wird ein Referenzergebnis erstellt und dann für alle Ergebnis-Mengen gemeinsam genutzt.
Die Ausgabe sieht so aus
// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// The declaration of I#foo
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 16, type: "vertex", label: "range", start: { line: 1, character: 2 }, end: { line: 1, character: 5 } }
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
// The reference result for I#foo
{ id: 30, type: "vertex", label: "referenceResult" }
{ id: 31, type: "edge", label: "textDocument/references", outV: 13, inV: 30 }
// The declaration of A#foo
{ id: 29, type: "vertex", label: "resultSet" }
{ id: 34, type: "vertex", label: "range", start: { line: 5, character: 2 }, end: { line: 5, character: 5 } }
{ id: 35, type: "edge", label: "next", outV: 34, inV: 29 }
// The declaration of B#foo
{ id: 47, type: "vertex", label: "resultSet" }
{ id: 50, type: "vertex", label: "range", start: { line: 10, character: 2 }, end: { line: 10, character: 5 } }
{ id: 51, type: "edge", label: "next", outV: 50, inV: 47 }
// The reference i.foo()
{ id: 65, type: "vertex", label: "range", start: { line: 15, character: 2 }, end: { line: 15, character: 5 } }
// The reference b.foo()
{ id: 78, type: "vertex", label: "range", start: { line: 18, character: 2 }, end: { line: 18, character: 5 } }
// The insertion of the ranges into the shared reference result
{ id: 90, type: "edge", label: "item", outV: 30, inVs: [16,34,50], document: 4, property: definitions }
{ id: 91, type: "edge", label: "item", outV: 30, inVs: [65,78], document: 4, property: references }
// Linking A#foo to I#foo
{ id: 101, type: "vertex", label: "referenceResult" }
{ id: 102, type: "edge", label: "textDocument/references", outV: 29, inV: 101 }
{ id: 103, type: "edge", label: "item", outV: 101, inVs: [30], document: 4, property: referenceResults }
// Linking B#foo to I#foo
{ id: 114, type: "vertex", label: "referenceResult" }
{ id: 115, type: "edge", label: "textDocument/references", outV: 47, inV: 114 }
{ id: 116, type: "edge", label: "item", outV: 114, inVs: [30], document: 4, property: referenceResults }
Ein Ziel des Language Server Index Format ist es, dass die Informationen so schnell wie möglich ausgegeben werden können, ohne zu viele Informationen im Speicher zu cachen. Bei Sprachen, die das Überschreiben von Methoden, die in mehr als einer Schnittstelle definiert sind, unterstützen, kann dies komplizierter sein, da der gesamte Vererbungsbaum möglicherweise erst nach der Analyse aller Dokumente bekannt ist.
Betrachten Sie das folgende TypeScript-Beispiel
interface I {
foo(): void;
}
interface II {
foo(): void;
}
class B implements I, II {
foo(): void {
}
}
let i: I;
i.foo();
let b: B;
b.foo();
Die Suche nach I#foo() liefert 4 Referenzen, die Suche nach II#foo() liefert 3 Referenzen und die Suche nach B#foo() liefert 5 Ergebnisse. Der interessante Teil hier ist, wenn die Deklaration der class B verarbeitet wird, die I und II implementiert. Weder das Referenzergebnis, das an I#foo() gebunden ist, noch das an II#foo() gebundene kann wiederverwendet werden. Daher müssen wir ein neues erstellen. Um immer noch von den für I#foo und II#foo generierten Ergebnissen zu profitieren, unterstützt LSIF verschachtelte Referenzergebnisse. So wird das von B#foo referenzierte Ergebnis das von I#foo und II#foo wiederverwenden. Abhängig davon, wie diese Deklarationen analysiert werden, können die beiden Referenzergebnisse dieselben Referenzen enthalten. Wenn ein Language Server Referenzergebnisse, die aus anderen Referenzergebnissen bestehen, interpretiert, ist der Server für die Deduplizierung der endgültigen Bereiche verantwortlich.
Im obigen Beispiel wird es drei Referenzergebnisse geben
// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// Declaration of I#foo
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 16, type: "vertex", label: "range", start: { line: 1, character: 2 }, end: { line: 1, character: 5 } }
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
// Declaration of II#foo
{ id: 27, type: "vertex", label: "resultSet" }
{ id: 30, type: "vertex", label: "range", start: { line: 5, character: 2 }, end: { line: 5, character: 5 } }
{ id: 31, type: "edge", label: "next", outV: 30, inV: 27 }
// Declaration of B#foo
{ id: 45, type: "vertex", label: "resultSet" }
{ id: 52, type: "vertex", label: "range", start: { line: 9, character: 2 }, end: { line: 9, character: 5 } }
{ id: 53, type: "edge", label: "next", outV: 52, inV: 45 }
// Reference result for I#foo
{ id: 46, type: "vertex", label: "referenceResult" }
{ id: 47, type: "edge", label: "textDocument/references", outV: 13, inV: 46 }
// Reference result for II#foo
{ id: 48, type: "vertex", label: "referenceResult" }
{ id: 49, type: "edge", label: "textDocument/references", outV: 27, inV: 48 }
// Reference result for B#foo
{ id: 116 "typ" :"vertex", label: "referenceResult" }
{ id: 117 "typ" :"edge", label: "textDocument/references", outV: 45, inV: 116 }
// Link B#foo reference result to I#foo and II#foo
{ id: 118 "typ" :"edge", label: "item", outV: 116, inVs: [46,48], document: 4, property: "referenceResults" }
Für TypeScript werden Methodenreferenzen bei ihrer abstraktesten Deklaration aufgezeichnet, und wenn Methoden zusammengeführt werden (B#foo), werden sie über ein Referenzergebnis zusammengeführt, das auf andere Ergebnisse verweist.
Anfrage: textDocument/implementation
Die Unterstützung einer textDocument/implementation-Anfrage erfolgt durch Wiederverwendung dessen, was wir für eine textDocument/references-Anfrage implementiert haben. In den meisten Fällen gibt die textDocument/implementation die Deklarationswerte des Referenzergebnisses zurück, auf das eine Symboldeklaration verweist. Für Fälle, in denen das Ergebnis abweicht, bietet LSIF ein ImplementationResult. Um Implementierungsergebnisse zu verschachteln, unterstützt die item-Kante einen property-Wert von "implementationResults".
Das entsprechende ImplementationResult sieht wie folgt aus
interface ImplementationResult {
label: `implementationResult`
}
Anfrage: textDocument/typeDefinition
Die Unterstützung von textDocument/typeDefinition ist unkompliziert. Die Kante wird entweder am Bereich oder an der ResultSet aufgezeichnet.
Das entsprechende TypeDefinitionResult sieht wie folgt aus
interface TypeDefinitionResult {
label: `typeDefinitionResult`
}
Für das folgende TypeScript-Beispiel
interface I {
foo(): void;
}
let i: I;
Die relevanten ausgegebenen Knoten und Kanten sehen wie folgt aus
// The document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// The declaration of I
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range", start: { line: 0, character: 10 }, end: { line: 0, character: 11 } }
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The declaration of i
{ id: 26, type: "vertex", label: "resultSet" }
// The type definition result
{ id: 37, type: "vertex", label: "typeDefinitionResult" }
// Hook the result to the declaration
{ id: 38, type: "edge", label: "textDocument/typeDefinition", outV: 26, inV:37 }
// Add the declaration of I as a target range.
{ id: 51, type: "edge", label: "item", outV: 37, inVs: [9], document: 4 }
Wie bei anderen Ergebnissen werden Bereiche über eine item-Kante hinzugefügt. In diesem Fall ohne property, da es nur eine Art von Bereich gibt.
Dokument-Anfragen
Das Language Server Protocol unterstützt auch Anfragen nur für Dokumente (ohne Positionsinformationen). Diese Anfragen sind textDocument/foldingRange, textDocument/documentLink und textDocument/documentSymbol. Wir folgen dem gleichen Muster wie zuvor, um diese zu modellieren, der Unterschied besteht darin, dass das Ergebnis an das Dokument und nicht an einen Bereich gebunden ist.
Anfrage: textDocument/foldingRange
Für das Ergebnis der Faltungsbereiche sieht dies wie folgt aus
function hello() {
console.log('Hello');
}
function world() {
console.log('world');
}
function space() {
console.log(' ');
}
hello();space();world();
{ id: 2, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 112, type: "vertex", label: "foldingRangeResult", result:[
{ startLine: 0, startCharacter: 16, endLine: 2, endCharacter: 1 },
{ startLine: 4, startCharacter: 16, endLine: 6, endCharacter: 1 },
{ startLine: 8, startCharacter: 16, endLine: 10, endCharacter: 1 }
]}
{ id: 113, type: "edge", label: "textDocument/foldingRange", outV: 2, inV: 112 }
Das entsprechende FoldingRangeResult ist wie folgt definiert
export interface FoldingRangeResult {
label: 'foldingRangeResult';
result: lsp.FoldingRange[];
}
Anfrage: textDocument/documentLink
Auch für Dokumentenlinks definieren wir einen Ergebnistyp und eine entsprechende Kante, um ihn mit einem Dokument zu verknüpfen. Da die Link-Positionen normalerweise in Kommentaren vorkommen, bezeichnen die Bereiche keine Symboldeklarationen oder -referenzen. Wir betten daher den Bereich in das Ergebnis ein, wie wir es bei Faltungsbereichen tun.
export interface DocumentLinkResult {
label: 'documentLinkResult';
result: lsp.DocumentLink[];
}
Anfrage: textDocument/documentSymbol
Als Nächstes betrachten wir die Anfrage textDocument/documentSymbol. Diese Anfrage gibt normalerweise eine Gliederungsansicht des Dokuments in hierarchischer Form zurück. Allerdings sind nicht alle in einem Dokument deklarierten oder definierten Programmsymbole Teil des Ergebnisses (z. B. werden Locals normalerweise weggelassen). Zusätzlich muss ein Gliederungselement zusätzliche Informationen wie den vollständigen Bereich und eine Symbolart bereitstellen. Es gibt zwei Möglichkeiten, dies zu modellieren: Entweder tun wir dasselbe wie für Faltungsbereiche und Dokumentenlinks und speichern die Informationen in einem `documentSymbolResult` als Literale, oder wir erweitern den `range`-Knoten um zusätzliche Informationen und verweisen in der `documentSymbolResult` auf diese Bereiche. Da die zusätzlichen Informationen für Bereiche auch in anderen Szenarien hilfreich sein können, unterstützen wir das Hinzufügen zusätzlicher Tags zu diesen Bereichen, indem wir eine tag-Eigenschaft auf dem range-Knoten definieren.
Die folgenden Tags werden derzeit unterstützt
/**
* The range represents a declaration
*/
export interface DeclarationTag {
/**
* A type identifier for the declaration tag.
*/
type: 'declaration';
/**
* The text covered by the range
*/
text: string;
/**
* The kind of the declaration.
*/
kind: lsp.SymbolKind;
/**
* The full range of the declaration not including leading/trailing whitespace but everything else, e.g comments and code.
* The range must be included in fullRange.
*/
fullRange: lsp.Range;
/**
* Optional detail information for the declaration.
*/
detail?: string;
}
/**
* The range represents a definition
*/
export interface DefinitionTag {
/**
* A type identifier for the declaration tag.
*/
type: 'definition';
/**
* The text covered by the range
*/
text: string;
/**
* The symbol kind.
*/
kind: lsp.SymbolKind;
/**
* The full range of the definition not including leading/trailing whitespace but everything else, e.g comments and code.
* The range must be included in fullRange.
*/
fullRange: lsp.Range;
/**
* Optional detail information for the definition.
*/
detail?: string;
}
/**
* The range represents a reference
*/
export interface ReferenceTag {
/**
* A type identifier for the reference tag.
*/
type: 'reference';
/**
* The text covered by the range
*/
text: string;
}
/**
* The type of the range is unknown.
*/
export interface UnknownTag {
/**
* A type identifier for the unknown tag.
*/
type: 'unknown';
/**
* The text covered by the range
*/
text: string;
}
Ausgabe der Tags für das folgende TypeScript-Beispiel
function hello() {
}
hello();
Sieht so aus
{ id: 2, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 4, type: "vertex", label: "resultSet" }
{ id: 7, type: "vertex", label: "range",
start: { line: 0, character: 9 }, end: { line: 0, character: 14 },
tag: { type: "definition", text: "hello", kind: 12, fullRange: { start: { line: 0, character: 0 }, end: { line: 1, character: 1 }}}
}
Das `documentSymbolResult` wird dann wie folgt modelliert
export interface RangeBasedDocumentSymbol {
id: RangeId
children?: RangeBasedDocumentSymbol[];
}
export interface DocumentSymbolResult extends V {
label: 'documentSymbolResult';
result: lsp.DocumentSymbol[] | RangeBasedDocumentSymbol[];
}
Das gegebene TypeScript-Beispiel
namespace Main {
function hello() {
}
function world() {
let i: number = 10;
}
}
Erzeugt die folgende Ausgabe
// The document
{ id: 2 , type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
// The declaration of Main
{ id: 7 , type: "vertex", label: "range", start: { line: 0, character: 10 }, end: { line: 0, character: 14 }, tag: { type: "definition", text: "Main", kind: 7, fullRange: { start: { line: 0, character: 0 }, end: { line: 5, character: 1 } } } }
// The declaration of hello
{ id: 18 , type: "vertex", label: "range", start: { line: 1, character: 11 }, end: { line: 1, character: 16 }, tag: { type: "definition", text: "hello", kind: 12, fullRange: { start: { line: 1, character: 2 }, end: { line: 2, character: 3 } } } }
// The declaration of world
{ id: 29 , type: "vertex", label: "range", start: { line: 3, character: 11 }, end: { line: 3, character: 16 }, tag: { type: "definition", text: "world", kind: 12, fullRange: { start: { line: 3, character: 2 }, end: { line: 4, character: 3 } } } }
// The document symbol
{ id: 39 , type: "vertex", label: "documentSymbolResult", result: [ { id: 7 , children: [ { id: 18 }, { id: 29 } ] } ] }
{ id: 40 , type: "edge", label: "textDocument/documentSymbol", outV: 2, inV: 39 }
Anfrage: textDocument/diagnostic
Die einzigen fehlenden Informationen, die in einem Dump nützlich sind, sind die dem Dokument zugeordneten Diagnosen. Diagnosen in LSP werden als Push-Benachrichtigungen modelliert, die vom Server an den Client gesendet werden. Dies funktioniert nicht gut mit einem Dump, der auf Anfragemethodennamen basiert. Die Push-Benachrichtigung kann jedoch als Anfrage emuliert werden, bei der das Ergebnis der Anfrage der Wert ist, der während des Pushs als Parameter gesendet wird.
Im Dump modellieren wir Diagnosen wie folgt
- Wir führen eine Pseudobeschränkung
textDocument/diagnosticein. - Wir führen ein `diagnosticResult` ein, das die dem Dokument zugeordneten Diagnosen enthält.
Das Ergebnis sieht so aus
export interface DiagnosticResult {
label: 'diagnosticResult';
result: lsp.Diagnostic[];
}
Das gegebene TypeScript-Beispiel
function foo() {
let x: string = 10;
}
Erzeugt die folgende Ausgabe
{ id: 2, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 18, type: "vertex", label: "diagnosticResult", result: [{ severity: 1, code: 2322, message: "Type '10' is not assignable to type 'string'.", range: { start : { line: 1, character: 5 }, end: { line: 1, character: 6 } } } ] }
{ id: 19, type: "edge", label: "textDocument/diagnostic", outV: 2, inV: 18 }
Da Diagnosen in Dumps nicht sehr häufig vorkommen, wurden keine Anstrengungen unternommen, Bereiche in Diagnosen wiederzuverwenden.
Der `Project`-Knoten
Normalerweise arbeiten Language Server in einer Art Projektkontext. In TypeScript wird ein Projekt mithilfe einer tsconfig.json-Datei definiert. C# und C++ haben ihre eigenen Mittel. Die Projektdatei enthält normalerweise Informationen über Kompilierungsoptionen und andere Parameter. Diese im Dump zu haben, kann wertvoll sein. LSIF definiert daher einen `project`-Knoten. Zusätzlich werden alle Dokumente, die zu diesem Projekt gehören, über eine contains-Kante mit dem Projekt verbunden. Wenn in den vorherigen Beispielen eine tsconfig.json vorhanden wäre, würden die ersten ausgegebenen Kanten und Knoten wie folgt aussehen
{ id: 1, type: "vertex", label: "project", resource: "file:///Users/dirkb/tsconfig.json", kind: "typescript"}
{ id: 2, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript" }
{ id: 3, type: "edge", label: "contains", outV: 1, inVs: [2] }
Die Definition des project-Knotens sieht wie folgt aus
export interface Project extends V {
/**
* The label property.
*/
label: VertexLabels.project;
/**
* The project kind like 'typescript' or 'csharp'. See also the language ids
* in the [specification](https://msdocs.de/language-server-protocol/specification)
*/
kind: string;
/**
* The resource URI of the project file.
*/
resource?: Uri;
/**
* Optional the content of the project file, `base64` encoded.
*/
contents?: string;
}
Eingebettete Inhalte
Es kann wertvoll sein, den Inhalt eines Dokument- oder Projektdateis auch in den Dump einzubetten. Zum Beispiel, wenn der Inhalt des Dokuments ein virtuelles Dokument ist, das aus Programmetadaten generiert wurde. Das Indexformat unterstützt daher eine optionale contents-Eigenschaft auf den Knoten document und project. Wenn verwendet, muss der Inhalt base64-kodiert sein.
Fortgeschrittene Konzepte
Ereignisse
Um die Verarbeitung eines LSIF-Dumps zu erleichtern, z. B. zum Import in eine Datenbank, gibt der Dump Start- und Endereignisse für Dokumente und Projekte aus. Nach der Ausgabe des Endereignisses eines Dokuments darf der Dump keine weiteren Daten mehr enthalten, die sich auf dieses Dokument beziehen. Zum Beispiel dürfen keine Bereiche dieses Dokuments mehr in item-Kanten referenziert werden. Ebenso wenig dürfen Ergebnis-Mengen oder andere Knoten, die mit den Bereichen dieses Dokuments verknüpft sind, referenziert werden. Das Dokument kann jedoch in einer contains-Kante referenziert werden, die das Dokument einem Projekt hinzufügt. Die Start-/Endereignisse für Dokumente sehen wie folgt aus
// The actual document
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript", contents: "..." }
// The begin event
{ id: 5, type: "vertex", label: "$event", kind: "begin", scope: "document" , data: 4 }
// The end event
{ id: 53, type: "vertex", label: "$event", kind: "end", scope: "document" , data: 4 }
Zwischen dem Dokumentknoten 4 und dem Dokument-Start-Ereignis 5 dürfen keine Informationen spezifisch für Dokument 4 ausgegeben werden. Bitte beachten Sie, dass zu einem bestimmten Zeitpunkt mehr als ein Dokument geöffnet sein kann, was bedeutet, dass n verschiedene Dokument-Start-Ereignisse ohne entsprechende Dokument-End-Ereignisse vorhanden sein können.
Die Ereignisse für Projekte sehen ähnlich aus
{ id: 2, type: "vertex", label: "project", kind: "typescript" }
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/sample.ts", languageId: "typescript", contents: "..." }
{ id: 5, type: "vertex", label: "$event", kind: "begin", scope: "document" , data: 4 }
{ id: 3, type: "vertex", label: "$event", kind: "begin", scope: "project", data: 2 }
{ id: 53, type: "vertex", label: "$event", kind: "end", scope: "document", data: 4 }
{ id: 54, type: "edge", label: "contains", outV: 2, inVs: [4] }
{ id: 55, type: "vertex", label: "$event", kind: "end", scope: "project", data: 2 }
Projekt-Exporte und externe Importe (Moniker)
Ein Anwendungsfall von LSIF ist die Erstellung von Dumps für veröffentlichte Versionen eines Produkts, sei es eine Bibliothek oder ein Programm. Wenn ein Projekt **A** eine Bibliothek **B** referenziert, wäre es auch nützlich, wenn die Informationen in diesen beiden Dumps in Beziehung gesetzt werden könnten. Um dies zu ermöglichen, führt LSIF optionale Moniker ein, die über eine entsprechende Kante mit Bereichen verknüpft werden können. Die Moniker können verwendet werden, um zu beschreiben, was ein Projekt exportiert und was es importiert. Schauen wir uns zuerst den Exportfall an.
Betrachten Sie die folgende TypeScript-Datei namens index.ts
export function func(): void {
}
export class Emitter {
private doEmit() {
}
public emit() {
this.doEmit();
}
}
{ id: 4, type: "vertex", label: "document", uri: "file:///Users/dirkb/index.ts", languageId: "typescript", contents: "..." }
{ id: 11, type: "vertex", label: "resultSet" }
{ id: 12, type: "vertex", label: "moniker", kind: "export", scheme: "tsc", identifier: "lib/index:func" }
{ id: 13, type: "edge", label: "moniker", outV: 11, inV: 12 }
{ id: 14, type: "vertex", label: "range", start: { line: 0, character: 16 }, end: { line: 0, character: 20 } }
{ id: 15, type: "edge", label: "next", outV: 14, inV: 11 }
{ id: 18, type: "vertex", label: "resultSet" }
{ id: 19, type: "vertex", label: "moniker", kind: "export", scheme: "tsc", identifier: "lib/index:Emitter" }
{ id: 20, type: "edge", label: "moniker", outV: 18, inV: 19 }
{ id: 21, type: "vertex", label: "range", start: { line: 3, character: 13 }, end: { line: 3, character: 20 } }
{ id: 22, type: "edge", label: "next", outV: 21, inV: 18 }
{ id: 25, type: "vertex", label: "resultSet" }
{ id: 26, type: "vertex", label: "moniker", kind: "export", scheme: "tsc", identifier: "lib/index:Emitter.doEmit" }
{ id: 27, type: "edge", label: "moniker", outV: 25, inV: 26 }
{ id: 28, type: "vertex", label: "range", start: { line: 4, character: 10 }, end: { line: 4, character: 16 } }
{ id: 29, type: "edge", label: "next", outV: 28, inV: 25 }
{ id: 32, type: "vertex", label: "resultSet" }
{ id: 33, type: "vertex", label: "moniker", kind: "export", scheme: "tsc", identifier: "lib/index:Emitter.emit" }
{ id: 34, type: "edge", label: "moniker", outV: 32, inV: 33 }
{ id: 35, type: "vertex", label: "range", start: { line: 7, character: 9 }, end: { line: 7, character: 13 } }
{ id: 36, type: "edge", label: "next", outV: 35, inV: 32 }
Dies beschreibt die exportierte Deklaration innerhalb von index.ts mit einem Moniker (z. B. ein Griff im Zeichenkettenformat), der an die entsprechende Bereichsdeklaration gebunden ist. Der generierte Moniker muss positionsunabhängig und stabil sein, damit er zur Identifizierung des Symbols in anderen Projekten oder Dokumenten verwendet werden kann. Er sollte ausreichend eindeutig sein, um das Abgleichen mit anderen Monikern in anderen Projekten zu vermeiden, es sei denn, sie beziehen sich tatsächlich auf dasselbe Symbol. Ein Moniker hat daher zwei Eigenschaften: ein scheme, um anzugeben, wie die identifiers zu interpretieren sind. Und die identifier, um das Symbol tatsächlich zu identifizieren. Seine Struktur ist für den Scheme-Besitzer opak. Im obigen Beispiel werden die Moniker vom TypeScript-Compiler tsc erstellt und können nur mit Monikern verglichen werden, die ebenfalls das Scheme tsc haben.
Bitte beachten Sie auch, dass die Methode Emitter#doEmit einen Moniker hat, obwohl die Methode privat ist. Ob private Elemente Moniker haben, hängt von der Programmiersprache ab. Da TypeScript keine Sichtbarkeit erzwingen kann (es kompiliert zu JS, das dieses Konzept nicht hat), behandeln wir sie als sichtbar. Selbst der TypeScript-Language-Server tut dies. Finden Sie alle Referenzen, die alle Referenzen auf private Methoden findet, auch wenn sie als Sichtbarkeitsverletzung gekennzeichnet ist.
Wie diese exportierten Elemente in anderen Projekten sichtbar sind, hängt in den meisten Programmiersprachen davon ab, wie viele Dateien in eine Bibliothek oder ein Programm gepackt werden. In TypeScript ist der Standardpaketmanager npm.
Betrachten Sie die folgende package.json-Datei
{
"name": "lsif-ts-sample",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"author": "MS",
"license": "MIT",
}
dann können diese Moniker in Moniker übersetzt werden, die npm-abhängig sind. Anstatt die Moniker zu ersetzen, geben wir einen zweiten Satz von Monikern aus und verknüpfen die tsc-Moniker mit den entsprechenden npm-Monikern über eine nextMoniker-Kante.
{ id: 991, type: "vertex", label: "packageInformation", name: "lsif-ts-sample", manager: "npm", version: "1.0.0" }
{ id: 987, type: "vertex", label: "moniker", kind: "export", scheme: "npm", identifier: "lsif-ts-sample::func" }
{ id: 986, type: "edge", label: "packageInformation", outV: 987, inV: 991 }
{ id: 985, type: "edge", label: "nextMoniker", outV: 12, inV: 987 }
{ id: 984, type: "vertex", label: "moniker", kind: "export", scheme: "npm", identifier: "lsif-ts-sample::Emitter" }
{ id: 983, type: "edge", label: "packageInformation", outV: 984, inV: 991 }
{ id: 982, type: "edge", label: "nextMoniker", outV: 19, inV: 984 }
{ id: 981, type: "vertex", label: "moniker", kind: "export", scheme: "npm", identifier: "lsif-ts-sample::Emitter.doEmit" }
{ id: 980, type: "edge", label: "packageInformation", outV: 981, inV: 991 }
{ id: 979, type: "edge", label: "nextMoniker", outV: 26, inV: 981 }
{id: 978, type: "vertex", label: "moniker", kind: "export", scheme: "npm", identifier: "lsif-ts-sample::Emitter.emit" }
{id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{id: 976, type: "edge", label: "nextMoniker", outV: 33, inV: 978 }
Zu beobachtende Dinge
- ein spezieller
packageInformation-Knoten wurde ausgegeben, um auf die entsprechenden npm-Paketinformationen zu verweisen. - der npm-Moniker verweist auf den Paketnamen.
- da die Datei
index.tsdie npm-Hauptdatei ist, hat der Moniker-Identifier keinen Dateipfad. Dies ist vergleichbar mit dem Importieren dieses Moduls in TypeScript oder JavaScript, wobei nur der Modulname und kein Dateipfad verwendet wird (z. B.import * as lsif from 'lsif-ts-sample'). - die
nextMoniker-Kante zeigt vom `tsc`-Moniker-Knoten zum `npm`-Moniker-Knoten.
Für LSIF empfehlen wir die Verwendung eines zweiten Werkzeugs, um die vom Indexer ausgegebenen Moniker paketmanagerabhängig zu machen. Dies unterstützt die Verwendung verschiedener Paketmanager und ermöglicht die Einbindung benutzerdefinierter Build-Tools. In der TypeScript-Implementierung geschieht dies durch ein npm-spezifisches Werkzeug, das die Moniker unter Berücksichtigung der npm-Paketinformationen umschreibt.
Das Melden des Imports von externen Symbolen erfolgt auf die gleiche Weise. LSIF gibt Moniker vom Typ import aus. Betrachten Sie das folgende TypeScript-Beispiel
import * as mobx from 'mobx';
let map: mobx.ObservableMap = new mobx.ObservableMap();
wobei mobx das npm mobx Paket ist. Das Ausführen des tsc-Indexwerkzeugs erzeugt
{ id: 41, type: "vertex", label: "document", uri: "file:///Users/dirkb/samples/node_modules/mobx/lib/types/observablemap.d.ts", languageId: "typescript", contents: "..." }
{ id: 55, type: "vertex", label: "resultSet" }
{ id: 57, type: "vertex", label: "moniker", kind: "import", scheme: "tsc", identifier: "node_modules/mobx/lib/mobx:ObservableMap" }
{ id: 58, type: "edge", label: "moniker", outV: 55, inV: 57 }
{ id: 59, type: "vertex", label: "range", start: { line: 17, character: 538 }, end: { line: 17, character: 551 } }
{ id: 60, type: "edge", label: "next", outV: 59, inV: 55 }
Drei Dinge sind hier zu beachten: Erstens verwendet TypeScript Deklarationsdateien für extern importierte Symbole. Dies hat den Vorteil, dass die Monikerinformationen an die Deklarationsbereiche in diesen Dateien angehängt werden können. In anderen Sprachen können die Informationen an die Datei angehängt werden, die das Symbol tatsächlich referenziert. Oder es wird ein virtuelles Dokument für das referenzierte Element generiert. Zweitens erzeugt das Werkzeug diese Informationen nur für tatsächlich referenzierte Symbole, nicht für alle verfügbaren Symbole. Drittens sind diese Moniker tsc-spezifisch und zeigen auf den node_modules-Ordner.
Das Durchleiten dieser Informationen durch das npm-Werkzeug erzeugt jedoch Folgendes
{id: 991, type: "vertex", label: "packageInformation", name: "mobx", manager: "npm", version: "5.6.0", repository: { type: "git", url: "git+https://github.com/mobxjs/mobx.git" } }
{ id: 978, type: "vertex", label: "moniker", kind: "import", scheme: "npm", identifier: "mobx::ObservableMap" }
{ id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{ id: 976, type: "edge", label: "nextMoniker", outV: 978, inV: 57 }
was den Moniker spezifisch für das npm mobx-Paket macht. Zusätzlich wurden Informationen über das mobx-Paket selbst ausgegeben. Bitte beachten Sie, dass dies ein Import-Moniker ist, die nextMoniker-Kante zeigt vom npm-Moniker zum tsc-Moniker.
Normalerweise werden Moniker an Ergebnis-Mengen angehängt, da sie für alle Bereiche, die auf die Ergebnis-Menge verweisen, gleich sind. Für Dumps, die keine Ergebnis-Mengen verwenden, können Moniker jedoch auch auf Bereiche angewendet werden.
Für Werkzeuge, die den Dump verarbeiten und in eine Datenbank importieren, ist es manchmal nützlich zu wissen, ob ein Ergebnis lokal zu einer Datei ist oder nicht (z. B. können Funktionsargumente nur innerhalb der Datei navigiert werden). Um Postprocessing-Werkzeuge bei dieser Entscheidung zu unterstützen, sollten LSIF-Generierungswerkzeuge auch für lokale Elemente ein Moniker generieren. Die entsprechende zu verwendende Art ist local. Der Bezeichner sollte innerhalb des Dokuments immer noch eindeutig sein.
Für das folgende Beispiel
function foo(x: number): void {
}
Das Moniker für x sieht wie folgt aus
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 14, type: "vertex", label: "moniker", kind: "local", scheme: "tsc", identifier: "SfeOP6s53Y2HAkcViolxYA==" }
{ id: 15, type: "edge", label: "moniker", outV: 13, inV: 14 }
{ id: 16, type: "vertex", label: "range", start: { line: 0, character: 13 }, end: { line: 0, character: 14 }, tag: { type: "definition", text: "x", kind: 7, fullRange: { start: { line: 0, character: 13 }, end: { line: 0, character: 22 } } } }
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
Zusätzlich zu diesen Moniker-Schemata, die mit $ beginnen, sind reserviert und sollten von einem LSIF-Tool nicht verwendet werden.
Ergebnisbereiche
Bereiche in LSIF haben derzeit zwei Bedeutungen
- sie fungieren als LSP-Anforderungssensitive Bereiche in einem Dokument (z. B. verwenden wir sie, um zu entscheiden, ob für eine gegebene Position ein entsprechendes LSP-Anforderungsergebnis existiert)
- sie fungieren als Navigationsziele (z. B. sind sie das Ergebnis einer Go-To-Deklarationsnavigation).
Um die erste Bedeutung zu erfüllen, spezifiziert LSIF, dass Bereiche sich nicht überlappen oder gleich sein dürfen. Diese Einschränkung ist jedoch für die zweite Bedeutung nicht notwendig. Um gleich oder überlappende Zielbereiche zu unterstützen, führen wir einen Knoten resultRange ein. Es ist nicht erlaubt, ein resultRange als Ziel in einer contains-Kante zu verwenden.
Metadaten-Knoten
Zur Unterstützung der Versionierung definiert LSIF einen Metadaten-Knoten wie folgt
export interface MetaData {
/**
* The label property.
*/
label: 'metaData';
/**
* The version of the LSIF format using semver notation. See https://semver.org/. Please note
* the version numbers starting with 0 don't adhere to semver and adopters have to assume
* the each new version is breaking.
*/
version: string;
/**
* The project root (in form of an URI) used to compute this dump.
*/
projectRoot: Uri;
/**
* The string encoding used to compute line and character values in
* positions and ranges. Currently only 'utf-16' is support due to the
* limitations in LSP.
*/
positionEncoding: 'utf-16',
/**
* Information about the tool that created the dump
*/
toolInfo?: {
name: string;
version?: string;
args?: string[];
}
}
Generierungsbeschränkungen
Die folgenden Generierungsbeschränkungen (von denen einige bereits im Dokument erwähnt wurden) existieren
- Ein Knoten muss generiert werden, bevor er in einer Kante referenziert werden kann.
- Ein
rangeundresultRangekönnen nur in einem Dokument enthalten sein. - Ein
resultRangekann nicht als Ziel in einercontains-Kante verwendet werden. - Nachdem ein Dokumentenende-Ereignis generiert wurde, können nur Ergebnissets, Referenz- oder Implementierungsergebnisse, die über dieses Dokument generiert wurden, in Kanten referenziert werden. Es ist beispielsweise nicht erlaubt, Bereiche oder Ergebnisbereiche aus diesem Dokument zu referenzieren. Dies schließt auch das Hinzufügen von Monikern zu Bereichen oder Ergebnissets ein. Die Dokumentendaten können sozusagen nicht mehr geändert werden.
- Wenn Bereiche auf Ergebnissets verweisen und Moniker generiert werden, müssen sie auf dem Ergebnisset generiert werden und können nicht auf einzelnen Bereichen generiert werden.
Zusätzliche Informationen
Tools
lsif-protocol: Protokoll definiert als TypeScript-Interfaceslsif-util: Hilfsprogramme für die LSIF-Entwicklunglsif-tsc: LSIF-Indexer für TypeScriptlsif-npm: Linker für NPM-Moniker
Offene Fragen (Open Questions)
Bei der Implementierung für TypeScript und npm haben wir eine Liste von offenen Fragen in Form von GitHub-Issues gesammelt, die uns bereits bekannt sind.