diff --git a/extensions/ql-vscode/scripts/source-map.ts b/extensions/ql-vscode/scripts/source-map.ts new file mode 100644 index 000000000..b93443577 --- /dev/null +++ b/extensions/ql-vscode/scripts/source-map.ts @@ -0,0 +1,124 @@ +/** + * This scripts helps finding the original source file and line number for a + * given file and line number in the compiled extension. It currently only + * works with released extensions. + * + * Usage: npx ts-node scripts/source-map.ts :: + */ + +import { spawnSync } from "child_process"; +import { basename, resolve } from "path"; +import { pathExists, readJSON } from "fs-extra"; +import { SourceMapConsumer } from "source-map"; + +if (process.argv.length !== 4) { + console.error( + "Expected 2 arguments - the version number and the filename:line number", + ); +} + +const versionNumber = process.argv[2].startsWith("v") + ? process.argv[2] + : `v${process.argv[2]}`; +const filenameAndLine = process.argv[3]; + +async function extractSourceMap() { + const sourceMapsDirectory = resolve( + __dirname, + "..", + "artifacts", + "source-maps", + versionNumber, + ); + + if (!(await pathExists(sourceMapsDirectory))) { + console.log("Downloading source maps..."); + + const workflowRuns = runGhJSON([ + "run", + "list", + "--workflow", + "release.yml", + "--branch", + versionNumber, + "--json", + "databaseId,number", + ]); + + if (workflowRuns.length !== 1) { + throw new Error( + `Expected exactly one workflow run for ${versionNumber}, got ${workflowRuns.length}`, + ); + } + + const workflowRun = workflowRuns[0]; + + runGh([ + "run", + "download", + workflowRun.databaseId.toString(), + "--name", + "vscode-codeql-sourcemaps", + "--dir", + sourceMapsDirectory, + ]); + } + + const [filename, line, column] = filenameAndLine.split(":", 3); + + const fileBasename = basename(filename); + + const sourcemapName = `${fileBasename}.map`; + const sourcemapPath = resolve(sourceMapsDirectory, sourcemapName); + + if (!(await pathExists(sourcemapPath))) { + throw new Error(`No source map found for ${fileBasename}`); + } + + const rawSourceMap = await readJSON(sourcemapPath); + + const originalPosition = await SourceMapConsumer.with( + rawSourceMap, + null, + async function (consumer) { + return consumer.originalPositionFor({ + line: parseInt(line), + column: parseInt(column), + }); + }, + ); + + if (!originalPosition.source) { + throw new Error(`No source found for ${filenameAndLine}`); + } + + const originalFilename = resolve(filename, "..", originalPosition.source); + + console.log( + `${originalFilename}:${originalPosition.line}:${originalPosition.column}`, + ); +} + +extractSourceMap().catch((e: unknown) => { + console.error(e); + process.exit(2); +}); + +function runGh(args: readonly string[]): string { + const gh = spawnSync("gh", args); + if (gh.status !== 0) { + throw new Error( + `Failed to get the source map for ${versionNumber}: ${gh.stderr}`, + ); + } + return gh.stdout.toString("utf-8"); +} + +function runGhJSON(args: readonly string[]): T { + return JSON.parse(runGh(args)); +} + +type WorkflowRunListItem = { + databaseId: number; + number: number; +};