From 855cb485d5bbfac7b276f2ccb2e5e1e4e349a97f Mon Sep 17 00:00:00 2001 From: Dave Bartolomeo Date: Thu, 14 Jul 2022 13:55:46 -0400 Subject: [PATCH] Initial implementation of sourcemap-based jump-to-QL command --- extensions/ql-vscode/package.json | 15 ++ extensions/ql-vscode/src/cli.ts | 1 + extensions/ql-vscode/src/extension.ts | 3 + .../log-insights/summary-language-support.ts | 139 ++++++++++++++++++ extensions/ql-vscode/src/pure/messages.ts | 4 + extensions/ql-vscode/src/run-queries.ts | 3 +- .../cli-integration/query.test.ts | 3 +- 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 extensions/ql-vscode/src/log-insights/summary-language-support.ts diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 3d455378e..411db1a21 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -110,6 +110,12 @@ "extensions": [ ".qhelp" ] + }, + { + "id": "ql-summary", + "filenames": [ + "evaluator-log.summary" + ] } ], "grammars": [ @@ -608,6 +614,11 @@ "light": "media/light/clear-all.svg", "dark": "media/dark/clear-all.svg" } + }, + { + "command": "codeQL.gotoQL", + "title": "Go to QL Code", + "enablement": "codeql.hasQLSource" } ], "menus": { @@ -1084,6 +1095,10 @@ { "command": "codeQL.previewQueryHelp", "when": "resourceExtname == .qhelp && isWorkspaceTrusted" + }, + { + "command": "codeQL.gotoQL", + "when": "editorLangId == ql-summary" } ] }, diff --git a/extensions/ql-vscode/src/cli.ts b/extensions/ql-vscode/src/cli.ts index 3db84d0e6..085698d13 100644 --- a/extensions/ql-vscode/src/cli.ts +++ b/extensions/ql-vscode/src/cli.ts @@ -683,6 +683,7 @@ export class CodeQLCliServer implements Disposable { const subcommandArgs = [ '--format=text', `--end-summary=${endSummaryPath}`, + `--sourcemap`, inputPath, outputPath ]; diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index fb46a1e00..b2470d332 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -98,6 +98,7 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging' import { HistoryItemLabelProvider } from './history-item-label-provider'; import { exportRemoteQueryResults } from './remote-queries/export-results'; import { RemoteQuery } from './remote-queries/remote-query'; +import { SummaryLanguageSupport } from './log-insights/summary-language-support'; /** * extension.ts @@ -1045,6 +1046,8 @@ async function activateWithInstalledDistribution( }) ); + ctx.subscriptions.push(new SummaryLanguageSupport()); + void logger.log('Starting language server.'); ctx.subscriptions.push(client.start()); diff --git a/extensions/ql-vscode/src/log-insights/summary-language-support.ts b/extensions/ql-vscode/src/log-insights/summary-language-support.ts new file mode 100644 index 000000000..5aec78f98 --- /dev/null +++ b/extensions/ql-vscode/src/log-insights/summary-language-support.ts @@ -0,0 +1,139 @@ +import * as fs from 'fs'; +import { RawSourceMap, SourceMapConsumer } from 'source-map'; +import { commands, Position, Selection, TextDocument, TextEditor, TextEditorRevealType, TextEditorSelectionChangeEvent, ViewColumn, window, workspace } from 'vscode'; +import { DisposableObject } from '../pure/disposable-object'; +import { commandRunner } from '../commandRunner'; + +/** A `Position` within a specified file on disk. */ +interface PositionInFile { + filePath: string; + position: Position; +} + +/** + * Opens the specified source location in a text editor. + * @param position The position (including file path) to show. + */ +async function showSourceLocation(position: PositionInFile): Promise { + const document = await workspace.openTextDocument(position.filePath); + const editor = await window.showTextDocument(document, ViewColumn.Active); + editor.selection = new Selection(position.position, position.position); + editor.revealRange(editor.selection, TextEditorRevealType.InCenterIfOutsideViewport); +} + +/** + * Simple language support for human-readable evaluator log summaries. + * + * This class implements the `codeQL.gotoQL` command, which jumps from RA code to the corresponding + * QL code that generated it. It also tracks the current selection and active editor to enable and + * disable that command based on whether there is a QL mapping for the current selection. + */ +export class SummaryLanguageSupport extends DisposableObject { + /** + * The last `TextDocument` (with language `ql-summary`) for which we tried to find a sourcemap, or + * `undefined` if we have not seen such a document yet. + */ + private lastDocument : TextDocument | undefined = undefined; + /** + * The sourcemap for `lastDocument`, or `undefined` if there was no such sourcemap or document. + */ + private sourceMap : SourceMapConsumer | undefined = undefined; + + constructor() { + super(); + + this.push(window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor)); + this.push(window.onDidChangeTextEditorSelection(this.handleDidChangeTextEditorSelection)); + + this.push(commandRunner('codeQL.gotoQL', this.handleGotoQL)); + } + + /** + * Gets the location of the QL code that generated the RA at the current selection in the active + * editor, or `undefined` if there is no mapping. + */ + private async getQLSourceLocation(): Promise { + const editor = window.activeTextEditor; + if (editor === undefined) { + return undefined; + } + + const document = editor.document; + if (document.languageId !== 'ql-summary') { + return undefined; + } + + if (document.uri.scheme !== 'file') { + return undefined; + } + + if (this.lastDocument !== document) { + if (this.sourceMap !== undefined) { + this.sourceMap.destroy(); + this.sourceMap = undefined; + } + + const mapPath = document.uri.fsPath + '.map'; + + try { + const sourceMapText = await fs.promises.readFile(mapPath, 'utf-8'); + const rawMap: RawSourceMap = JSON.parse(sourceMapText); + this.sourceMap = await new SourceMapConsumer(rawMap); + } catch { + // Error reading sourcemap. Pretend there was no sourcemap + this.sourceMap = undefined; + } + this.lastDocument = document; + } + + if (this.sourceMap === undefined) { + return undefined; + } + + const qlPosition = this.sourceMap.originalPositionFor({ + line: editor.selection.start.line + 1, + column: editor.selection.start.character, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND + }); + + if ((qlPosition.source === null) || (qlPosition.line === null)) { + // No position found. + return undefined; + } + const line = qlPosition.line - 1; // In `source-map`, lines are 1-based... + const column = qlPosition.column ?? 0; // ...but columns are 0-based :( + + return { + filePath: qlPosition.source, + position: new Position(line, column) + }; + } + + /** + * Updates the `codeql.hasQLSource` context variable based on the current selection. This variable + * controls whether or not the `codeQL.gotoQL` command is enabled. + */ + private async updateContext(): Promise { + const position = await this.getQLSourceLocation(); + + const result = await commands.executeCommand('setContext', 'codeql.hasQLSource', position !== undefined); + void result; + } + + handleDidChangeActiveTextEditor = async (editor: TextEditor | undefined): Promise => { + void editor; + await this.updateContext(); + } + + handleDidChangeTextEditorSelection = async (e: TextEditorSelectionChangeEvent): Promise => { + void e; + await this.updateContext(); + } + + handleGotoQL = async (): Promise => { + const position = await this.getQLSourceLocation(); + if (position !== undefined) { + await showSourceLocation(position); + } + }; +} diff --git a/extensions/ql-vscode/src/pure/messages.ts b/extensions/ql-vscode/src/pure/messages.ts index ac3cac01d..cec4391a5 100644 --- a/extensions/ql-vscode/src/pure/messages.ts +++ b/extensions/ql-vscode/src/pure/messages.ts @@ -155,6 +155,10 @@ export interface CompilationOptions { * get reported anyway. Useful for universal compilation options. */ computeDefaultStrings: boolean; + /** + * Emit debug information in compiled query. + */ + emitDebugInfo: boolean; } /** diff --git a/extensions/ql-vscode/src/run-queries.ts b/extensions/ql-vscode/src/run-queries.ts index 71fcb8fe0..835408724 100644 --- a/extensions/ql-vscode/src/run-queries.ts +++ b/extensions/ql-vscode/src/run-queries.ts @@ -248,7 +248,8 @@ export class QueryEvaluationInfo { localChecking: false, noComputeGetUrl: false, noComputeToString: false, - computeDefaultStrings: true + computeDefaultStrings: true, + emitDebugInfo: true }, extraOptions: { timeoutSecs: qs.config.timeoutSecs diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/query.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/query.test.ts index bb90a352c..bf1fdcdf8 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/query.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/query.test.ts @@ -151,7 +151,8 @@ describe('using the query server', function() { localChecking: false, noComputeGetUrl: false, noComputeToString: false, - computeDefaultStrings: true + computeDefaultStrings: true, + emitDebugInfo: true }, queryToCheck: qlProgram, resultPath: COMPILED_QUERY_PATH,