This makes it possible to decode source maps containing references to code that is not part of the extension. If it finds any such references, it will simply not decode the source map and use the original stack trace instead.
256 lines
7.4 KiB
TypeScript
256 lines
7.4 KiB
TypeScript
/**
|
|
* 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 <version-number> <filename>:<line>:<column>
|
|
* For example: npx ts-node scripts/source-map.ts v1.7.8 "/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131164:13"
|
|
*
|
|
* Alternative usage: npx ts-node scripts/source-map.ts <version-number> <multi-line-stacktrace>
|
|
* For example: npx ts-node scripts/source-map.ts v1.7.8 'Error: Failed to find CodeQL distribution.
|
|
* at CodeQLCliServer.getCodeQlPath (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131164:13)
|
|
* at CodeQLCliServer.launchProcess (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131169:24)
|
|
* at CodeQLCliServer.runCodeQlCliInternal (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131194:24)
|
|
* at CodeQLCliServer.runJsonCodeQlCliCommand (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131330:20)
|
|
* at CodeQLCliServer.resolveRam (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131455:12)
|
|
* at QueryServerClient2.startQueryServerImpl (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:138618:21)'
|
|
*/
|
|
|
|
import { spawnSync } from "child_process";
|
|
import { basename, resolve } from "path";
|
|
import { pathExists, readJSON } from "fs-extra";
|
|
import { RawSourceMap, SourceMapConsumer } from "source-map";
|
|
import { Open } from "unzipper";
|
|
|
|
if (process.argv.length !== 4) {
|
|
console.error(
|
|
"Expected 2 arguments - the version number and the filename:line number",
|
|
);
|
|
}
|
|
|
|
const stackLineRegex =
|
|
/at (?<name>.*)? \((?<file>.*):(?<line>\d+):(?<column>\d+)\)/gm;
|
|
|
|
const versionNumber = process.argv[2].startsWith("v")
|
|
? process.argv[2]
|
|
: `v${process.argv[2]}`;
|
|
const stacktrace = process.argv[3];
|
|
|
|
async function extractSourceMap() {
|
|
const releaseAssetsDirectory = resolve(
|
|
__dirname,
|
|
"..",
|
|
"release-assets",
|
|
versionNumber,
|
|
);
|
|
const sourceMapsDirectory = resolve(
|
|
__dirname,
|
|
"..",
|
|
"artifacts",
|
|
"source-maps",
|
|
versionNumber,
|
|
);
|
|
|
|
if (!(await pathExists(sourceMapsDirectory))) {
|
|
console.log("Downloading source maps...");
|
|
|
|
const release = runGhJSON<Release>([
|
|
"release",
|
|
"view",
|
|
versionNumber,
|
|
"--json",
|
|
"id,name,assets",
|
|
]);
|
|
|
|
const sourcemapAsset = release.assets.find(
|
|
(asset) => asset.name === `vscode-codeql-sourcemaps-${versionNumber}.zip`,
|
|
);
|
|
|
|
if (sourcemapAsset) {
|
|
// This downloads a ZIP file of the source maps
|
|
runGh([
|
|
"release",
|
|
"download",
|
|
versionNumber,
|
|
"--pattern",
|
|
sourcemapAsset.name,
|
|
"--dir",
|
|
releaseAssetsDirectory,
|
|
]);
|
|
|
|
const file = await Open.file(
|
|
resolve(releaseAssetsDirectory, sourcemapAsset.name),
|
|
);
|
|
await file.extract({ path: sourceMapsDirectory });
|
|
} else {
|
|
const workflowRuns = runGhJSON<WorkflowRunListItem[]>([
|
|
"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,
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (stacktrace.includes("at")) {
|
|
const rawSourceMaps = new Map<string, RawSourceMap | null>();
|
|
|
|
const mappedStacktrace = await replaceAsync(
|
|
stacktrace,
|
|
stackLineRegex,
|
|
async (match, name, file, line, column) => {
|
|
if (!rawSourceMaps.has(file)) {
|
|
try {
|
|
const rawSourceMap: RawSourceMap = await readJSON(
|
|
resolve(sourceMapsDirectory, `${basename(file)}.map`),
|
|
);
|
|
rawSourceMaps.set(file, rawSourceMap);
|
|
} catch (e: unknown) {
|
|
// If the file is not found, we will not decode it and not try reading this source map again
|
|
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
|
rawSourceMaps.set(file, null);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
const sourceMap = rawSourceMaps.get(file) as RawSourceMap | null;
|
|
if (!sourceMap) {
|
|
return match;
|
|
}
|
|
|
|
const originalPosition = await SourceMapConsumer.with(
|
|
sourceMap,
|
|
null,
|
|
async function (consumer) {
|
|
return consumer.originalPositionFor({
|
|
line: parseInt(line, 10),
|
|
column: parseInt(column, 10),
|
|
});
|
|
},
|
|
);
|
|
|
|
if (!originalPosition.source) {
|
|
return match;
|
|
}
|
|
|
|
const originalFilename = resolve(file, "..", originalPosition.source);
|
|
|
|
return `at ${originalPosition.name ?? name} (${originalFilename}:${
|
|
originalPosition.line
|
|
}:${originalPosition.column})`;
|
|
},
|
|
);
|
|
|
|
console.log(mappedStacktrace);
|
|
} else {
|
|
// This means it's just a filename:line:column
|
|
const [filename, line, column] = stacktrace.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: RawSourceMap = await readJSON(sourcemapPath);
|
|
|
|
const originalPosition = await SourceMapConsumer.with(
|
|
rawSourceMap,
|
|
null,
|
|
async function (consumer) {
|
|
return consumer.originalPositionFor({
|
|
line: parseInt(line, 10),
|
|
column: parseInt(column, 10),
|
|
});
|
|
},
|
|
);
|
|
|
|
if (!originalPosition.source) {
|
|
throw new Error(`No source found for ${stacktrace}`);
|
|
}
|
|
|
|
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<T>(args: readonly string[]): T {
|
|
return JSON.parse(runGh(args));
|
|
}
|
|
|
|
type ReleaseAsset = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
type Release = {
|
|
id: string;
|
|
name: string;
|
|
assets: ReleaseAsset[];
|
|
};
|
|
|
|
type WorkflowRunListItem = {
|
|
databaseId: number;
|
|
number: number;
|
|
};
|
|
|
|
async function replaceAsync(
|
|
str: string,
|
|
regex: RegExp,
|
|
replacer: (substring: string, ...args: any[]) => Promise<string>,
|
|
) {
|
|
const promises: Array<Promise<string>> = [];
|
|
str.replace(regex, (match, ...args) => {
|
|
const promise = replacer(match, ...args);
|
|
promises.push(promise);
|
|
return match;
|
|
});
|
|
const data = await Promise.all(promises);
|
|
return str.replace(regex, () => data.shift() as string);
|
|
}
|