Skip to content

Commit 855cb48

Browse files
author
Dave Bartolomeo
committed
Initial implementation of sourcemap-based jump-to-QL command
1 parent e57bbcb commit 855cb48

7 files changed

Lines changed: 166 additions & 2 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@
110110
"extensions": [
111111
".qhelp"
112112
]
113+
},
114+
{
115+
"id": "ql-summary",
116+
"filenames": [
117+
"evaluator-log.summary"
118+
]
113119
}
114120
],
115121
"grammars": [
@@ -608,6 +614,11 @@
608614
"light": "media/light/clear-all.svg",
609615
"dark": "media/dark/clear-all.svg"
610616
}
617+
},
618+
{
619+
"command": "codeQL.gotoQL",
620+
"title": "Go to QL Code",
621+
"enablement": "codeql.hasQLSource"
611622
}
612623
],
613624
"menus": {
@@ -1084,6 +1095,10 @@
10841095
{
10851096
"command": "codeQL.previewQueryHelp",
10861097
"when": "resourceExtname == .qhelp && isWorkspaceTrusted"
1098+
},
1099+
{
1100+
"command": "codeQL.gotoQL",
1101+
"when": "editorLangId == ql-summary"
10871102
}
10881103
]
10891104
},

extensions/ql-vscode/src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ export class CodeQLCliServer implements Disposable {
683683
const subcommandArgs = [
684684
'--format=text',
685685
`--end-summary=${endSummaryPath}`,
686+
`--sourcemap`,
686687
inputPath,
687688
outputPath
688689
];

extensions/ql-vscode/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging'
9898
import { HistoryItemLabelProvider } from './history-item-label-provider';
9999
import { exportRemoteQueryResults } from './remote-queries/export-results';
100100
import { RemoteQuery } from './remote-queries/remote-query';
101+
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
101102

102103
/**
103104
* extension.ts
@@ -1045,6 +1046,8 @@ async function activateWithInstalledDistribution(
10451046
})
10461047
);
10471048

1049+
ctx.subscriptions.push(new SummaryLanguageSupport());
1050+
10481051
void logger.log('Starting language server.');
10491052
ctx.subscriptions.push(client.start());
10501053

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as fs from 'fs';
2+
import { RawSourceMap, SourceMapConsumer } from 'source-map';
3+
import { commands, Position, Selection, TextDocument, TextEditor, TextEditorRevealType, TextEditorSelectionChangeEvent, ViewColumn, window, workspace } from 'vscode';
4+
import { DisposableObject } from '../pure/disposable-object';
5+
import { commandRunner } from '../commandRunner';
6+
7+
/** A `Position` within a specified file on disk. */
8+
interface PositionInFile {
9+
filePath: string;
10+
position: Position;
11+
}
12+
13+
/**
14+
* Opens the specified source location in a text editor.
15+
* @param position The position (including file path) to show.
16+
*/
17+
async function showSourceLocation(position: PositionInFile): Promise<void> {
18+
const document = await workspace.openTextDocument(position.filePath);
19+
const editor = await window.showTextDocument(document, ViewColumn.Active);
20+
editor.selection = new Selection(position.position, position.position);
21+
editor.revealRange(editor.selection, TextEditorRevealType.InCenterIfOutsideViewport);
22+
}
23+
24+
/**
25+
* Simple language support for human-readable evaluator log summaries.
26+
*
27+
* This class implements the `codeQL.gotoQL` command, which jumps from RA code to the corresponding
28+
* QL code that generated it. It also tracks the current selection and active editor to enable and
29+
* disable that command based on whether there is a QL mapping for the current selection.
30+
*/
31+
export class SummaryLanguageSupport extends DisposableObject {
32+
/**
33+
* The last `TextDocument` (with language `ql-summary`) for which we tried to find a sourcemap, or
34+
* `undefined` if we have not seen such a document yet.
35+
*/
36+
private lastDocument : TextDocument | undefined = undefined;
37+
/**
38+
* The sourcemap for `lastDocument`, or `undefined` if there was no such sourcemap or document.
39+
*/
40+
private sourceMap : SourceMapConsumer | undefined = undefined;
41+
42+
constructor() {
43+
super();
44+
45+
this.push(window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor));
46+
this.push(window.onDidChangeTextEditorSelection(this.handleDidChangeTextEditorSelection));
47+
48+
this.push(commandRunner('codeQL.gotoQL', this.handleGotoQL));
49+
}
50+
51+
/**
52+
* Gets the location of the QL code that generated the RA at the current selection in the active
53+
* editor, or `undefined` if there is no mapping.
54+
*/
55+
private async getQLSourceLocation(): Promise<PositionInFile | undefined> {
56+
const editor = window.activeTextEditor;
57+
if (editor === undefined) {
58+
return undefined;
59+
}
60+
61+
const document = editor.document;
62+
if (document.languageId !== 'ql-summary') {
63+
return undefined;
64+
}
65+
66+
if (document.uri.scheme !== 'file') {
67+
return undefined;
68+
}
69+
70+
if (this.lastDocument !== document) {
71+
if (this.sourceMap !== undefined) {
72+
this.sourceMap.destroy();
73+
this.sourceMap = undefined;
74+
}
75+
76+
const mapPath = document.uri.fsPath + '.map';
77+
78+
try {
79+
const sourceMapText = await fs.promises.readFile(mapPath, 'utf-8');
80+
const rawMap: RawSourceMap = JSON.parse(sourceMapText);
81+
this.sourceMap = await new SourceMapConsumer(rawMap);
82+
} catch {
83+
// Error reading sourcemap. Pretend there was no sourcemap
84+
this.sourceMap = undefined;
85+
}
86+
this.lastDocument = document;
87+
}
88+
89+
if (this.sourceMap === undefined) {
90+
return undefined;
91+
}
92+
93+
const qlPosition = this.sourceMap.originalPositionFor({
94+
line: editor.selection.start.line + 1,
95+
column: editor.selection.start.character,
96+
bias: SourceMapConsumer.GREATEST_LOWER_BOUND
97+
});
98+
99+
if ((qlPosition.source === null) || (qlPosition.line === null)) {
100+
// No position found.
101+
return undefined;
102+
}
103+
const line = qlPosition.line - 1; // In `source-map`, lines are 1-based...
104+
const column = qlPosition.column ?? 0; // ...but columns are 0-based :(
105+
106+
return {
107+
filePath: qlPosition.source,
108+
position: new Position(line, column)
109+
};
110+
}
111+
112+
/**
113+
* Updates the `codeql.hasQLSource` context variable based on the current selection. This variable
114+
* controls whether or not the `codeQL.gotoQL` command is enabled.
115+
*/
116+
private async updateContext(): Promise<void> {
117+
const position = await this.getQLSourceLocation();
118+
119+
const result = await commands.executeCommand('setContext', 'codeql.hasQLSource', position !== undefined);
120+
void result;
121+
}
122+
123+
handleDidChangeActiveTextEditor = async (editor: TextEditor | undefined): Promise<void> => {
124+
void editor;
125+
await this.updateContext();
126+
}
127+
128+
handleDidChangeTextEditorSelection = async (e: TextEditorSelectionChangeEvent): Promise<void> => {
129+
void e;
130+
await this.updateContext();
131+
}
132+
133+
handleGotoQL = async (): Promise<void> => {
134+
const position = await this.getQLSourceLocation();
135+
if (position !== undefined) {
136+
await showSourceLocation(position);
137+
}
138+
};
139+
}

extensions/ql-vscode/src/pure/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ export interface CompilationOptions {
155155
* get reported anyway. Useful for universal compilation options.
156156
*/
157157
computeDefaultStrings: boolean;
158+
/**
159+
* Emit debug information in compiled query.
160+
*/
161+
emitDebugInfo: boolean;
158162
}
159163

160164
/**

extensions/ql-vscode/src/run-queries.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ export class QueryEvaluationInfo {
248248
localChecking: false,
249249
noComputeGetUrl: false,
250250
noComputeToString: false,
251-
computeDefaultStrings: true
251+
computeDefaultStrings: true,
252+
emitDebugInfo: true
252253
},
253254
extraOptions: {
254255
timeoutSecs: qs.config.timeoutSecs

extensions/ql-vscode/src/vscode-tests/cli-integration/query.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ describe('using the query server', function() {
151151
localChecking: false,
152152
noComputeGetUrl: false,
153153
noComputeToString: false,
154-
computeDefaultStrings: true
154+
computeDefaultStrings: true,
155+
emitDebugInfo: true
155156
},
156157
queryToCheck: qlProgram,
157158
resultPath: COMPILED_QUERY_PATH,

0 commit comments

Comments
 (0)