Skip to content

Commit 8efb060

Browse files
authored
Merge pull request #337 from jcreedcmu/jcreed/jump-to-def
Add experimental support for Jump-to-def and Find-references
2 parents 23b1c00 + e242a8f commit 8efb060

File tree

10 files changed

+401
-61
lines changed

10 files changed

+401
-61
lines changed

extensions/ql-vscode/src/bqrs-cli-types.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11

22
export const PAGE_SIZE = 1000;
33

4-
export type ColumnKind = "f" | "i" | "s" | "b" | "d" | "e";
4+
/**
5+
* The single-character codes used in the bqrs format for the the kind
6+
* of a result column. This namespace is intentionally not an enum, see
7+
* the "for the sake of extensibility" comment in messages.ts.
8+
*/
9+
// eslint-disable-next-line @typescript-eslint/no-namespace
10+
export namespace ColumnKindCode {
11+
export const FLOAT = "f";
12+
export const INTEGER = "i";
13+
export const STRING = "s";
14+
export const BOOLEAN = "b";
15+
export const DATE = "d";
16+
export const ENTITY = "e";
17+
}
18+
19+
export type ColumnKind =
20+
| typeof ColumnKindCode.FLOAT
21+
| typeof ColumnKindCode.INTEGER
22+
| typeof ColumnKindCode.STRING
23+
| typeof ColumnKindCode.BOOLEAN
24+
| typeof ColumnKindCode.DATE
25+
| typeof ColumnKindCode.ENTITY;
526

627
export interface Column {
728
name?: string;
829
kind: ColumnKind;
930
}
1031

11-
1232
export interface ResultSetSchema {
1333
name: string;
1434
rows: number;

extensions/ql-vscode/src/cli.ts

Lines changed: 25 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.
@@ -614,6 +614,27 @@ export class CodeQLCliServer implements Disposable {
614614
"Resolving qlpack information",
615615
);
616616
}
617+
618+
/**
619+
* Gets information about queries in a query suite.
620+
* @param suite The suite to resolve.
621+
* @param additionalPacks A list of directories to search for qlpacks before searching in `searchPath`.
622+
* @param searchPath A list of directories to search for packs not found in `additionalPacks`. If undefined,
623+
* the default CLI search path is used.
624+
* @returns A list of query files found.
625+
*/
626+
resolveQueriesInSuite(suite: string, additionalPacks: string[], searchPath?: string[]): Promise<string[]> {
627+
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
628+
if (searchPath !== undefined) {
629+
args.push('--search-path', path.join(...searchPath));
630+
}
631+
args.push(suite);
632+
return this.runJsonCodeQlCliCommand<string[]>(
633+
['resolve', 'queries'],
634+
args,
635+
"Resolving queries",
636+
);
637+
}
617638
}
618639

619640
/**

extensions/ql-vscode/src/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ class Setting {
3939

4040
const ROOT_SETTING = new Setting('codeQL');
4141

42+
// Enable experimental features
43+
44+
/**
45+
* This setting is deliberately not in package.json so that it does
46+
* not appear in the settings ui in vscode itself. If users want to
47+
* enable experimental features, they can add
48+
* "codeQl.experimentalFeatures" directly in their vscode settings
49+
* json file.
50+
*/
51+
export const EXPERIMENTAL_FEATURES_SETTING = new Setting('experimentalFeatures', ROOT_SETTING);
52+
4253
// Distribution configuration
4354

4455
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);

extensions/ql-vscode/src/databases.ts

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

