Initial implementation of sourcemap-based jump-to-QL command

This commit is contained in:
Dave Bartolomeo
2022-07-14 13:55:46 -04:00
parent e57bbcb711
commit 855cb485d5
7 changed files with 166 additions and 2 deletions

View File

@@ -110,6 +110,12 @@
"extensions": [ "extensions": [
".qhelp" ".qhelp"
] ]
},
{
"id": "ql-summary",
"filenames": [
"evaluator-log.summary"
]
} }
], ],
"grammars": [ "grammars": [
@@ -608,6 +614,11 @@
"light": "media/light/clear-all.svg", "light": "media/light/clear-all.svg",
"dark": "media/dark/clear-all.svg" "dark": "media/dark/clear-all.svg"
} }
},
{
"command": "codeQL.gotoQL",
"title": "Go to QL Code",
"enablement": "codeql.hasQLSource"
} }
], ],
"menus": { "menus": {
@@ -1084,6 +1095,10 @@
{ {
"command": "codeQL.previewQueryHelp", "command": "codeQL.previewQueryHelp",
"when": "resourceExtname == .qhelp && isWorkspaceTrusted" "when": "resourceExtname == .qhelp && isWorkspaceTrusted"
},
{
"command": "codeQL.gotoQL",
"when": "editorLangId == ql-summary"
} }
] ]
}, },

View File

@@ -683,6 +683,7 @@ export class CodeQLCliServer implements Disposable {
const subcommandArgs = [ const subcommandArgs = [
'--format=text', '--format=text',
`--end-summary=${endSummaryPath}`, `--end-summary=${endSummaryPath}`,
`--sourcemap`,
inputPath, inputPath,
outputPath outputPath
]; ];

View File

@@ -98,6 +98,7 @@ import { handleDownloadPacks, handleInstallPackDependencies } from './packaging'
import { HistoryItemLabelProvider } from './history-item-label-provider'; import { HistoryItemLabelProvider } from './history-item-label-provider';
import { exportRemoteQueryResults } from './remote-queries/export-results'; import { exportRemoteQueryResults } from './remote-queries/export-results';
import { RemoteQuery } from './remote-queries/remote-query'; import { RemoteQuery } from './remote-queries/remote-query';
import { SummaryLanguageSupport } from './log-insights/summary-language-support';
/** /**
* extension.ts * extension.ts
@@ -1045,6 +1046,8 @@ async function activateWithInstalledDistribution(
}) })
); );
ctx.subscriptions.push(new SummaryLanguageSupport());
void logger.log('Starting language server.'); void logger.log('Starting language server.');
ctx.subscriptions.push(client.start()); ctx.subscriptions.push(client.start());

View File

@@ -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<void> {
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<PositionInFile | undefined> {
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<void> {
const position = await this.getQLSourceLocation();
const result = await commands.executeCommand('setContext', 'codeql.hasQLSource', position !== undefined);
void result;
}
handleDidChangeActiveTextEditor = async (editor: TextEditor | undefined): Promise<void> => {
void editor;
await this.updateContext();
}
handleDidChangeTextEditorSelection = async (e: TextEditorSelectionChangeEvent): Promise<void> => {
void e;
await this.updateContext();
}
handleGotoQL = async (): Promise<void> => {
const position = await this.getQLSourceLocation();
if (position !== undefined) {
await showSourceLocation(position);
}
};
}

View File

@@ -155,6 +155,10 @@ export interface CompilationOptions {
* get reported anyway. Useful for universal compilation options. * get reported anyway. Useful for universal compilation options.
*/ */
computeDefaultStrings: boolean; computeDefaultStrings: boolean;
/**
* Emit debug information in compiled query.
*/
emitDebugInfo: boolean;
} }
/** /**

View File

@@ -248,7 +248,8 @@ export class QueryEvaluationInfo {
localChecking: false, localChecking: false,
noComputeGetUrl: false, noComputeGetUrl: false,
noComputeToString: false, noComputeToString: false,
computeDefaultStrings: true computeDefaultStrings: true,
emitDebugInfo: true
}, },
extraOptions: { extraOptions: {
timeoutSecs: qs.config.timeoutSecs timeoutSecs: qs.config.timeoutSecs

View File

@@ -151,7 +151,8 @@ describe('using the query server', function() {
localChecking: false, localChecking: false,
noComputeGetUrl: false, noComputeGetUrl: false,
noComputeToString: false, noComputeToString: false,
computeDefaultStrings: true computeDefaultStrings: true,
emitDebugInfo: true
}, },
queryToCheck: qlProgram, queryToCheck: qlProgram,
resultPath: COMPILED_QUERY_PATH, resultPath: COMPILED_QUERY_PATH,