Skip to content

Commit 881c909

Browse files
committed
Make rudimentary jump-to-definition work
1 parent c292f58 commit 881c909

File tree

4 files changed

+248
-15
lines changed

4 files changed

+248
-15
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import * as tk from 'tree-kill';
1010
import * as util from 'util';
1111
import { CancellationToken, Disposable } from 'vscode';
1212
import { BQRSInfo, DecodedBqrsChunk } from "./bqrs-cli-types";
13-
import { DistributionProvider } from './distribution';
14-
import { assertNever } from './helpers-pure';
15-
import { QueryMetadata, SortDirection } from './interface-types';
16-
import { Logger, ProgressReporter } from './logging';
13+
import { DistributionProvider } from "./distribution";
14+
import { assertNever } from "./helpers-pure";
15+
import { QueryMetadata, SortDirection } from "./interface-types";
16+
import { Logger, ProgressReporter } from "./logging";
1717

1818
/**
1919
* The version of the SARIF format that we are using.

extensions/ql-vscode/src/databases.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,11 @@ export class DatabaseManager extends DisposableObject {
611611
return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);
612612
}
613613

614+
public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {
615+
const uriString = uri.toString(true);
616+
return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);
617+
}
618+
614619
private async addDatabaseItem(item: DatabaseItemImpl) {
615620
this._databaseItems.push(item);
616621
this.updatePersistedDatabaseList();
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import * as vscode from "vscode"
2+
import * as messages from "./messages"
3+
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from "./run-queries";
4+
import { QueryServerClient } from "./queryserver-client";
5+
import { DatabaseManager, DatabaseItem } from "./databases";
6+
import { CodeQLCliServer } from "./cli";
7+
import { getResultSetSchema, UrlValue, LineColumnLocation, EntityValue } from "./bqrs-cli-types";
8+
import { decodeSourceArchiveUri, zipArchiveScheme } from "./archive-filesystem-provider";
9+
10+
const TEMPLATE_NAME = "selectedSourceFile";
11+
const SELECT_QUERY_NAME = "#select";
12+
13+
enum KeyType {
14+
DefinitionQuery, ReferenceQuery
15+
}
16+
17+
async function resolveQueries(keyType: KeyType): Promise<string[]> {
18+
switch (keyType) {
19+
case KeyType.DefinitionQuery: return ["/home/jcreed/semmle/code/ql/cpp/ql/src/localDefinitions.ql"]
20+
case KeyType.ReferenceQuery: return ["/home/jcreed/semmle/code/ql/cpp/ql/src/localReferences.ql"]
21+
}
22+
}
23+
24+
export function createDefinitionsHandler(cli: CodeQLCliServer, qs: QueryServerClient, dbm: DatabaseManager): vscode.DefinitionProvider {
25+
let fileCache = new CachedOperation<vscode.LocationLink[]>(async (uriString: string) => {
26+
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
27+
const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme });
28+
29+
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
30+
if (db) {
31+
const links: vscode.DefinitionLink[] = []
32+
for (const query of await resolveQueries(KeyType.DefinitionQuery)) {
33+
const templates: messages.TemplateDefinitions = {
34+
[TEMPLATE_NAME]: {
35+
values: {
36+
tuples: [[{
37+
stringValue: uri.pathWithinSourceArchive
38+
}]]
39+
}
40+
}
41+
};
42+
const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates);
43+
if (results.result.resultType == messages.QueryResultType.SUCCESS) {
44+
links.push(...await getLinksFromResults(results, cli, db, (src, _dest) => src === uriString));
45+
}
46+
}
47+
return links;
48+
} else {
49+
return [];
50+
}
51+
});
52+
53+
return {
54+
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
55+
const fileLinks = await fileCache.get(document.uri.toString());
56+
let locLinks: vscode.LocationLink[] = [];
57+
for (const link of fileLinks) {
58+
if (link.originSelectionRange!.contains(position)) {
59+
locLinks.push(link);
60+
}
61+
}
62+
return locLinks;
63+
}
64+
65+
};
66+
}
67+
68+
interface FullLocationLink extends vscode.LocationLink {
69+
originUri: vscode.Uri;
70+
}
71+
72+
export function createReferencesHander(cli: CodeQLCliServer, qs: QueryServerClient, dbm: DatabaseManager): vscode.ReferenceProvider {
73+
let fileCache = new CachedOperation<FullLocationLink[]>(async (uriString: string) => {
74+
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
75+
const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme });
76+
77+
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
78+
if (db) {
79+
const links: FullLocationLink[] = []
80+
for (const query of await resolveQueries(KeyType.ReferenceQuery)) {
81+
const templates: messages.TemplateDefinitions = {
82+
[TEMPLATE_NAME]: {
83+
values: {
84+
tuples: [[{
85+
stringValue: uri.pathWithinSourceArchive
86+
}]]
87+
}
88+
}
89+
};
90+
const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates);
91+
if (results.result.resultType == messages.QueryResultType.SUCCESS) {
92+
links.push(...await getLinksFromResults(results, cli, db, (_src, dest) => dest === uriString));
93+
}
94+
}
95+
return links;
96+
} else {
97+
return [];
98+
}
99+
})
100+
101+
return {
102+
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
103+
const fileLinks = await fileCache.get(document.uri.toString());
104+
let locLinks: vscode.Location[] = [];
105+
for (const link of fileLinks) {
106+
if (link.targetRange!.contains(position)) {
107+
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
108+
}
109+
}
110+
return locLinks;
111+
}
112+
113+
};
114+
}
115+
116+
interface FileRange {
117+
file: vscode.Uri,
118+
range: vscode.Range
119+
}
120+
121+
async function getLinksFromResults(results: QueryWithResults, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean): Promise<FullLocationLink[]> {
122+
const localLinks: FullLocationLink[] = [];
123+
const bqrsPath = results.query.resultsPaths.resultsPath;
124+
const info = await cli.bqrsInfo(bqrsPath);
125+
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
126+
if (selectInfo && selectInfo.columns.length == 3
127+
&& selectInfo.columns[0].kind == "e"
128+
&& selectInfo.columns[1].kind == "e"
129+
&& selectInfo.columns[2].kind == "s") {
130+
// TODO: Page this
131+
const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME);
132+
for (const tuple of allTuples.tuples) {
133+
const src = tuple[0] as EntityValue;
134+
const dest = tuple[1] as EntityValue;
135+
const srcFile = src.url && fileRangeFromURI(src.url, db);
136+
const destFile = dest.url && fileRangeFromURI(dest.url, db);
137+
if (srcFile && destFile && filter(srcFile.file.toString(), destFile.file.toString())) {
138+
localLinks.push({ targetRange: destFile.range, targetUri: destFile.file, originSelectionRange: srcFile.range, originUri: srcFile.file });
139+
}
140+
}
141+
}
142+
return localLinks;
143+
}
144+
145+
function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined {
146+
if (typeof uri === "string") {
147+
return undefined;
148+
} else if ('startOffset' in uri) {
149+
return undefined;
150+
} else {
151+
const loc = uri as LineColumnLocation;
152+
const range = new vscode.Range(Math.max(0, loc.startLine - 1),
153+
Math.max(0, loc.startColumn - 1),
154+
Math.max(0, loc.endLine - 1),
155+
Math.max(0, loc.endColumn));
156+
try {
157+
const parsed = vscode.Uri.parse(uri.uri, true);
158+
if (parsed.scheme === "file") {
159+
return { file: db.resolveSourceFile(parsed.fsPath), range };
160+
}
161+
return undefined;
162+
} catch (e) {
163+
return undefined;
164+
}
165+
}
166+
}
167+
168+
const CACHE_SIZE = 100;
169+
class CachedOperation<U> {
170+
private readonly operation: (t: string) => Promise<U>;
171+
private readonly cached: Map<string, U>;
172+
private readonly lru: string[];
173+
private readonly inProgressCallbacks: Map<string, [(u: U) => void, (reason?: any) => void][]>;
174+
175+
constructor(operation: (t: string) => Promise<U>) {
176+
this.operation = operation;
177+
this.lru = [];
178+
this.inProgressCallbacks = new Map<string, [(u: U) => void, (reason?: any) => void][]>();
179+
this.cached = new Map<string, U>();
180+
}
181+
182+
async get(t: string): Promise<U> {
183+
// Try and retrieve from the cache
184+
const fromCache = this.cached.get(t);
185+
if (fromCache !== undefined) {
186+
// Move to end of lru list
187+
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0])
188+
return fromCache;
189+
}
190+
// Otherwise check if in progress
191+
const inProgressCallback = this.inProgressCallbacks.get(t);
192+
if (inProgressCallback !== undefined) {
193+
// If so wait for it to resolve
194+
return await new Promise((resolve, reject) => {
195+
inProgressCallback.push([resolve, reject]);
196+
});
197+
}
198+
199+
// Otherwise compute the new value, but leave a callback to allow sharing work
200+
const callbacks: [(u: U) => void, (reason?: any) => void][] = [];
201+
this.inProgressCallbacks.set(t, callbacks);
202+
try {
203+
const result = await this.operation(t);
204+
callbacks.forEach(f => f[0](result));
205+
this.inProgressCallbacks.delete(t);
206+
if (this.lru.length > CACHE_SIZE) {
207+
const toRemove = this.lru.shift()!;
208+
this.cached.delete(toRemove);
209+
}
210+
this.lru.push(t);
211+
this.cached.set(t, result);
212+
return result;
213+
} catch (e) {
214+
// Rethrow error on all callbacks
215+
callbacks.forEach(f => f[1](e));
216+
throw e;
217+
} finally {
218+
this.inProgressCallbacks.delete(t);
219+
}
220+
}
221+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
1+
import { commands, Disposable, ExtensionContext, extensions, languages, ProgressLocation, ProgressOptions, Uri, window as Window } from 'vscode';
22
import { LanguageClient } from 'vscode-languageclient';
3+
import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api';
34
import * as archiveFilesystemProvider from './archive-filesystem-provider';
4-
import { DistributionConfigListener, QueryServerConfigListener, QueryHistoryConfigListener } from './config';
5+
import { CodeQLCliServer } from './cli';
6+
import { DistributionConfigListener, QueryHistoryConfigListener, QueryServerConfigListener } from './config';
57
import { DatabaseManager } from './databases';
68
import { DatabaseUI } from './databases-ui';
7-
import {
8-
DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
9-
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError
10-
} from './distribution';
9+
import { createDefinitionsHandler, createReferencesHander } from './definitions';
10+
import { DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, DistributionManager, DistributionUpdateCheckResultKind, FindDistributionResult, FindDistributionResultKind, GithubApiError, GithubRateLimitedError } from './distribution';
1111
import * as helpers from './helpers';
12+
import { assertNever } from './helpers-pure';
1213
import { spawnIdeServer } from './ide-server';
1314
import { InterfaceManager, WebviewReveal } from './interface';
1415
import { ideServerLogger, logger, queryServerLogger } from './logging';
15-
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
16-
import { CompletedQuery } from './query-results';
1716
import { QueryHistoryManager } from './query-history';
17+
import { CompletedQuery } from './query-results';
1818
import * as qsClient from './queryserver-client';
19-
import { CodeQLCliServer } from './cli';
20-
import { assertNever } from './helpers-pure';
2119
import { displayQuickQuery } from './quick-query';
22-
import { TestHub, testExplorerExtensionId } from 'vscode-test-adapter-api';
20+
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
2321
import { QLTestAdapterFactory } from './test-adapter';
2422
import { TestUIService } from './test-ui';
2523

@@ -337,6 +335,15 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
337335
}));
338336

339337
ctx.subscriptions.push(client.start());
338+
339+
languages.registerDefinitionProvider(
340+
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
341+
createDefinitionsHandler(cliServer, qs, dbm)
342+
);
343+
languages.registerReferenceProvider(
344+
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
345+
createReferencesHander(cliServer, qs, dbm)
346+
);
340347
}
341348

342349
function initializeLogging(ctx: ExtensionContext): void {

0 commit comments

Comments
 (0)