634+
public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {
635+
const uriString = uri.toString(true);
636+
return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);
637+
}
638+
634639
private async addDatabaseItem(item: DatabaseItemImpl) {
635640
this._databaseItems.push(item);
636641
this.updatePersistedDatabaseList();
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import * as fs from 'fs-extra';
2+
import * as yaml from 'js-yaml';
3+
import * as tmp from 'tmp';
4+
import * as vscode from "vscode";
5+
import { decodeSourceArchiveUri, zipArchiveScheme } from "./archive-filesystem-provider";
6+
import { ColumnKindCode, EntityValue, getResultSetSchema, LineColumnLocation, UrlValue } from "./bqrs-cli-types";
7+
import { CodeQLCliServer } from "./cli";
8+
import { DatabaseItem, DatabaseManager } from "./databases";
9+
import * as helpers from './helpers';
10+
import { CachedOperation } from './helpers';
11+
import * as messages from "./messages";
12+
import { QueryServerClient } from "./queryserver-client";
13+
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from "./run-queries";
14+
15+
/**
16+
* Run templated CodeQL queries to find definitions and references in
17+
* source-language files. We may eventually want to find a way to
18+
* generalize this to other custom queries, e.g. showing dataflow to
19+
* or from a selected identifier.
20+
*/
21+
22+
const TEMPLATE_NAME = "selectedSourceFile";
23+
const SELECT_QUERY_NAME = "#select";
24+
25+
enum KeyType {
26+
DefinitionQuery = 'DefinitionQuery',
27+
ReferenceQuery = 'ReferenceQuery',
28+
}
29+
30+
function tagOfKeyType(keyType: KeyType): string {
31+
switch (keyType) {
32+
case KeyType.DefinitionQuery: return "ide-contextual-queries/local-definitions";
33+
case KeyType.ReferenceQuery: return "ide-contextual-queries/local-references";
34+
}
35+
}
36+
37+
async function resolveQueries(cli: CodeQLCliServer, qlpack: string, keyType: KeyType): Promise<string[]> {
38+
const suiteFile = tmp.fileSync({ postfix: '.qls' }).name;
39+
const suiteYaml = { qlpack, include: { kind: 'definitions', 'tags contain': tagOfKeyType(keyType) } };
40+
await fs.writeFile(suiteFile, yaml.safeDump(suiteYaml), 'utf8');
41+
42+
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
43+
if (queries.length === 0) {
44+
throw new Error("Couldn't find any queries for qlpack");
45+
}
46+
return queries;
47+
}
48+
49+
async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<string | undefined> {
50+
if (db.contents === undefined)
51+
return undefined;
52+
const datasetPath = db.contents.datasetUri.fsPath;
53+
const { qlpack } = await helpers.resolveDatasetFolder(cli, datasetPath);
54+
return qlpack;
55+
}
56+
57+
interface FullLocationLink extends vscode.LocationLink {
58+
originUri: vscode.Uri;
59+
}
60+
61+
export class TemplateQueryDefinitionProvider implements vscode.DefinitionProvider {
62+
private cache: CachedOperation<vscode.LocationLink[]>;
63+
64+
constructor(
65+
private cli: CodeQLCliServer,
66+
private qs: QueryServerClient,
67+
private dbm: DatabaseManager,
68+
) {
69+
this.cache = new CachedOperation<vscode.LocationLink[]>(this.getDefinitions.bind(this));
70+
}
71+
72+
async getDefinitions(uriString: string): Promise<vscode.LocationLink[]> {
73+
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (src, _dest) => src === uriString);
74+
}
75+
76+
async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.LocationLink[]> {
77+
const fileLinks = await this.cache.get(document.uri.toString());
78+
const locLinks: vscode.LocationLink[] = [];
79+
for (const link of fileLinks) {
80+
if (link.originSelectionRange!.contains(position)) {
81+
locLinks.push(link);
82+
}
83+
}
84+
return locLinks;
85+
}
86+
}
87+
88+
export class TemplateQueryReferenceProvider implements vscode.ReferenceProvider {
89+
private cache: CachedOperation<FullLocationLink[]>;
90+
91+
constructor(
92+
private cli: CodeQLCliServer,
93+
private qs: QueryServerClient,
94+
private dbm: DatabaseManager,
95+
) {
96+
this.cache = new CachedOperation<FullLocationLink[]>(this.getReferences.bind(this));
97+
}
98+
99+
async getReferences(uriString: string): Promise<FullLocationLink[]> {
100+
return getLinksForUriString(this.cli, this.qs, this.dbm, uriString, (_src, dest) => dest === uriString);
101+
}
102+
103+
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise<vscode.Location[]> {
104+
const fileLinks = await this.cache.get(document.uri.toString());
105+
const locLinks: vscode.Location[] = [];
106+
for (const link of fileLinks) {
107+
if (link.targetRange!.contains(position)) {
108+
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
109+
}
110+
}
111+
return locLinks;
112+
}
113+
}
114+
115+
interface FileRange {
116+
file: vscode.Uri;
117+
range: vscode.Range;
118+
}
119+
120+
async function getLinksFromResults(results: QueryWithResults, cli: CodeQLCliServer, db: DatabaseItem, filter: (srcFile: string, destFile: string) => boolean): Promise<FullLocationLink[]> {
121+
const localLinks: FullLocationLink[] = [];
122+
const bqrsPath = results.query.resultsPaths.resultsPath;
123+
const info = await cli.bqrsInfo(bqrsPath);
124+
const selectInfo = getResultSetSchema(SELECT_QUERY_NAME, info);
125+
if (selectInfo && selectInfo.columns.length == 3
126+
&& selectInfo.columns[0].kind == ColumnKindCode.ENTITY
127+
&& selectInfo.columns[1].kind == ColumnKindCode.ENTITY
128+
&& selectInfo.columns[2].kind == ColumnKindCode.STRING) {
129+
// TODO: Page this
130+
const allTuples = await cli.bqrsDecode(bqrsPath, SELECT_QUERY_NAME);
131+
for (const tuple of allTuples.tuples) {
132+
const src = tuple[0] as EntityValue;
133+
const dest = tuple[1] as EntityValue;
134+
const srcFile = src.url && fileRangeFromURI(src.url, db);
135+
const destFile = dest.url && fileRangeFromURI(dest.url, db);
136+
if (srcFile && destFile && filter(srcFile.file.toString(), destFile.file.toString())) {
137+
localLinks.push({ targetRange: destFile.range, targetUri: destFile.file, originSelectionRange: srcFile.range, originUri: srcFile.file });
138+
}
139+
}
140+
}
141+
return localLinks;
142+
}
143+
144+
async function getLinksForUriString(
145+
cli: CodeQLCliServer,
146+
qs: QueryServerClient,
147+
dbm: DatabaseManager,
148+
uriString: string,
149+
filter: (src: string, dest: string) => boolean
150+
) {
151+
const uri = decodeSourceArchiveUri(vscode.Uri.parse(uriString));
152+
const sourceArchiveUri = vscode.Uri.file(uri.sourceArchiveZipPath).with({ scheme: zipArchiveScheme });
153+
154+
const db = dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
155+
if (db) {
156+
const qlpack = await qlpackOfDatabase(cli, db);
157+
if (qlpack === undefined) {
158+
throw new Error("Can't infer qlpack from database source archive");
159+
}
160+
const links: FullLocationLink[] = []
161+
for (const query of await resolveQueries(cli, qlpack, KeyType.ReferenceQuery)) {
162+
const templates: messages.TemplateDefinitions = {
163+
[TEMPLATE_NAME]: {
164+
values: {
165+
tuples: [[{
166+
stringValue: uri.pathWithinSourceArchive
167+
}]]
168+
}
169+
}
170+
};
171+
const results = await compileAndRunQueryAgainstDatabase(cli, qs, db, false, vscode.Uri.file(query), templates);
172+
if (results.result.resultType == messages.QueryResultType.SUCCESS) {
173+
links.push(...await getLinksFromResults(results, cli, db, filter));
174+
}
175+
}
176+
return links;
177+
} else {
178+
return [];
179+
}
180+
}
181+
182+
function fileRangeFromURI(uri: UrlValue, db: DatabaseItem): FileRange | undefined {
183+
if (typeof uri === "string") {
184+
return undefined;
185+
} else if ('startOffset' in uri) {
186+
return undefined;
187+
} else {
188+
const loc = uri as LineColumnLocation;
189+
const range = new vscode.Range(Math.max(0, loc.startLine - 1),
190+
Math.max(0, loc.startColumn - 1),
191+
Math.max(0, loc.endLine - 1),
192+
Math.max(0, loc.endColumn));
193+
try {
194+
const parsed = vscode.Uri.parse(uri.uri, true);
195+
if (parsed.scheme === "file") {
196+
return { file: db.resolveSourceFile(parsed.fsPath), range };
197+
}
198+
return undefined;
199+
} catch (e) {
200+
return undefined;
201+
}
202+
}
203+
}

extensions/ql-vscode/src/discovery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,4 @@ export abstract class Discovery<T> extends DisposableObject {
8484
* @param results The discovery results returned by the `discover` function.
8585
*/
8686
protected abstract update(results: T): void;
87-
}
87+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 20 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, EXPERIMENTAL_FEATURES_SETTING } 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 { TemplateQueryDefinitionProvider, TemplateQueryReferenceProvider } 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,17 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
337335
}));
338336

339337
ctx.subscriptions.push(client.start());
338+
339+
if (EXPERIMENTAL_FEATURES_SETTING.getValue()) {
340+
languages.registerDefinitionProvider(
341+
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
342+
new TemplateQueryDefinitionProvider(cliServer, qs, dbm)
343+
);
344+
languages.registerReferenceProvider(
345+
{ scheme: archiveFilesystemProvider.zipArchiveScheme },
346+
new TemplateQueryReferenceProvider(cliServer, qs, dbm)
347+
);
348+
}
340349
}
341350

342351
function initializeLogging(ctx: ExtensionContext): void {

0 commit comments

Comments
 (0)