Language Server Index Format Spezifikation - 0.5.0
Die Version 0.5.0 von LSIF ist derzeit in Arbeit.
Language Server Index Format
Der Zweck des Language Server Index Format (LSIF) ist es, ein Standardformat für Sprachserver oder andere Programmierwerkzeuge zu definieren, um ihr Wissen über einen Arbeitsbereich zu speichern. Diese Speicherung kann später verwendet werden, um Anfragen des Sprachservers LSP für denselben Arbeitsbereich zu beantworten, ohne den Sprachserver selbst ausführen zu müssen. Da viele Informationen durch eine Änderung des Arbeitsbereichs ungültig würden, schließt die gespeicherte Information typischerweise keine Anfragen aus, die zum Ändern eines Dokuments verwendet werden. So ist beispielsweise das Ergebnis einer Code-Vervollständigungsanfrage typischerweise nicht Teil einer solchen Speicherung.
Changelog
Version 0.5.0
In Version 0.4.0 wurde die Unterstützung hinzugefügt, um größere Systeme Projekt für Projekt (in ihrer umgekehrten Abhängigkeitsreihenfolge) zu speichern und dann die Speichervorgänge in einer Datenbank wieder zusammenzuführen, indem Ergebnismengen über ihre entsprechenden Moniker verknüpft werden. Die Verwendung des Formats hat gezeigt, dass einige Funktionen fehlen, um dies reibungslos zu gestalten
- Unterstützung zur logischen Gruppierung von Projekten. Zur Unterstützung wurde ein
Group-Vertex hinzugefügt. - Wissen, wie eindeutig ein Moniker ist. Zur Unterstützung wurde eine Eigenschaft
uniquezumMonikerhinzugefügt. - der
nextMoniker-Edge wurde durch eine allgemeinereattach-Edge ersetzt. Dies war möglich, da Moniker nun eine Eigenschaftuniquetragen, die zuvor in der Richtung dernextMoniker-Edge kodiert war. - In Programmiersprachen, die Polymorphie unterstützen, können Aufrufe zur Laufzeit an einen anderen Typ gebunden werden als statisch bekannt ist. Ein Beispiel sind überschriebene Methoden in objektorientierten Programmiersprachen. Da Speichervorgänge pro Projekt erstellt werden können, müssen wir den Speichervorgängen zusätzliche Informationen hinzufügen, damit diese polymorphen Bindungen erfasst werden können. Das allgemeine Konzept der Referenzverknüpfungen wurde daher eingeführt (siehe Abschnitt Mehrere Projekte). Kurz gesagt, es ermöglicht einem Werkzeug, eine
item-Edge mit den EigenschaftswertenreferenceLinkszu annotieren. - Um die Ausgabe besser in Chunks zu zerlegen, trägt die
items-Edge eine zusätzliche Eigenschaftshard. Diese Eigenschaft wurde in einer früheren Version der 0.5-Spezifikationdocumentgenannt.
Eine alte 0.4.0-Version der Spezifikation ist hier verfügbar
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, eine Speicherung zu verarbeiten und sie beispielsweise in eine Datenbank zu importieren, ohne die Speicherung 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 result set
{ id: 6, type: "vertex", label: "resultSet" }
// The bar declaration
{ 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], shard: 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], shard: 4 }
Die item-Edge hat eine zusätzliche Eigenschaft shard, die den Vertex angibt, der die Quelle (z. B. ein Dokument oder ein Projekt) dieser Deklarationen ist. Wir haben diese Information hinzugefügt, um die Ausgabe von Daten immer noch einfach zu gestalten, aber auch, um die Daten beim Speichern in einer Datenbank einfach zu verarbeiten und zu zerlegen. Ohne diese Information müssten wir entweder eine Reihenfolge angeben, in der die Daten ausgegeben werden müssen (z. B. eine item-Edge und nur auf einen Bereich verweisen, der bereits über eine contains-Edge zu einem Dokument hinzugefügt wurde) oder wir zwingen die verarbeitenden Werkzeuge, viele Vertices und Edges im Speicher zu halten. Der Ansatz, diese shard-Eigenschaft zu haben, scheint ein fairer Ausgleich 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], shard: 4, property: "definitions"
}
// Add the bar reference as a reference to the reference result
{ id: 28, type: "edge", label: "item",
outV: 25, inVs: [20], shard: 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], shard: 4, property: "definitions"
}
{ id: 91, type: "edge", label: "item",
outV: 30, inVs: [65,78], shard: 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], shard: 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], shard: 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() findet 4 Referenzen, die Suche nach II#foo() findet 3 Referenzen und die Suche nach B#foo() findet 5 Ergebnisse. Das Interessante hier ist, wenn die Deklaration von class B verarbeitet wird, die I und II implementiert, weder das Referenzergebnis, das an I#foo() gebunden ist, noch das, das an II#foo() gebunden ist, wiederverwendet werden kann. Wir müssen also ein neues erstellen. Um immer noch von den Ergebnissen profitieren zu können, die für I#foo und II#foo generiert wurden, unterstützt LSIF verschachtelte Referenzergebnisse. Auf diese Weise wird das von B#foo referenzierte Ergebnis das von I#foo und II#foo wiederverwenden. Je nachdem, wie diese Deklarationen geparst werden, können die beiden Referenzergebnisse dieselben Referenzen enthalten. Wenn ein Sprachserver Referenzergebnisse interpretiert, die aus anderen Referenzergebnissen bestehen, 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], shard: 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 für reine 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 mit dem Dokument statt mit einem Bereich verknüpft 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 lokale Variablen normalerweise weggelassen). Darüber hinaus muss ein Gliederungselement zusätzliche Informationen wie den vollständigen Bereich und eine Symbolart bereitstellen. Es gibt zwei Möglichkeiten, dies zu modellieren: Entweder wir tun dasselbe wie bei Folding-Ranges und Dokumentlinks und speichern die Informationen als Literale in einem Document-Symbol-Ergebnis, oder wir erweitern den Range-Vertex um zusätzliche Informationen und verweisen auf diese Ranges im Document-Symbol-Ergebnis. Da die zusätzlichen Informationen für Ranges auch in anderen Szenarien hilfreich sein könnten, unterstützen wir das Hinzufügen zusätzlicher Tags zu diesen Ranges, indem wir eine tag-Eigenschaft auf dem range-Vertex 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.
* See 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. für den Import in eine Datenbank, gibt der Dump Start- und End-Ereignisse für Dokumente und Projekte aus. Nach dem End-Ereignis eines Dokuments darf der Dump keine weiteren Daten enthalten, die sich auf dieses Dokument beziehen. Zum Beispiel dürfen keine Bereiche aus diesem Dokument in item-Edges referenziert werden. Ebenso dürfen keine Ergebnismengen oder andere Vertices, die mit den Bereichen in diesem Dokument verknüpft sind, referenziert werden. Das Dokument kann jedoch in einer contains-Edge referenziert werden, die das Dokument einem Projekt hinzufügt. Die Start-/End-Ereignisse 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)
Geändert in 0.5.0
Ein Anwendungsfall von LSIF ist die Erstellung von Speichervorgängen für veröffentlichte Versionen eines Produkts, sei es eine Bibliothek oder ein Programm. Wenn Projekt P2 eine Bibliothek P1 referenziert, wäre es auch nützlich, wenn die Informationen in diesen beiden Speichervorgängen 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 zunächst 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", unique: "group"
}
{ 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",
unique: "group"
}
{ 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",
unique: "group"
}
{ 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",
unique: "group"
}
{ 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 in index.ts mit einem Moniker (z. B. einem Handle im String-Format), 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 nicht mit anderen Monikern in anderen Projekten übereinzustimmen, es sei denn, sie beziehen sich tatsächlich auf dasselbe Symbol. Ein Moniker hat daher die folgenden Eigenschaften
scheme, um anzugeben, wie dieidentifierszu interpretieren sind.identifier, um das Symbol tatsächlich zu identifizieren. Seine Struktur ist für den Schema-Besitzer opak. Im obigen Beispiel werden die Moniker vom TypeScript-Compiler tsc erstellt und können nur mit Monikern verglichen werden, die ebenfalls das Schematschaben.kind, um anzugeben, ob der Moniker exportiert, importiert oder lokal zum Projekt ist.unique, um anzugeben, wie eindeutig der Moniker ist. Weitere Informationen hierzu finden Sie im Abschnitt über mehrere Projekte.
Bitte beachten Sie auch, dass die Methode Emitter#doEmit einen Export-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 kein solches Konzept hat), behandeln wir sie als sichtbar. Sogar der TypeScript-Sprachserver tut dies. Finden Sie alle Referenzen, findet alle Referenzen auf private Methoden, auch wenn sie als Sichtbarkeitsverletzung markiert sind.
Systeme mit mehreren Projekten
Neu in 0.5.0
Die meisten Softwaresysteme bestehen heute aus mehreren Projekten. Immer LSIF-Speichervorgänge für alle Projekte eines Systems zu erstellen, auch wenn nur ein Projekt geändert wird, ist nicht sehr praktikabel, insbesondere wenn sich nur interne Änderungen in einem Projekt geändert haben. LSIF erlaubt seit 0.4.0 daher, einen LSIF-Speichervorgang pro Projekt zu erstellen und sie in der DB wieder zu größeren Systemen zu verknüpfen. Allerdings fehlten 0.4.0 einige Konzepte, um dies zu realisieren. Um sie zu motivieren, betrachten Sie das folgende Beispiel
Projekt P1
Projekt P1 besteht aus einer p1Main.ts-Datei mit folgendem Inhalt
export interface Disposable {
dispose(): void;
}
let d: Disposable;
d.dispose();
Projekt P2
Projekt P2 ist von P1 abhängig und besteht aus einer p2Main.ts-Datei mit folgendem Inhalt
import { Disposable } from 'p1';
class Widget implements Disposable {
public dispose(): void {
}
}
let w: Widget;
w.dispose();
Wenn nun ein Benutzer nach Referenzen auf Widget#dispose sucht, wird erwartet, dass die Referenz d.dispose in P1 im Ergebnis enthalten ist. Wenn P1 verarbeitet wird, kennt das Werkzeug P2 jedoch nicht. Und wenn P2 verarbeitet wird, kennt es normalerweise die Quelle von P1 nicht. Es kennt nur seine API-Form (z. B. in TypeScript die entsprechende d.ts-Datei).
Damit dies funktioniert, müssen wir zunächst Projekte zu größeren Einheiten gruppieren, damit wir wissen, in welchen Projekten d.dispose tatsächlich eine Übereinstimmung ist. Angenommen, es gibt ein völlig unrelated Projekt PX, das ebenfalls Disposable von P1 verwendet, aber P2 wird nie mit PX in ein System integriert. Ein Objekt vom Typ Widget kann also niemals in Code in PX fließen, daher sollten Referenzen in PX nicht aufgelistet werden. Wir führen daher die Notation einer Gruppe ein, um Projekte logisch zu größeren Systemen zusammenzufassen. Projekte gehören zu einer Gruppe, und Gruppen werden mithilfe einer URI identifiziert. Sehen wir uns die konkreten Speichervorgänge für P1 und P2 an
{id: 2, type: "vertex", label: "group",
uri: "https://github.com/microsoft/lsif-node.git/samples/ts-cascade",
conflictResolution: "takeDB", name: "ts-cascade",
rootUri: "file:///Users/dirkb/samples/ts-cascade"
}
{id: 4, type: "vertex", label: "project", kind: "typescript", name: "p1" }
{id: 5, type: "edge", label: "belongsTo", outV: 4, inV:2 }
Als Gruppen-URI wird der Pfad in einem GitHub-Repository verwendet. Die URI könnte aber auch etwas wie lsif-group:://com.microsoft/vscode/lsif-node/samples/ts-cascade sein, wenn die URI repositoryunabhängig sein soll. Dies wäre nützlich, wenn ein Unternehmen Code in vielen verschiedenen Repository-Systemen speichert. Die Kante mit der ID 5 bindet das Projekt an die Gruppe.
Der Dump für Projekt P2 sieht wie folgt aus
{id: 2, type: "vertex", label: "group",
uri: "https://github.com/Microsoft/lsif-node.git/samples/ts-cascade",
conflictResolution: "takeDB", name: "ts-cascade",
rootUri: "file:///Users/dirkb/samples/ts-cascade"
}
{id: 4, type: "vertex", label: "project", kind: "typescript", name: "p2" }
{id: 5, type: "edge", label: "belongsTo", outV: 4, inV: 2 }
Beachten Sie, dass dies P2 an dieselbe Gruppe bindet, zu der P1 gehört. Um jegliche Art von Gruppenverwaltung zu vermeiden, trägt die Gruppe eine Eigenschaft conflictResolution, um einer DB mitzuteilen, welche Gruppeninformationen verwendet werden sollen, wenn die DB bereits eine Gruppe mit der angegebenen URL enthält. takeDB bedeutet, die bereits in der DB gespeicherte zu verwenden, und takeDump bedeutet, dass die aus dem Dump den DB-Wert überschreiben soll.
Wo immer möglich, sollten Gruppen-URIs hierarchisch organisiert werden, um die Gruppierung von Projekten in einen breiteren Geltungsbereich zu ermöglichen. Zum Beispiel sollte eine URI https://github.com/microsoft alle Projekte erfassen, die unter der GitHub-Organisation Microsoft organisiert sind.
Schauen wir uns nun an, wie wir sicherstellen, dass die Suche nach Referenzen auf Widget#dispose die Übereinstimmung d.dispose() in P1 findet. Betrachten wir zunächst, welche Informationen im Dump von P1 für Disposable#dispose enthalten sein werden
// The result set for the Disposable#dispose symbol
{ id: 21, type: "vertex", label: "resultSet" }
// The export moniker of Disposable#dispose in P1 (note kind export).
{ id: 22, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "p1/lib/p1Main:Disposable.dispose",
unique: "group", kind:"export"
}
{ id: 23, type: "edge", label: "moniker", outV: 21, inV: 22 }
// The actual definition of the symbol
{ id: 24, type: "vertex", label: "range",
start: { line: 1, character: 1 }, end: { line: 1, character: 8 },
tag: {
type: definition, text: "dispose", kind: 7,
fullRange: {
start : { line: 1, character:1 }, end: { line: 1, character: 17 }
}
}
}
// Bind the reference result to the result set
{ id: 57, type: "vertex", label: "referenceResult" }
{ id: 58, type: "edge", label: "textDocument/references", outV: 21, inV: 57 }
Interessant ist hier Zeile 22, die den Moniker für Disposable#dispose definiert. Sie hat eine neue Eigenschaft unique, die angibt, dass der Moniker innerhalb einer group von Projekten eindeutig ist, aber nicht unbedingt außerhalb. Andere mögliche Werte für unique sind
document, um anzuzeigen, dass der Moniker nur innerhalb eines Dokuments eindeutig ist. Wird z. B. für lokale Variablen oder private Member verwendet.project, um anzuzeigen, dass der Moniker nur innerhalb eines Projekts eindeutig ist. Wird z. B. für projekinterne Symbole verwendet.group, um anzuzeigen, dass der Moniker innerhalb einer Gruppe von Projekten eindeutig ist. Wird z. B. für exportierte Member verwendet.scheme, um anzuzeigen, dass der Moniker innerhalb des Schemas des Monikers eindeutig ist. Wenn der Moniker beispielsweise für einen bestimmten Paketmanager generiert wird (siehe npm-Beispiel unten), sind diese Moniker normalerweise innerhalb des Themas des Monikers eindeutig (z. B. tragen alle für npm generierten Moniker das Schemanpmund sind eindeutig)global, um anzuzeigen, dass der Moniker global eindeutig ist (z. B. ist sein Bezeichner unabhängig vom Schema oder Typ eindeutig)
Bei der Generierung des Dumps für P2 sehen die Informationen für Widget#dispose wie folgt aus
// The import moniker for importing Disposable#dispose into P2
{ id: 22, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "p1/lib/p1Main:Disposable.dispose",
unique: "group", kind: "import"
}
// The result set for Widget#dispose
{ id: 78, type: "vertex", label: "resultSet" }
// The moniker for Widget#dispose. Note that the moniker is local since the
// Widget class is not exported
{ id: 79, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "2Q46RTVRZTuVW1ajf68/Vw==",
unique: "document", kind: "local"
}
{ id: 80, type: "edge", label: "moniker", outV: 78, inV: 79 }
// The actual definition of the symbol
{ id: 81, type: "vertex", label: "range",
start: { line: 3, character: 8 }, end: { line: 3, character: 15 },
tag: {
type: "definition", text: "dispose", kind: 6,
fullRange: {
start: { line: 3, character: 1 }, end: { line: 4, character: 2 }
}
}
}
// Bind the reference result to Widget#dispose
{ id: 116, type: "vertex", label: "referenceResult" }
{ id: 117, type: "edge", label: "textDocument/references", outV: 78, inV: 116}
{ id: 118, type: "edge", label: "item",
outV: 116, inVs: [43], shard: 52, property: "referenceResults"
}
// Link the reference result set of Disposable#dispose to this result set
// using a moniker
{ id: 119, type: "edge", label: "item",
outV: 116, inVs: [22], shard: 52, property: "referenceLinks"
}
{ id: 120, type: "edge", label: "item",
outV: 43, inVs: [81], shard: 52, property: "definitions"
}
{ id: 121, type: "edge", label: "item",
outV: 43, inVs: [96], shard: 52, property: "references"
}
Die bemerkenswerten Teile sind
- der Vertex mit
id: 22: ist der Import-Moniker fürDisposable#disposeaus P1. - die Kante mit
id: 119: fügt einen Referenzlink zum Referenzergebnis vonWidget#disposehinzu. Item-Edges mitreferenceLinkssind konzeptionell wie Item-Edges mit einerreferenceResults-Eigenschaft. Sie ermöglichen zusammengesetzte Referenzergebnisse. Der Unterschied besteht darin, dass einereferenceResults-Item-Edge ein anderes Ergebnis unter Verwendung der Vertex-ID referenziert, da das Referenzergebnis Teil desselben Dumps ist. EinereferenceLinks-Item-Edge referenziert ein anderes Ergebnis mithilfe eines Monikers. Die eigentliche Auflösung muss also in einer Datenbank erfolgen, die die Daten für P1 und P2 enthält. Wie beireferenceResults-Item-Edges ist ein Sprachserver für die Deduplizierung der endgültigen Bereiche verantwortlich.
Paketmanager
Geändert in 0.5.0
Wie exportierte Elemente in anderen Projekten sichtbar sind, hängt in den meisten Programmiersprachen davon ab, wie Dateien in eine Bibliothek oder ein Programm verpackt 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",
}
für die folgende TypeScript-Datei (wie oben)
export function func(): void {
}
export class Emitter {
private doEmit() {
}
public emit() {
this.doEmit();
}
}
dann können diese Moniker in Moniker übersetzt werden, die von 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 attach-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",
unique: "scheme"
}
{ id: 986, type: "edge", label: "packageInformation", outV: 987, inV: 991 }
{ id: 985, type: "edge", label: "attach", outV: 987, inV: 12 }
{ id: 984, type: "vertex", label: "moniker",
kind: "export", scheme: "npm", identifier: "lsif-ts-sample::Emitter",
unique: "scheme"
}
{ id: 983, type: "edge", label: "packageInformation", outV: 984, inV: 991 }
{ id: 982, type: "edge", label: "attach", outV: 984, inV: 19 }
{ id: 981, type: "vertex", label: "moniker",
kind: "export", scheme: "npm",
identifier: "lsif-ts-sample::Emitter.doEmit", unique: "scheme"
}
{ id: 980, type: "edge", label: "packageInformation", outV: 981, inV: 991 }
{ id: 979, type: "edge", label: "attach", outV: 981, inV: 26 }
{ id: 978, type: "vertex", label: "moniker",
kind: "export", scheme: "npm",
identifier: "lsif-ts-sample::Emitter.emit", unique: "scheme"
}
{ id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{ id: 976, type: "edge", label: "attach", outV: 978, inV: 33 }
Zu beobachtende Dinge
- ein spezieller Vertex
packageInformationwurde ausgegeben, um auf die entsprechenden npm-Paketinformationen zu verweisen. - der npm-Moniker verweist auf den Paketnamen.
- sein
unique-Wert istscheme, was bedeutet, dass der Bezeichner des Monikers eindeutig über allenpm-Moniker ist. - 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
attach-Kante zeigt vom npm-Moniker-Vertex zum tsc-Moniker-Vertex.
Für LSIF empfehlen wir, dass ein zweites Werkzeug verwendet wird, 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 anhängt.
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:///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", unique: 'group'
}
{ 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",
unique: 'scheme'
}
{ id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{ id: 976, type: "edge", label: "attach", outV: 978, inV: 57 }
was den Moniker spezifisch für das npm-Paket mobx gemacht hat. Zusätzlich wurden Informationen über das mobx-Paket selbst ausgegeben.
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 nachbearbeitende Werkzeuge dabei zu unterstützen, dies effizient zu entscheiden, sollten LSIF-Generierungswerkzeuge auch für lokale Variablen einen Moniker generieren. Der entsprechende Typ, der verwendet werden soll, ist local. Der Bezeichner sollte innerhalb des Dokuments 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==",
unique: 'document'
}
{ 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
Geändert in 0.5.0
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 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 Dokument-End-Ereignis ausgegeben wurde, können nur Ergebnissets, Referenz- oder Implementierungsergebnisse, die über dieses Dokument ausgegeben 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 Dokumentdaten 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.