Run Prettier on all files

This will change all existing files to match Prettier formatting.

The command used is `npm run format`.
This commit is contained in:
Koen Vlaswinkel
2022-11-16 19:06:13 +01:00
parent f41ca1a330
commit ebcdf8ad0b
384 changed files with 22050 additions and 14350 deletions

View File

@@ -1,20 +1,17 @@
import type { StorybookConfig } from '@storybook/core-common';
import type { StorybookConfig } from "@storybook/core-common";
const config: StorybookConfig = {
stories: [
'../src/**/*.stories.mdx',
'../src/**/*.stories.@(js|jsx|ts|tsx)'
],
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'./vscode-theme-addon/preset.ts',
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"./vscode-theme-addon/preset.ts",
],
framework: '@storybook/react',
framework: "@storybook/react",
core: {
builder: '@storybook/builder-webpack5'
}
builder: "@storybook/builder-webpack5",
},
};
module.exports = config;

View File

@@ -1,5 +1,5 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import { addons } from "@storybook/addons";
import { themes } from "@storybook/theming";
addons.setConfig({
theme: themes.dark,

View File

@@ -1,13 +1,13 @@
import { themes } from '@storybook/theming';
import { action } from '@storybook/addon-actions';
import { themes } from "@storybook/theming";
import { action } from "@storybook/addon-actions";
// Allow all stories/components to use Codicons
import '@vscode/codicons/dist/codicon.css';
import "@vscode/codicons/dist/codicon.css";
// https://storybook.js.org/docs/react/configure/overview#configure-story-rendering
export const parameters = {
// All props starting with `on` will automatically receive an action as a prop
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: "^on[A-Z].*" },
// All props matching these names will automatically get the correct control
controls: {
matchers: {
@@ -22,10 +22,10 @@ export const parameters = {
backgrounds: {
// The background is injected by our theme CSS files
disable: true,
}
},
};
(window as any).acquireVsCodeApi = () => ({
postMessage: action('post-vscode-message'),
setState: action('set-vscode-state'),
postMessage: action("post-vscode-message"),
setState: action("set-vscode-state"),
});

View File

@@ -1,30 +1,44 @@
import * as React from 'react';
import { FunctionComponent, useCallback } from 'react';
import * as React from "react";
import { FunctionComponent, useCallback } from "react";
import { useGlobals } from '@storybook/api';
import { IconButton, Icons, WithTooltip, TooltipLinkList, Link, WithHideFn } from '@storybook/components';
import { useGlobals } from "@storybook/api";
import {
IconButton,
Icons,
WithTooltip,
TooltipLinkList,
Link,
WithHideFn,
} from "@storybook/components";
import { themeNames, VSCodeTheme } from './theme';
import { themeNames, VSCodeTheme } from "./theme";
export const ThemeSelector: FunctionComponent = () => {
const [{ vscodeTheme }, updateGlobals] = useGlobals();
const changeTheme = useCallback((theme: VSCodeTheme) => {
updateGlobals({
vscodeTheme: theme,
});
}, [updateGlobals]);
const createLinks = useCallback((onHide: () => void): Link[] => Object.values(VSCodeTheme).map((theme) => ({
id: theme,
onClick() {
changeTheme(theme);
onHide();
const changeTheme = useCallback(
(theme: VSCodeTheme) => {
updateGlobals({
vscodeTheme: theme,
});
},
title: themeNames[theme],
value: theme,
active: vscodeTheme === theme,
})), [vscodeTheme, changeTheme]);
[updateGlobals],
);
const createLinks = useCallback(
(onHide: () => void): Link[] =>
Object.values(VSCodeTheme).map((theme) => ({
id: theme,
onClick() {
changeTheme(theme);
onHide();
},
title: themeNames[theme],
value: theme,
active: vscodeTheme === theme,
})),
[vscodeTheme, changeTheme],
);
return (
<WithTooltip
@@ -32,9 +46,7 @@ export const ThemeSelector: FunctionComponent = () => {
trigger="click"
closeOnClick
tooltip={({ onHide }: WithHideFn) => (
<TooltipLinkList
links={createLinks(onHide)}
/>
<TooltipLinkList links={createLinks(onHide)} />
)}
>
<IconButton

View File

@@ -1,12 +1,12 @@
import * as React from 'react';
import { addons, types } from '@storybook/addons';
import { ThemeSelector } from './ThemeSelector';
import * as React from "react";
import { addons, types } from "@storybook/addons";
import { ThemeSelector } from "./ThemeSelector";
const ADDON_ID = 'vscode-theme-addon';
const ADDON_ID = "vscode-theme-addon";
addons.register(ADDON_ID, () => {
addons.add(ADDON_ID, {
title: 'VSCode Themes',
title: "VSCode Themes",
type: types.TOOL,
match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
render: () => <ThemeSelector />,

View File

@@ -1,7 +1,7 @@
export function config(entry = []) {
return [...entry, require.resolve('./preview.ts')];
return [...entry, require.resolve("./preview.ts")];
}
export function managerEntries(entry = []) {
return [...entry, require.resolve('./manager.tsx')];
return [...entry, require.resolve("./manager.tsx")];
}

View File

@@ -1,5 +1,5 @@
import { withTheme } from './withTheme';
import { VSCodeTheme } from './theme';
import { withTheme } from "./withTheme";
import { VSCodeTheme } from "./theme";
export const decorators = [withTheme];

View File

@@ -1,9 +1,9 @@
export enum VSCodeTheme {
Dark = 'dark',
Light = 'light',
Dark = "dark",
Light = "light",
}
export const themeNames: { [key in VSCodeTheme]: string } = {
[VSCodeTheme.Dark]: 'Dark+',
[VSCodeTheme.Light]: 'Light+',
[VSCodeTheme.Dark]: "Dark+",
[VSCodeTheme.Light]: "Light+",
};

View File

@@ -1,35 +1,45 @@
import { useEffect, useGlobals } from '@storybook/addons';
import type { AnyFramework, PartialStoryFn as StoryFunction, StoryContext } from '@storybook/csf';
import { useEffect, useGlobals } from "@storybook/addons";
import type {
AnyFramework,
PartialStoryFn as StoryFunction,
StoryContext,
} from "@storybook/csf";
import { VSCodeTheme } from './theme';
import { VSCodeTheme } from "./theme";
const themeFiles: { [key in VSCodeTheme]: string } = {
// eslint-disable-next-line @typescript-eslint/no-var-requires
[VSCodeTheme.Dark]: require('!file-loader?modules!../../src/stories/vscode-theme-dark.css').default,
// eslint-disable-next-line @typescript-eslint/no-var-requires
[VSCodeTheme.Light]: require('!file-loader?modules!../../src/stories/vscode-theme-light.css').default,
[VSCodeTheme.Dark]:
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("!file-loader?modules!../../src/stories/vscode-theme-dark.css")
.default,
[VSCodeTheme.Light]:
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("!file-loader?modules!../../src/stories/vscode-theme-light.css")
.default,
};
export const withTheme = (
StoryFn: StoryFunction<AnyFramework>,
context: StoryContext<AnyFramework>
context: StoryContext<AnyFramework>,
) => {
const [{ vscodeTheme }] = useGlobals();
useEffect(() => {
const styleSelectorId =
context.viewMode === 'docs'
context.viewMode === "docs"
? `addon-vscode-theme-docs-${context.id}`
: 'addon-vscode-theme-theme';
: "addon-vscode-theme-theme";
const theme = Object.values(VSCodeTheme).includes(vscodeTheme) ? vscodeTheme as VSCodeTheme : VSCodeTheme.Dark;
const theme = Object.values(VSCodeTheme).includes(vscodeTheme)
? (vscodeTheme as VSCodeTheme)
: VSCodeTheme.Dark;
document.getElementById(styleSelectorId)?.remove();
const link = document.createElement('link');
const link = document.createElement("link");
link.id = styleSelectorId;
link.href = themeFiles[theme];
link.rel = 'stylesheet';
link.rel = "stylesheet";
document.head.appendChild(link);
}, [vscodeTheme]);

View File

@@ -1,17 +1,20 @@
import * as gulp from 'gulp';
import * as gulp from "gulp";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const replace = require('gulp-replace');
const replace = require("gulp-replace");
/** Inject the application insights key into the telemetry file */
export function injectAppInsightsKey() {
if (!process.env.APP_INSIGHTS_KEY) {
// noop
console.log('APP_INSIGHTS_KEY environment variable is not set. So, cannot inject it into the application.');
console.log(
"APP_INSIGHTS_KEY environment variable is not set. So, cannot inject it into the application.",
);
return Promise.resolve();
}
// replace the key
return gulp.src(['out/telemetry.js'])
return gulp
.src(["out/telemetry.js"])
.pipe(replace(/REPLACE-APP-INSIGHTS-KEY/, process.env.APP_INSIGHTS_KEY))
.pipe(gulp.dest('out/'));
.pipe(gulp.dest("out/"));
}

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
export interface DeployedPackage {
distPath: string;
@@ -8,45 +8,64 @@ export interface DeployedPackage {
}
const packageFiles = [
'.vscodeignore',
'CHANGELOG.md',
'README.md',
'language-configuration.json',
'snippets.json',
'media',
'node_modules',
'out',
'workspace-databases-schema.json'
".vscodeignore",
"CHANGELOG.md",
"README.md",
"language-configuration.json",
"snippets.json",
"media",
"node_modules",
"out",
"workspace-databases-schema.json",
];
async function copyPackage(sourcePath: string, destPath: string): Promise<void> {
async function copyPackage(
sourcePath: string,
destPath: string,
): Promise<void> {
for (const file of packageFiles) {
console.log(`copying ${path.resolve(sourcePath, file)} to ${path.resolve(destPath, file)}`);
console.log(
`copying ${path.resolve(sourcePath, file)} to ${path.resolve(
destPath,
file,
)}`,
);
await fs.copy(path.resolve(sourcePath, file), path.resolve(destPath, file));
}
}
export async function deployPackage(packageJsonPath: string): Promise<DeployedPackage> {
export async function deployPackage(
packageJsonPath: string,
): Promise<DeployedPackage> {
try {
const packageJson: any = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const packageJson: any = JSON.parse(
await fs.readFile(packageJsonPath, "utf8"),
);
// Default to development build; use flag --release to indicate release build.
const isDevBuild = !process.argv.includes('--release');
const distDir = path.join(__dirname, '../../../dist');
const isDevBuild = !process.argv.includes("--release");
const distDir = path.join(__dirname, "../../../dist");
await fs.mkdirs(distDir);
if (isDevBuild) {
// NOTE: rootPackage.name had better not have any regex metacharacters
const oldDevBuildPattern = new RegExp('^' + packageJson.name + '[^/]+-dev[0-9.]+\\.vsix$');
const oldDevBuildPattern = new RegExp(
"^" + packageJson.name + "[^/]+-dev[0-9.]+\\.vsix$",
);
// Dev package filenames are of the form
// vscode-codeql-0.0.1-dev.2019.9.27.19.55.20.vsix
(await fs.readdir(distDir)).filter(name => name.match(oldDevBuildPattern)).map(build => {
console.log(`Deleting old dev build ${build}...`);
fs.unlinkSync(path.join(distDir, build));
});
(await fs.readdir(distDir))
.filter((name) => name.match(oldDevBuildPattern))
.map((build) => {
console.log(`Deleting old dev build ${build}...`);
fs.unlinkSync(path.join(distDir, build));
});
const now = new Date();
packageJson.version = packageJson.version +
`-dev.${now.getUTCFullYear()}.${now.getUTCMonth() + 1}.${now.getUTCDate()}` +
packageJson.version =
packageJson.version +
`-dev.${now.getUTCFullYear()}.${
now.getUTCMonth() + 1
}.${now.getUTCDate()}` +
`.${now.getUTCHours()}.${now.getUTCMinutes()}.${now.getUTCSeconds()}`;
}
@@ -54,19 +73,23 @@ export async function deployPackage(packageJsonPath: string): Promise<DeployedPa
await fs.remove(distPath);
await fs.mkdirs(distPath);
await fs.writeFile(path.join(distPath, 'package.json'), JSON.stringify(packageJson, null, 2));
await fs.writeFile(
path.join(distPath, "package.json"),
JSON.stringify(packageJson, null, 2),
);
const sourcePath = path.join(__dirname, '..');
console.log(`Copying package '${packageJson.name}' and its dependencies to '${distPath}'...`);
const sourcePath = path.join(__dirname, "..");
console.log(
`Copying package '${packageJson.name}' and its dependencies to '${distPath}'...`,
);
await copyPackage(sourcePath, distPath);
return {
distPath: distPath,
name: packageJson.name,
version: packageJson.version
version: packageJson.version,
};
}
catch (e) {
} catch (e) {
console.error(e);
throw e;
}

View File

@@ -1,18 +1,20 @@
import * as gulp from 'gulp';
import { compileTypeScript, watchTypeScript, cleanOutput } from './typescript';
import { compileTextMateGrammar } from './textmate';
import { copyTestData, watchTestData } from './tests';
import { compileView, watchView } from './webpack';
import { packageExtension } from './package';
import { injectAppInsightsKey } from './appInsights';
import * as gulp from "gulp";
import { compileTypeScript, watchTypeScript, cleanOutput } from "./typescript";
import { compileTextMateGrammar } from "./textmate";
import { copyTestData, watchTestData } from "./tests";
import { compileView, watchView } from "./webpack";
import { packageExtension } from "./package";
import { injectAppInsightsKey } from "./appInsights";
export const buildWithoutPackage =
gulp.series(
cleanOutput,
gulp.parallel(
compileTypeScript, compileTextMateGrammar, compileView, copyTestData
)
);
export const buildWithoutPackage = gulp.series(
cleanOutput,
gulp.parallel(
compileTypeScript,
compileTextMateGrammar,
compileView,
copyTestData,
),
);
export {
cleanOutput,
@@ -25,4 +27,8 @@ export {
injectAppInsightsKey,
compileView,
};
export default gulp.series(buildWithoutPackage, injectAppInsightsKey, packageExtension);
export default gulp.series(
buildWithoutPackage,
injectAppInsightsKey,
packageExtension,
);

View File

@@ -1,21 +1,28 @@
import * as path from 'path';
import { deployPackage } from './deploy';
import * as childProcess from 'child-process-promise';
import * as path from "path";
import { deployPackage } from "./deploy";
import * as childProcess from "child-process-promise";
export async function packageExtension(): Promise<void> {
const deployedPackage = await deployPackage(path.resolve('package.json'));
console.log(`Packaging extension '${deployedPackage.name}@${deployedPackage.version}'...`);
const deployedPackage = await deployPackage(path.resolve("package.json"));
console.log(
`Packaging extension '${deployedPackage.name}@${deployedPackage.version}'...`,
);
const args = [
'package',
'--out', path.resolve(deployedPackage.distPath, '..', `${deployedPackage.name}-${deployedPackage.version}.vsix`)
"package",
"--out",
path.resolve(
deployedPackage.distPath,
"..",
`${deployedPackage.name}-${deployedPackage.version}.vsix`,
),
];
const proc = childProcess.spawn('./node_modules/.bin/vsce', args, {
cwd: deployedPackage.distPath
const proc = childProcess.spawn("./node_modules/.bin/vsce", args, {
cwd: deployedPackage.distPath,
});
proc.childProcess.stdout!.on('data', (data) => {
proc.childProcess.stdout!.on("data", (data) => {
console.log(data.toString());
});
proc.childProcess.stderr!.on('data', (data) => {
proc.childProcess.stderr!.on("data", (data) => {
console.error(data.toString());
});

View File

@@ -1,22 +1,21 @@
import * as gulp from 'gulp';
import * as gulp from "gulp";
export function copyTestData() {
return Promise.all([
copyNoWorkspaceData(),
copyCliIntegrationData()
]);
return Promise.all([copyNoWorkspaceData(), copyCliIntegrationData()]);
}
export function watchTestData() {
return gulp.watch(['src/vscode-tests/*/data/**/*'], copyTestData);
return gulp.watch(["src/vscode-tests/*/data/**/*"], copyTestData);
}
function copyNoWorkspaceData() {
return gulp.src('src/vscode-tests/no-workspace/data/**/*')
.pipe(gulp.dest('out/vscode-tests/no-workspace/data'));
return gulp
.src("src/vscode-tests/no-workspace/data/**/*")
.pipe(gulp.dest("out/vscode-tests/no-workspace/data"));
}
function copyCliIntegrationData() {
return gulp.src('src/vscode-tests/cli-integration/data/**/*')
.pipe(gulp.dest('out/vscode-tests/cli-integration/data'));
return gulp
.src("src/vscode-tests/cli-integration/data/**/*")
.pipe(gulp.dest("out/vscode-tests/cli-integration/data"));
}

View File

@@ -1,8 +1,8 @@
import * as gulp from 'gulp';
import * as jsYaml from 'js-yaml';
import * as through from 'through2';
import * as PluginError from 'plugin-error';
import * as Vinyl from 'vinyl';
import * as gulp from "gulp";
import * as jsYaml from "js-yaml";
import * as through from "through2";
import * as PluginError from "plugin-error";
import * as Vinyl from "vinyl";
/**
* Replaces all rule references with the match pattern of the referenced rule.
@@ -11,7 +11,10 @@ import * as Vinyl from 'vinyl';
* @param replacements Map from rule name to match text.
* @returns The new regex after replacement.
*/
function replaceReferencesWithStrings(value: string, replacements: Map<string, string>): string {
function replaceReferencesWithStrings(
value: string,
replacements: Map<string, string>,
): string {
let result = value;
// eslint-disable-next-line no-constant-condition
while (true) {
@@ -52,21 +55,19 @@ function getNodeMatchText(rule: any): string {
if (rule.match !== undefined) {
// For a match string, just use that string as the replacement.
return rule.match;
}
else if (rule.patterns !== undefined) {
} else if (rule.patterns !== undefined) {
const patterns: string[] = [];
// For a list of patterns, use the disjunction of those patterns.
for (const patternIndex in rule.patterns) {
const pattern = rule.patterns[patternIndex];
if (pattern.include !== null) {
patterns.push('(?' + pattern.include + ')');
patterns.push("(?" + pattern.include + ")");
}
}
return '(?:' + patterns.join('|') + ')';
}
else {
return '';
return "(?:" + patterns.join("|") + ")";
} else {
return "";
}
}
@@ -109,7 +110,7 @@ function visitAllRulesInFile(yaml: any, action: (rule: any) => void) {
function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) {
for (const key in ruleMap) {
const rule = ruleMap[key];
if ((typeof rule) === 'object') {
if (typeof rule === "object") {
action(rule);
if (rule.patterns !== undefined) {
visitAllRulesInRuleMap(rule.patterns, action);
@@ -127,10 +128,10 @@ function visitAllRulesInRuleMap(ruleMap: any, action: (rule: any) => void) {
function visitAllMatchesInRule(rule: any, action: (match: any) => any) {
for (const key in rule) {
switch (key) {
case 'begin':
case 'end':
case 'match':
case 'while':
case "begin":
case "end":
case "match":
case "while":
rule[key] = action(rule[key]);
break;
@@ -147,21 +148,21 @@ function visitAllMatchesInRule(rule: any, action: (match: any) => any) {
* @param rule Rule to be transformed.
* @param key Base key of the property to be transformed.
*/
function expandPatternMatchProperties(rule: any, key: 'begin' | 'end') {
const patternKey = key + 'Pattern';
const capturesKey = key + 'Captures';
function expandPatternMatchProperties(rule: any, key: "begin" | "end") {
const patternKey = key + "Pattern";
const capturesKey = key + "Captures";
const pattern = rule[patternKey];
if (pattern !== undefined) {
const patterns: string[] = Array.isArray(pattern) ? pattern : [pattern];
rule[key] = patterns.map(p => `((?${p}))`).join('|');
rule[key] = patterns.map((p) => `((?${p}))`).join("|");
const captures: { [index: string]: any } = {};
for (const patternIndex in patterns) {
captures[(Number(patternIndex) + 1).toString()] = {
patterns: [
{
include: patterns[patternIndex]
}
]
include: patterns[patternIndex],
},
],
};
}
rule[capturesKey] = captures;
@@ -177,20 +178,19 @@ function expandPatternMatchProperties(rule: any, key: 'begin' | 'end') {
function transformFile(yaml: any) {
const macros = gatherMacros(yaml);
visitAllRulesInFile(yaml, (rule) => {
expandPatternMatchProperties(rule, 'begin');
expandPatternMatchProperties(rule, 'end');
expandPatternMatchProperties(rule, "begin");
expandPatternMatchProperties(rule, "end");
});
// Expand macros in matches.
visitAllRulesInFile(yaml, (rule) => {
visitAllMatchesInRule(rule, (match) => {
if ((typeof match) === 'object') {
if (typeof match === "object") {
for (const key in match) {
return macros.get(key)!.replace('(?#)', `(?:${match[key]})`);
return macros.get(key)!.replace("(?#)", `(?:${match[key]})`);
}
throw new Error('No key in macro map.');
}
else {
throw new Error("No key in macro map.");
} else {
return match;
}
});
@@ -207,7 +207,7 @@ function transformFile(yaml: any) {
});
if (yaml.regexOptions !== undefined) {
const regexOptions = '(?' + yaml.regexOptions + ')';
const regexOptions = "(?" + yaml.regexOptions + ")";
visitAllRulesInFile(yaml, (rule) => {
visitAllMatchesInRule(rule, (match) => {
return regexOptions + match;
@@ -219,28 +219,36 @@ function transformFile(yaml: any) {
}
export function transpileTextMateGrammar() {
return through.obj((file: Vinyl, _encoding: string, callback: (err: string | null, file: Vinyl | PluginError) => void): void => {
if (file.isNull()) {
callback(null, file);
}
else if (file.isBuffer()) {
const buf: Buffer = file.contents;
const yamlText: string = buf.toString('utf8');
const jsonData: any = jsYaml.load(yamlText);
transformFile(jsonData);
return through.obj(
(
file: Vinyl,
_encoding: string,
callback: (err: string | null, file: Vinyl | PluginError) => void,
): void => {
if (file.isNull()) {
callback(null, file);
} else if (file.isBuffer()) {
const buf: Buffer = file.contents;
const yamlText: string = buf.toString("utf8");
const jsonData: any = jsYaml.load(yamlText);
transformFile(jsonData);
file.contents = Buffer.from(JSON.stringify(jsonData, null, 2), 'utf8');
file.extname = '.json';
callback(null, file);
}
else {
callback('error', new PluginError('transpileTextMateGrammar', 'Format not supported.'));
}
});
file.contents = Buffer.from(JSON.stringify(jsonData, null, 2), "utf8");
file.extname = ".json";
callback(null, file);
} else {
callback(
"error",
new PluginError("transpileTextMateGrammar", "Format not supported."),
);
}
},
);
}
export function compileTextMateGrammar() {
return gulp.src('syntaxes/*.tmLanguage.yml')
return gulp
.src("syntaxes/*.tmLanguage.yml")
.pipe(transpileTextMateGrammar())
.pipe(gulp.dest('out/syntaxes'));
.pipe(gulp.dest("out/syntaxes"));
}

View File

@@ -1,41 +1,62 @@
import * as colors from 'ansi-colors';
import * as gulp from 'gulp';
import * as sourcemaps from 'gulp-sourcemaps';
import * as ts from 'gulp-typescript';
import * as del from 'del';
import * as colors from "ansi-colors";
import * as gulp from "gulp";
import * as sourcemaps from "gulp-sourcemaps";
import * as ts from "gulp-typescript";
import * as del from "del";
function goodReporter(): ts.reporter.Reporter {
return {
error: (error, typescript) => {
if (error.tsFile) {
console.log('[' + colors.gray('gulp-typescript') + '] ' + colors.red(error.fullFilename
+ '(' + (error.startPosition!.line + 1) + ',' + error.startPosition!.character + '): ')
+ 'error TS' + error.diagnostic.code + ': ' + typescript.flattenDiagnosticMessageText(error.diagnostic.messageText, '\n'));
}
else {
console.log(
"[" +
colors.gray("gulp-typescript") +
"] " +
colors.red(
error.fullFilename +
"(" +
(error.startPosition!.line + 1) +
"," +
error.startPosition!.character +
"): ",
) +
"error TS" +
error.diagnostic.code +
": " +
typescript.flattenDiagnosticMessageText(
error.diagnostic.messageText,
"\n",
),
);
} else {
console.log(error.message);
}
},
};
}
const tsProject = ts.createProject('tsconfig.json');
const tsProject = ts.createProject("tsconfig.json");
export function cleanOutput() {
return tsProject.projectDirectory ? del(tsProject.projectDirectory + '/out/*') : Promise.resolve();
return tsProject.projectDirectory
? del(tsProject.projectDirectory + "/out/*")
: Promise.resolve();
}
export function compileTypeScript() {
return tsProject.src()
return tsProject
.src()
.pipe(sourcemaps.init())
.pipe(tsProject(goodReporter()))
.pipe(sourcemaps.write('.', {
includeContent: false,
sourceRoot: '.',
}))
.pipe(gulp.dest('out'));
.pipe(
sourcemaps.write(".", {
includeContent: false,
sourceRoot: ".",
}),
)
.pipe(gulp.dest("out"));
}
export function watchTypeScript() {
gulp.watch('src/**/*.ts', compileTypeScript);
gulp.watch("src/**/*.ts", compileTypeScript);
}

View File

@@ -1,80 +1,80 @@
import * as path from 'path';
import * as webpack from 'webpack';
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as path from "path";
import * as webpack from "webpack";
import * as MiniCssExtractPlugin from "mini-css-extract-plugin";
export const config: webpack.Configuration = {
mode: 'development',
mode: "development",
entry: {
webview: './src/view/webview.tsx'
webview: "./src/view/webview.tsx",
},
output: {
path: path.resolve(__dirname, '..', 'out'),
filename: '[name].js'
path: path.resolve(__dirname, "..", "out"),
filename: "[name].js",
},
devtool: 'inline-source-map',
devtool: "inline-source-map",
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json'],
extensions: [".js", ".ts", ".tsx", ".json"],
fallback: {
path: require.resolve('path-browserify')
}
path: require.resolve("path-browserify"),
},
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
loader: "ts-loader",
options: {
configFile: 'src/view/tsconfig.json',
}
configFile: "src/view/tsconfig.json",
},
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
loader: "css-loader",
options: {
importLoaders: 1,
sourceMap: true
}
sourceMap: true,
},
},
{
loader: 'less-loader',
loader: "less-loader",
options: {
javascriptEnabled: true,
sourceMap: true
}
}
]
sourceMap: true,
},
},
],
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader'
}
]
loader: "css-loader",
},
],
},
{
test: /\.(woff(2)?|ttf|eot)$/,
use: [
{
loader: 'file-loader',
loader: "file-loader",
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
name: "[name].[ext]",
outputPath: "fonts/",
// We need this to make Webpack use the correct path for the fonts.
// Without this, the CSS file will use `url([object Module])`
esModule: false
}
esModule: false,
},
},
],
}
]
},
],
},
performance: {
hints: false
hints: false,
},
plugins: [new MiniCssExtractPlugin()],
};

View File

@@ -1,5 +1,5 @@
import * as webpack from 'webpack';
import { config } from './webpack.config';
import * as webpack from "webpack";
import { config } from "./webpack.config";
export function compileView(cb: (err?: Error) => void) {
doWebpack(config, true, cb);
@@ -12,35 +12,41 @@ export function watchView(cb: (err?: Error) => void) {
watchOptions: {
aggregateTimeout: 200,
poll: 1000,
}
},
};
doWebpack(watchConfig, false, cb);
}
function doWebpack(internalConfig: webpack.Configuration, failOnError: boolean, cb: (err?: Error) => void) {
function doWebpack(
internalConfig: webpack.Configuration,
failOnError: boolean,
cb: (err?: Error) => void,
) {
const resultCb = (error: Error | undefined, stats?: webpack.Stats) => {
if (error) {
cb(error);
}
if (stats) {
console.log(stats.toString({
errorDetails: true,
colors: true,
assets: false,
builtAt: false,
version: false,
hash: false,
entrypoints: false,
timings: false,
modules: false,
errors: true
}));
console.log(
stats.toString({
errorDetails: true,
colors: true,
assets: false,
builtAt: false,
version: false,
hash: false,
entrypoints: false,
timings: false,
modules: false,
errors: true,
}),
);
if (stats.hasErrors()) {
if (failOnError) {
cb(new Error('Compilation errors detected.'));
cb(new Error("Compilation errors detected."));
return;
} else {
console.error('Compilation errors detected.');
console.error("Compilation errors detected.");
}
}
cb();

View File

@@ -11,20 +11,23 @@
* Usage: npx ts-node scripts/add-fields-to-scenarios.ts
*/
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
import { throttling } from '@octokit/plugin-throttling';
import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
import { throttling } from "@octokit/plugin-throttling";
import { getFiles } from './util/files';
import type { GitHubApiRequest } from '../src/mocks/gh-api-request';
import { isGetVariantAnalysisRequest } from '../src/mocks/gh-api-request';
import { VariantAnalysis } from '../src/remote-queries/gh-api/variant-analysis';
import { RepositoryWithMetadata } from '../src/remote-queries/gh-api/repository';
import { getFiles } from "./util/files";
import type { GitHubApiRequest } from "../src/mocks/gh-api-request";
import { isGetVariantAnalysisRequest } from "../src/mocks/gh-api-request";
import { VariantAnalysis } from "../src/remote-queries/gh-api/variant-analysis";
import { RepositoryWithMetadata } from "../src/remote-queries/gh-api/repository";
const extensionDirectory = path.resolve(__dirname, '..');
const scenariosDirectory = path.resolve(extensionDirectory, 'src/mocks/scenarios');
const extensionDirectory = path.resolve(__dirname, "..");
const scenariosDirectory = path.resolve(
extensionDirectory,
"src/mocks/scenarios",
);
// Make sure we don't run into rate limits by automatically waiting until we can
// make another request.
@@ -35,25 +38,36 @@ const auth = process.env.GITHUB_TOKEN;
const octokit = new MyOctokit({
auth,
throttle: {
onRateLimit: (retryAfter: number, options: any, octokit: Octokit): boolean => {
onRateLimit: (
retryAfter: number,
options: any,
octokit: Octokit,
): boolean => {
octokit.log.warn(
`Request quota exhausted for request ${options.method} ${options.url}. Retrying after ${retryAfter} seconds!`
`Request quota exhausted for request ${options.method} ${options.url}. Retrying after ${retryAfter} seconds!`,
);
return true;
},
onSecondaryRateLimit: (_retryAfter: number, options: any, octokit: Octokit): void => {
onSecondaryRateLimit: (
_retryAfter: number,
options: any,
octokit: Octokit,
): void => {
octokit.log.warn(
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
`SecondaryRateLimit detected for request ${options.method} ${options.url}`,
);
},
}
},
});
const repositories = new Map<number, RestEndpointMethodTypes['repos']['get']['response']['data']>();
const repositories = new Map<
number,
RestEndpointMethodTypes["repos"]["get"]["response"]["data"]
>();
async function addFieldsToRepository(repository: RepositoryWithMetadata) {
if (!repositories.has(repository.id)) {
const [owner, repo] = repository.full_name.split('/');
const [owner, repo] = repository.full_name.split("/");
const apiRepository = await octokit.repos.get({
owner,
@@ -71,12 +85,12 @@ async function addFieldsToRepository(repository: RepositoryWithMetadata) {
async function addFieldsToScenarios() {
if (!(await fs.pathExists(scenariosDirectory))) {
console.error('Scenarios directory does not exist: ' + scenariosDirectory);
console.error("Scenarios directory does not exist: " + scenariosDirectory);
return;
}
for await (const file of getFiles(scenariosDirectory)) {
if (!file.endsWith('.json')) {
if (!file.endsWith(".json")) {
continue;
}
@@ -86,11 +100,13 @@ async function addFieldsToScenarios() {
continue;
}
if (!data.response.body || !('controller_repo' in data.response.body)) {
if (!data.response.body || !("controller_repo" in data.response.body)) {
continue;
}
console.log(`Adding fields to '${path.relative(scenariosDirectory, file)}'`);
console.log(
`Adding fields to '${path.relative(scenariosDirectory, file)}'`,
);
const variantAnalysis = data.response.body as VariantAnalysis;
@@ -101,19 +117,22 @@ async function addFieldsToScenarios() {
}
if (variantAnalysis.skipped_repositories?.access_mismatch_repos) {
for (const item of variantAnalysis.skipped_repositories.access_mismatch_repos.repositories) {
for (const item of variantAnalysis.skipped_repositories
.access_mismatch_repos.repositories) {
await addFieldsToRepository(item);
}
}
if (variantAnalysis.skipped_repositories?.no_codeql_db_repos) {
for (const item of variantAnalysis.skipped_repositories.no_codeql_db_repos.repositories) {
for (const item of variantAnalysis.skipped_repositories.no_codeql_db_repos
.repositories) {
await addFieldsToRepository(item);
}
}
if (variantAnalysis.skipped_repositories?.over_limit_repos) {
for (const item of variantAnalysis.skipped_repositories.over_limit_repos.repositories) {
for (const item of variantAnalysis.skipped_repositories.over_limit_repos
.repositories) {
await addFieldsToRepository(item);
}
}
@@ -122,7 +141,7 @@ async function addFieldsToScenarios() {
}
}
addFieldsToScenarios().catch(e => {
addFieldsToScenarios().catch((e) => {
console.error(e);
process.exit(2);
});

View File

@@ -1,50 +1,53 @@
/**
* This scripts helps after recording a scenario to be used for replaying
* with the mock GitHub API server.
*
*
* Once the scenario has been recorded, it's often useful to remove some of
* the requests to speed up the replay, particularly ones that fetch the
* variant analysis status. Once some of the requests have manually been
* the requests to speed up the replay, particularly ones that fetch the
* variant analysis status. Once some of the requests have manually been
* removed, this script can be used to update the numbering of the files.
*
*
* Usage: npx ts-node scripts/fix-scenario-file-numbering.ts <scenario-name>
*/
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
if (process.argv.length !== 3) {
console.error('Expected 1 argument - the scenario name');
console.error("Expected 1 argument - the scenario name");
}
const scenarioName = process.argv[2];
const extensionDirectory = path.resolve(__dirname, '..');
const scenariosDirectory = path.resolve(extensionDirectory, 'src/mocks/scenarios');
const extensionDirectory = path.resolve(__dirname, "..");
const scenariosDirectory = path.resolve(
extensionDirectory,
"src/mocks/scenarios",
);
const scenarioDirectory = path.resolve(scenariosDirectory, scenarioName);
async function fixScenarioFiles() {
console.log(scenarioDirectory);
if (!(await fs.pathExists(scenarioDirectory))) {
console.error('Scenario directory does not exist: ' + scenarioDirectory);
console.error("Scenario directory does not exist: " + scenarioDirectory);
return;
}
const files = await fs.readdir(scenarioDirectory);
const orderedFiles = files.sort((a, b) => {
const aNum = parseInt(a.split('-')[0]);
const bNum = parseInt(b.split('-')[0]);
const aNum = parseInt(a.split("-")[0]);
const bNum = parseInt(b.split("-")[0]);
return aNum - bNum;
});
let index = 0;
for (const file of orderedFiles) {
const ext = path.extname(file);
if (ext === '.json') {
if (ext === ".json") {
const fileName = path.basename(file, ext);
const fileCurrentIndex = parseInt(fileName.split('-')[0]);
const fileNameWithoutIndex = fileName.split('-')[1];
const fileCurrentIndex = parseInt(fileName.split("-")[0]);
const fileNameWithoutIndex = fileName.split("-")[1];
if (fileCurrentIndex !== index) {
const newFileName = `${index}-${fileNameWithoutIndex}${ext}`;
const oldFilePath = path.join(scenarioDirectory, file);
@@ -52,7 +55,7 @@ async function fixScenarioFiles() {
console.log(`Rename: ${oldFilePath} -> ${newFilePath}`);
await fs.rename(oldFilePath, newFilePath);
if (fileNameWithoutIndex === 'getVariantAnalysisRepoResult') {
if (fileNameWithoutIndex === "getVariantAnalysisRepoResult") {
const oldZipFileName = `${fileCurrentIndex}-getVariantAnalysisRepoResult.body.zip`;
const newZipFileName = `${index}-getVariantAnalysisRepoResult.body.zip`;
const oldZipFilePath = path.join(scenarioDirectory, oldZipFileName);
@@ -72,7 +75,7 @@ async function fixScenarioFiles() {
}
}
fixScenarioFiles().catch(e => {
fixScenarioFiles().catch((e) => {
console.error(e);
process.exit(2);
});

View File

@@ -1,31 +1,36 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
import Ajv from 'ajv';
import * as tsj from 'ts-json-schema-generator';
import Ajv from "ajv";
import * as tsj from "ts-json-schema-generator";
import { getFiles } from './util/files';
import { getFiles } from "./util/files";
const extensionDirectory = path.resolve(__dirname, '..');
const rootDirectory = path.resolve(extensionDirectory, '../..');
const scenariosDirectory = path.resolve(extensionDirectory, 'src/mocks/scenarios');
const extensionDirectory = path.resolve(__dirname, "..");
const rootDirectory = path.resolve(extensionDirectory, "../..");
const scenariosDirectory = path.resolve(
extensionDirectory,
"src/mocks/scenarios",
);
const debug = process.env.RUNNER_DEBUG || process.argv.includes('--debug');
const debug = process.env.RUNNER_DEBUG || process.argv.includes("--debug");
async function lintScenarios() {
const schema = tsj.createGenerator({
path: path.resolve(extensionDirectory, 'src/mocks/gh-api-request.ts'),
tsconfig: path.resolve(extensionDirectory, 'tsconfig.json'),
type: 'GitHubApiRequest',
skipTypeCheck: true,
topRef: true,
additionalProperties: true,
}).createSchema('GitHubApiRequest');
const schema = tsj
.createGenerator({
path: path.resolve(extensionDirectory, "src/mocks/gh-api-request.ts"),
tsconfig: path.resolve(extensionDirectory, "tsconfig.json"),
type: "GitHubApiRequest",
skipTypeCheck: true,
topRef: true,
additionalProperties: true,
})
.createSchema("GitHubApiRequest");
const ajv = new Ajv();
if (!ajv.validateSchema(schema)) {
throw new Error('Invalid schema: ' + ajv.errorsText());
throw new Error("Invalid schema: " + ajv.errorsText());
}
const validate = await ajv.compile(schema);
@@ -33,23 +38,27 @@ async function lintScenarios() {
let invalidFiles = 0;
if (!(await fs.pathExists(scenariosDirectory))) {
console.error('Scenarios directory does not exist: ' + scenariosDirectory);
console.error("Scenarios directory does not exist: " + scenariosDirectory);
// Do not exit with a non-zero status code, as this is not a fatal error.
return;
}
for await (const file of getFiles(scenariosDirectory)) {
if (!file.endsWith('.json')) {
if (!file.endsWith(".json")) {
continue;
}
const contents = await fs.readFile(file, 'utf8');
const contents = await fs.readFile(file, "utf8");
const data = JSON.parse(contents);
if (!validate(data)) {
validate.errors?.forEach(error => {
validate.errors?.forEach((error) => {
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
console.log(`::error file=${path.relative(rootDirectory, file)}::${error.instancePath}: ${error.message}`);
console.log(
`::error file=${path.relative(rootDirectory, file)}::${
error.instancePath
}: ${error.message}`,
);
});
invalidFiles++;
} else if (debug) {
@@ -62,7 +71,7 @@ async function lintScenarios() {
}
}
lintScenarios().catch(e => {
lintScenarios().catch((e) => {
console.error(e);
process.exit(2);
});

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
// https://stackoverflow.com/a/45130990
export async function* getFiles(dir: string): AsyncGenerator<string> {

View File

@@ -6,12 +6,16 @@ import {
Uri,
WebviewPanelOptions,
WebviewOptions,
} from 'vscode';
import * as path from 'path';
} from "vscode";
import * as path from "path";
import { DisposableObject, DisposeHandler } from './pure/disposable-object';
import { tmpDir } from './helpers';
import { getHtmlForWebview, WebviewMessage, WebviewView } from './interface-utils';
import { DisposableObject, DisposeHandler } from "./pure/disposable-object";
import { tmpDir } from "./helpers";
import {
getHtmlForWebview,
WebviewMessage,
WebviewView,
} from "./interface-utils";
export type WebviewPanelConfig = {
viewId: string;
@@ -20,18 +24,19 @@ export type WebviewPanelConfig = {
view: WebviewView;
preserveFocus?: boolean;
additionalOptions?: WebviewPanelOptions & WebviewOptions;
}
};
export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMessage extends WebviewMessage> extends DisposableObject {
export abstract class AbstractWebview<
ToMessage extends WebviewMessage,
FromMessage extends WebviewMessage,
> extends DisposableObject {
protected panel: WebviewPanel | undefined;
protected panelLoaded = false;
protected panelLoadedCallBacks: (() => void)[] = [];
private panelResolves?: Array<(panel: WebviewPanel) => void>;
constructor(
protected readonly ctx: ExtensionContext
) {
constructor(protected readonly ctx: ExtensionContext) {
super();
}
@@ -78,9 +83,9 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
localResourceRoots: [
...(config.additionalOptions?.localResourceRoots ?? []),
Uri.file(tmpDir.name),
Uri.file(path.join(ctx.extensionPath, 'out'))
Uri.file(path.join(ctx.extensionPath, "out")),
],
}
},
);
this.panel = panel;
@@ -101,8 +106,8 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
this.onPanelDispose();
},
null,
this.ctx.subscriptions
)
this.ctx.subscriptions,
),
);
panel.webview.html = getHtmlForWebview(
@@ -111,18 +116,20 @@ export abstract class AbstractWebview<ToMessage extends WebviewMessage, FromMess
config.view,
{
allowInlineStyles: true,
}
},
);
this.push(
panel.webview.onDidReceiveMessage(
async (e) => this.onMessage(e),
undefined,
this.ctx.subscriptions
)
this.ctx.subscriptions,
),
);
}
protected abstract getPanelConfig(): WebviewPanelConfig | Promise<WebviewPanelConfig>;
protected abstract getPanelConfig():
| WebviewPanelConfig
| Promise<WebviewPanelConfig>;
protected abstract onPanelDispose(): void;

View File

@@ -1,11 +1,11 @@
import * as fs from 'fs-extra';
import * as unzipper from 'unzipper';
import * as vscode from 'vscode';
import { logger } from './logging';
import * as fs from "fs-extra";
import * as unzipper from "unzipper";
import * as vscode from "vscode";
import { logger } from "./logging";
// All path operations in this file must be on paths *within* the zip
// archive.
import * as _path from 'path';
import * as _path from "path";
const path = _path.posix;
export class File implements vscode.FileStat {
@@ -72,19 +72,20 @@ export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
// Since we will use an authority component, we add a leading slash if necessary
// (paths on Windows usually start with the drive letter).
let sourceArchiveZipPathStartIndex: number;
if (encodedPath.startsWith('/')) {
if (encodedPath.startsWith("/")) {
sourceArchiveZipPathStartIndex = 0;
} else {
encodedPath = '/' + encodedPath;
encodedPath = "/" + encodedPath;
sourceArchiveZipPathStartIndex = 1;
}
// The authority component of the URI records the 0-based inclusive start and exclusive end index
// of the source archive zip path within the path component of the resulting URI.
// This lets us separate the paths, ignoring the leading slash if we added one.
const sourceArchiveZipPathEndIndex = sourceArchiveZipPathStartIndex + sourceArchiveZipPath.length;
const sourceArchiveZipPathEndIndex =
sourceArchiveZipPathStartIndex + sourceArchiveZipPath.length;
const authority = `${sourceArchiveZipPathStartIndex}-${sourceArchiveZipPathEndIndex}`;
return vscode.Uri.parse(zipArchiveScheme + ':/', true).with({
return vscode.Uri.parse(zipArchiveScheme + ":/", true).with({
path: encodedPath,
authority,
});
@@ -99,7 +100,7 @@ export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
export function encodeArchiveBasePath(sourceArchiveZipPath: string) {
return encodeSourceArchiveUri({
sourceArchiveZipPath,
pathWithinSourceArchive: ''
pathWithinSourceArchive: "",
});
}
@@ -107,7 +108,9 @@ const sourceArchiveUriAuthorityPattern = /^(\d+)-(\d+)$/;
class InvalidSourceArchiveUriError extends Error {
constructor(uri: vscode.Uri) {
super(`Can't decode uri ${uri}: authority should be of the form startIndex-endIndex (where both indices are integers).`);
super(
`Can't decode uri ${uri}: authority should be of the form startIndex-endIndex (where both indices are integers).`,
);
}
}
@@ -115,22 +118,26 @@ class InvalidSourceArchiveUriError extends Error {
export function decodeSourceArchiveUri(uri: vscode.Uri): ZipFileReference {
if (!uri.authority) {
// Uri is malformed, but this is recoverable
void logger.log(`Warning: ${new InvalidSourceArchiveUriError(uri).message}`);
void logger.log(
`Warning: ${new InvalidSourceArchiveUriError(uri).message}`,
);
return {
pathWithinSourceArchive: '/',
sourceArchiveZipPath: uri.path
pathWithinSourceArchive: "/",
sourceArchiveZipPath: uri.path,
};
}
const match = sourceArchiveUriAuthorityPattern.exec(uri.authority);
if (match === null)
throw new InvalidSourceArchiveUriError(uri);
if (match === null) throw new InvalidSourceArchiveUriError(uri);
const zipPathStartIndex = parseInt(match[1]);
const zipPathEndIndex = parseInt(match[2]);
if (isNaN(zipPathStartIndex) || isNaN(zipPathEndIndex))
throw new InvalidSourceArchiveUriError(uri);
return {
pathWithinSourceArchive: uri.path.substring(zipPathEndIndex) || '/',
sourceArchiveZipPath: uri.path.substring(zipPathStartIndex, zipPathEndIndex),
pathWithinSourceArchive: uri.path.substring(zipPathEndIndex) || "/",
sourceArchiveZipPath: uri.path.substring(
zipPathStartIndex,
zipPathEndIndex,
),
};
}
@@ -139,7 +146,7 @@ export function decodeSourceArchiveUri(uri: vscode.Uri): ZipFileReference {
*/
function ensureFile(map: DirectoryHierarchyMap, file: string) {
const dirname = path.dirname(file);
if (dirname === '.') {
if (dirname === ".") {
const error = `Ill-formed path ${file} in zip archive (expected absolute path)`;
void logger.log(error);
throw new Error(error);
@@ -154,8 +161,9 @@ function ensureFile(map: DirectoryHierarchyMap, file: string) {
function ensureDir(map: DirectoryHierarchyMap, dir: string) {
const parent = path.dirname(dir);
if (!map.has(dir)) {
map.set(dir, new Map);
if (dir !== parent) { // not the root directory
map.set(dir, new Map());
if (dir !== parent) {
// not the root directory
ensureDir(map, parent);
map.get(parent)!.set(path.basename(dir), vscode.FileType.Directory);
}
@@ -168,16 +176,23 @@ type Archive = {
};
async function parse_zip(zipPath: string): Promise<Archive> {
if (!await fs.pathExists(zipPath))
if (!(await fs.pathExists(zipPath)))
throw vscode.FileSystemError.FileNotFound(zipPath);
const archive: Archive = { unzipped: await unzipper.Open.file(zipPath), dirMap: new Map };
archive.unzipped.files.forEach(f => { ensureFile(archive.dirMap, path.resolve('/', f.path)); });
const archive: Archive = {
unzipped: await unzipper.Open.file(zipPath),
dirMap: new Map(),
};
archive.unzipped.files.forEach((f) => {
ensureFile(archive.dirMap, path.resolve("/", f.path));
});
return archive;
}
export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
private readOnlyError = vscode.FileSystemError.NoPermissions('write operation attempted, but source archive filesystem is readonly');
private archives: Map<string, Promise<Archive>> = new Map;
private readOnlyError = vscode.FileSystemError.NoPermissions(
"write operation attempted, but source archive filesystem is readonly",
);
private archives: Map<string, Promise<Archive>> = new Map();
private async getArchive(zipPath: string): Promise<Archive> {
if (!this.archives.has(zipPath)) {
@@ -186,8 +201,7 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
return await this.archives.get(zipPath)!;
}
root = new Directory('');
root = new Directory("");
// metadata
@@ -199,7 +213,8 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
const contents = archive.dirMap.get(ref.pathWithinSourceArchive);
const result = contents === undefined ? undefined : Array.from(contents.entries());
const result =
contents === undefined ? undefined : Array.from(contents.entries());
if (result === undefined) {
throw vscode.FileSystemError.FileNotFound(uri);
}
@@ -218,11 +233,19 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// write operations, all disabled
writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { create: boolean; overwrite: boolean }): void {
writeFile(
_uri: vscode.Uri,
_content: Uint8Array,
_options: { create: boolean; overwrite: boolean },
): void {
throw this.readOnlyError;
}
rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { overwrite: boolean }): void {
rename(
_oldUri: vscode.Uri,
_newUri: vscode.Uri,
_options: { overwrite: boolean },
): void {
throw this.readOnlyError;
}
@@ -244,18 +267,18 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
// use '/' as path separator throughout
const reqPath = ref.pathWithinSourceArchive;
const file = archive.unzipped.files.find(
f => {
const absolutePath = path.resolve('/', f.path);
return absolutePath === reqPath
|| absolutePath === path.join('/src_archive', reqPath);
}
);
const file = archive.unzipped.files.find((f) => {
const absolutePath = path.resolve("/", f.path);
return (
absolutePath === reqPath ||
absolutePath === path.join("/src_archive", reqPath)
);
});
if (file !== undefined) {
if (file.type === 'File') {
if (file.type === "File") {
return new File(reqPath, await file.buffer());
}
else { // file.type === 'Directory'
} else {
// file.type === 'Directory'
// I haven't observed this case in practice. Could it happen
// with a zip file that contains empty directories?
return new Directory(reqPath);
@@ -264,7 +287,11 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
if (archive.dirMap.has(reqPath)) {
return new Directory(reqPath);
}
throw vscode.FileSystemError.FileNotFound(`uri '${uri.toString()}', interpreted as '${reqPath}' in archive '${ref.sourceArchiveZipPath}'`);
throw vscode.FileSystemError.FileNotFound(
`uri '${uri.toString()}', interpreted as '${reqPath}' in archive '${
ref.sourceArchiveZipPath
}'`,
);
}
private async _lookupAsFile(uri: vscode.Uri): Promise<File> {
@@ -279,11 +306,14 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
private _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._emitter.event;
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> =
this._emitter.event;
watch(_resource: vscode.Uri): vscode.Disposable {
// ignore, fires for all changes...
return new vscode.Disposable(() => { /**/ });
return new vscode.Disposable(() => {
/**/
});
}
}
@@ -295,15 +325,17 @@ export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
* (cf. https://www.ietf.org/rfc/rfc2396.txt (Appendix A, page 26) for
* the fact that hyphens are allowed in uri schemes)
*/
export const zipArchiveScheme = 'codeql-zip-archive';
export const zipArchiveScheme = "codeql-zip-archive";
export function activate(ctx: vscode.ExtensionContext) {
ctx.subscriptions.push(vscode.workspace.registerFileSystemProvider(
zipArchiveScheme,
new ArchiveFileSystemProvider(),
{
isCaseSensitive: true,
isReadonly: true,
}
));
ctx.subscriptions.push(
vscode.workspace.registerFileSystemProvider(
zipArchiveScheme,
new ArchiveFileSystemProvider(),
{
isCaseSensitive: true,
isReadonly: true,
},
),
);
}

View File

@@ -11,17 +11,21 @@ import {
TextEditorSelectionChangeKind,
Location,
Range,
Uri
} from 'vscode';
import * as path from 'path';
Uri,
} from "vscode";
import * as path from "path";
import { DatabaseItem } from './databases';
import { UrlValue, BqrsId } from './pure/bqrs-cli-types';
import { showLocation } from './interface-utils';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
import { DatabaseItem } from "./databases";
import { UrlValue, BqrsId } from "./pure/bqrs-cli-types";
import { showLocation } from "./interface-utils";
import {
isStringLoc,
isWholeFileLoc,
isLineColumnLoc,
} from "./pure/bqrs-utils";
import { commandRunner } from "./commandRunner";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogErrorMessage } from "./helpers";
export interface AstItem {
id: BqrsId;
@@ -36,23 +40,25 @@ export interface ChildAstItem extends AstItem {
parent: ChildAstItem | AstItem;
}
class AstViewerDataProvider extends DisposableObject implements TreeDataProvider<AstItem> {
class AstViewerDataProvider
extends DisposableObject
implements TreeDataProvider<AstItem>
{
public roots: AstItem[] = [];
public db: DatabaseItem | undefined;
private _onDidChangeTreeData =
this.push(new EventEmitter<AstItem | undefined>());
private _onDidChangeTreeData = this.push(
new EventEmitter<AstItem | undefined>(),
);
readonly onDidChangeTreeData: Event<AstItem | undefined> =
this._onDidChangeTreeData.event;
constructor() {
super();
this.push(
commandRunner('codeQLAstViewer.gotoCode',
async (item: AstItem) => {
await showLocation(item.fileLocation);
})
commandRunner("codeQLAstViewer.gotoCode", async (item: AstItem) => {
await showLocation(item.fileLocation);
}),
);
}
@@ -61,7 +67,7 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
}
getChildren(item?: AstItem): ProviderResult<AstItem[]> {
const children = item ? item.children : this.roots;
return children.sort((c1, c2) => (c1.order - c2.order));
return children.sort((c1, c2) => c1.order - c2.order);
}
getParent(item: ChildAstItem): ProviderResult<AstItem> {
@@ -74,22 +80,22 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
const state = item.children.length
? TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None;
const treeItem = new TreeItem(item.label || '', state);
treeItem.description = line ? `Line ${line}` : '';
const treeItem = new TreeItem(item.label || "", state);
treeItem.description = line ? `Line ${line}` : "";
treeItem.id = String(item.id);
treeItem.tooltip = `${treeItem.description} ${treeItem.label}`;
treeItem.command = {
command: 'codeQLAstViewer.gotoCode',
title: 'Go To Code',
command: "codeQLAstViewer.gotoCode",
title: "Go To Code",
tooltip: `Go To ${item.location}`,
arguments: [item]
arguments: [item],
};
return treeItem;
}
private extractLineInfo(loc?: UrlValue) {
if (!loc) {
return '';
return "";
} else if (isStringLoc(loc)) {
return loc;
} else if (isWholeFileLoc(loc)) {
@@ -97,7 +103,7 @@ class AstViewerDataProvider extends DisposableObject implements TreeDataProvider
} else if (isLineColumnLoc(loc)) {
return loc.startLine;
} else {
return '';
return "";
}
}
}
@@ -111,19 +117,21 @@ export class AstViewer extends DisposableObject {
super();
this.treeDataProvider = new AstViewerDataProvider();
this.treeView = window.createTreeView('codeQLAstViewer', {
this.treeView = window.createTreeView("codeQLAstViewer", {
treeDataProvider: this.treeDataProvider,
showCollapseAll: true
showCollapseAll: true,
});
this.push(this.treeView);
this.push(this.treeDataProvider);
this.push(
commandRunner('codeQLAstViewer.clear', async () => {
commandRunner("codeQLAstViewer.clear", async () => {
this.clear();
})
}),
);
this.push(
window.onDidChangeTextEditorSelection(this.updateTreeSelection, this),
);
this.push(window.onDidChangeTextEditorSelection(this.updateTreeSelection, this));
}
updateRoots(roots: AstItem[], db: DatabaseItem, fileUri: Uri) {
@@ -135,8 +143,10 @@ export class AstViewer extends DisposableObject {
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
);
}
@@ -149,7 +159,10 @@ export class AstViewer extends DisposableObject {
// range that contains the selection.
// Some nodes do not have a location, but their children might, so must
// recurse though location-less AST nodes to see if children are correct.
function findBest(selectedRange: Range, items?: AstItem[]): AstItem | undefined {
function findBest(
selectedRange: Range,
items?: AstItem[],
): AstItem | undefined {
if (!items || !items.length) {
return;
}
@@ -188,8 +201,10 @@ export class AstViewer extends DisposableObject {
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(targetItem)?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
);
}
}

View File

@@ -1,13 +1,13 @@
import * as vscode from 'vscode';
import * as Octokit from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
import * as vscode from "vscode";
import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
const GITHUB_AUTH_PROVIDER_ID = 'github';
const GITHUB_AUTH_PROVIDER_ID = "github";
// We need 'repo' scope for triggering workflows and 'gist' scope for exporting results to Gist.
// For a comprehensive list of scopes, see:
// https://docs.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps
const SCOPES = ['repo', 'gist'];
const SCOPES = ["repo", "gist"];
/**
* Handles authentication to GitHub, using the VS Code [authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication).
@@ -18,7 +18,7 @@ export class Credentials {
// Explicitly make the constructor private, so that we can't accidentally call the constructor from outside the class
// without also initializing the class.
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() { }
private constructor() {}
/**
* Initializes an instance of credentials with an octokit instance.
@@ -29,7 +29,9 @@ export class Credentials {
* @param context The extension context.
* @returns An instance of credentials.
*/
static async initialize(context: vscode.ExtensionContext): Promise<Credentials> {
static async initialize(
context: vscode.ExtensionContext,
): Promise<Credentials> {
const c = new Credentials();
c.registerListeners(context);
c.octokit = await c.createOctokit(false);
@@ -50,17 +52,24 @@ export class Credentials {
return c;
}
private async createOctokit(createIfNone: boolean, overrideToken?: string): Promise<Octokit.Octokit | undefined> {
private async createOctokit(
createIfNone: boolean,
overrideToken?: string,
): Promise<Octokit.Octokit | undefined> {
if (overrideToken) {
return new Octokit.Octokit({ auth: overrideToken, retry });
}
const session = await vscode.authentication.getSession(GITHUB_AUTH_PROVIDER_ID, SCOPES, { createIfNone });
const session = await vscode.authentication.getSession(
GITHUB_AUTH_PROVIDER_ID,
SCOPES,
{ createIfNone },
);
if (session) {
return new Octokit.Octokit({
auth: session.accessToken,
retry
retry,
});
} else {
return undefined;
@@ -69,11 +78,13 @@ export class Credentials {
registerListeners(context: vscode.ExtensionContext): void {
// Sessions are changed when a user logs in or logs out.
context.subscriptions.push(vscode.authentication.onDidChangeSessions(async e => {
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
this.octokit = await this.createOctokit(false);
}
}));
context.subscriptions.push(
vscode.authentication.onDidChangeSessions(async (e) => {
if (e.provider.id === GITHUB_AUTH_PROVIDER_ID) {
this.octokit = await this.createOctokit(false);
}
}),
);
}
/**
@@ -91,7 +102,7 @@ export class Credentials {
if (!this.octokit) {
if (requireAuthentication) {
throw new Error('Did not initialize Octokit.');
throw new Error("Did not initialize Octokit.");
}
// We don't want to set this in this.octokit because that would prevent

View File

@@ -1,25 +1,30 @@
import * as semver from 'semver';
import { runCodeQlCliCommand } from './cli';
import { Logger } from './logging';
import { getErrorMessage } from './pure/helpers-pure';
import * as semver from "semver";
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "./logging";
import { getErrorMessage } from "./pure/helpers-pure";
/**
* Get the version of a CodeQL CLI.
*/
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<semver.SemVer | undefined> {
export async function getCodeQlCliVersion(
codeQlPath: string,
logger: Logger,
): Promise<semver.SemVer | undefined> {
try {
const output: string = await runCodeQlCliCommand(
codeQlPath,
['version'],
['--format=terse'],
'Checking CodeQL version',
logger
["version"],
["--format=terse"],
"Checking CodeQL version",
logger,
);
return semver.parse(output.trim()) || undefined;
} catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
// Either way, we can't determine compatibility.
void logger.log(`Failed to run 'codeql version'. Reason: ${getErrorMessage(e)}`);
void logger.log(
`Failed to run 'codeql version'. Reason: ${getErrorMessage(e)}`,
);
return undefined;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,12 @@ import {
window as Window,
commands,
Disposable,
ProgressLocation
} from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
import { logger } from './logging';
import { getErrorMessage, getErrorStack } from './pure/helpers-pure';
import { telemetryListener } from './telemetry';
ProgressLocation,
} from "vscode";
import { showAndLogErrorMessage, showAndLogWarningMessage } from "./helpers";
import { logger } from "./logging";
import { getErrorMessage, getErrorStack } from "./pure/helpers-pure";
import { telemetryListener } from "./telemetry";
export class UserCancellationException extends Error {
/**
@@ -67,7 +67,7 @@ export type ProgressTask<R> = (
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
type NoProgressTask = ((...args: any[]) => Promise<any>);
type NoProgressTask = (...args: any[]) => Promise<any>;
/**
* This mediates between the kind of progress callbacks we want to
@@ -91,15 +91,18 @@ export function withProgress<R>(
...args: any[]
): Thenable<R> {
let progressAchieved = 0;
return Window.withProgress(options,
(progress, token) => {
return task(p => {
return Window.withProgress(options, (progress, token) => {
return task(
(p) => {
const { message, step, maxStep } = p;
const increment = 100 * (step - progressAchieved) / maxStep;
const increment = (100 * (step - progressAchieved)) / maxStep;
progressAchieved = step;
progress.report({ message, increment });
}, token, ...args);
});
},
token,
...args,
);
});
}
/**
@@ -138,7 +141,7 @@ export function commandRunner(
? `${errorMessage}\n${errorStack}`
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
fullMessage
fullMessage,
});
}
return undefined;
@@ -163,14 +166,14 @@ export function commandRunnerWithProgress<R>(
commandId: string,
task: ProgressTask<R>,
progressOptions: Partial<ProgressOptions>,
outputLogger = logger
outputLogger = logger,
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const startTime = Date.now();
let error: Error | undefined;
const progressOptionsWithDefaults = {
location: ProgressLocation.Notification,
...progressOptions
...progressOptions,
};
try {
return await withProgress(progressOptionsWithDefaults, task, ...args);
@@ -192,7 +195,7 @@ export function commandRunnerWithProgress<R>(
: errorMessage;
void showAndLogErrorMessage(errorMessage, {
outputLogger,
fullMessage
fullMessage,
});
}
return undefined;
@@ -216,23 +219,26 @@ export function reportStreamProgress(
readable: NodeJS.ReadableStream,
messagePrefix: string,
totalNumBytes?: number,
progress?: ProgressCallback
progress?: ProgressCallback,
) {
if (progress && totalNumBytes) {
let numBytesDownloaded = 0;
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const bytesToDisplayMB = (numBytes: number): string =>
`${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = () => {
progress({
step: numBytesDownloaded,
maxStep: totalNumBytes,
message: `${messagePrefix} [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`,
message: `${messagePrefix} [${bytesToDisplayMB(
numBytesDownloaded,
)} of ${bytesToDisplayMB(totalNumBytes)}]`,
});
};
// Display the progress straight away rather than waiting for the first chunk.
updateProgress();
readable.on('data', data => {
readable.on("data", (data) => {
numBytesDownloaded += data.length;
updateProgress();
});

View File

@@ -1,5 +1,5 @@
import { Disposable } from '../pure/disposable-object';
import { AppEventEmitter } from './events';
import { Disposable } from "../pure/disposable-object";
import { AppEventEmitter } from "./events";
export interface App {
createEventEmitter<T>(): AppEventEmitter<T>;

View File

@@ -1,4 +1,4 @@
import { Disposable } from '../pure/disposable-object';
import { Disposable } from "../pure/disposable-object";
export interface AppEvent<T> {
(listener: (event: T) => void): Disposable;

View File

@@ -5,12 +5,11 @@ export class ValueResult<TValue> {
private constructor(
private readonly errorMsgs: string[],
private readonly val?: TValue,
) {
}
) {}
public static ok<TValue>(value: TValue): ValueResult<TValue> {
if (value === undefined) {
throw new Error('Value must be set for successful result');
throw new Error("Value must be set for successful result");
}
return new ValueResult([], value);
@@ -18,7 +17,9 @@ export class ValueResult<TValue> {
public static fail<TValue>(errorMsgs: string[]): ValueResult<TValue> {
if (errorMsgs.length === 0) {
throw new Error('At least one error message must be set for a failed result');
throw new Error(
"At least one error message must be set for a failed result",
);
}
return new ValueResult<TValue>(errorMsgs, undefined);
@@ -34,7 +35,7 @@ export class ValueResult<TValue> {
public get errors(): string[] {
if (!this.errorMsgs) {
throw new Error('Cannot get error for successful result');
throw new Error("Cannot get error for successful result");
}
return this.errorMsgs;
@@ -42,7 +43,7 @@ export class ValueResult<TValue> {
public get value(): TValue {
if (this.val === undefined) {
throw new Error('Cannot get value for unsuccessful result');
throw new Error("Cannot get value for unsuccessful result");
}
return this.val;

View File

@@ -1,7 +1,6 @@
import * as vscode from 'vscode';
import { AppEventEmitter } from '../events';
import * as vscode from "vscode";
import { AppEventEmitter } from "../events";
export class VSCodeAppEventEmitter<T>
extends vscode.EventEmitter<T>
implements AppEventEmitter<T> {
}
implements AppEventEmitter<T> {}

View File

@@ -1,14 +1,13 @@
import * as vscode from 'vscode';
import { Disposable } from '../../pure/disposable-object';
import { App, AppMode } from '../app';
import { AppEventEmitter } from '../events';
import { VSCodeAppEventEmitter } from './events';
import * as vscode from "vscode";
import { Disposable } from "../../pure/disposable-object";
import { App, AppMode } from "../app";
import { AppEventEmitter } from "../events";
import { VSCodeAppEventEmitter } from "./events";
export class ExtensionApp implements App {
public constructor(
public readonly extensionContext: vscode.ExtensionContext
) {
}
public readonly extensionContext: vscode.ExtensionContext,
) {}
public get extensionPath(): string {
return this.extensionContext.extensionPath;

View File

@@ -1,30 +1,34 @@
import {
ExtensionContext,
ViewColumn,
} from 'vscode';
import { ExtensionContext, ViewColumn } from "vscode";
import {
FromCompareViewMessage,
ToCompareViewMessage,
QueryCompareResult,
} from '../pure/interface-types';
import { Logger } from '../logging';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
import { jumpToLocation } from '../interface-utils';
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../pure/bqrs-cli-types';
import resultsDiff from './resultsDiff';
import { CompletedLocalQueryInfo } from '../query-results';
import { getErrorMessage } from '../pure/helpers-pure';
import { HistoryItemLabelProvider } from '../history-item-label-provider';
import { AbstractWebview, WebviewPanelConfig } from '../abstract-webview';
} from "../pure/interface-types";
import { Logger } from "../logging";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager } from "../databases";
import { jumpToLocation } from "../interface-utils";
import {
transformBqrsResultSet,
RawResultSet,
BQRSInfo,
} from "../pure/bqrs-cli-types";
import resultsDiff from "./resultsDiff";
import { CompletedLocalQueryInfo } from "../query-results";
import { getErrorMessage } from "../pure/helpers-pure";
import { HistoryItemLabelProvider } from "../history-item-label-provider";
import { AbstractWebview, WebviewPanelConfig } from "../abstract-webview";
interface ComparePair {
from: CompletedLocalQueryInfo;
to: CompletedLocalQueryInfo;
}
export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompareViewMessage> {
export class CompareView extends AbstractWebview<
ToCompareViewMessage,
FromCompareViewMessage
> {
private comparePair: ComparePair | undefined;
constructor(
@@ -34,8 +38,8 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
private logger: Logger,
private labelProvider: HistoryItemLabelProvider,
private showQueryResultsCallback: (
item: CompletedLocalQueryInfo
) => Promise<void>
item: CompletedLocalQueryInfo,
) => Promise<void>,
) {
super(ctx);
}
@@ -43,7 +47,7 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
async showResults(
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
selectedResultSetName?: string
selectedResultSetName?: string,
) {
this.comparePair = { from, to };
const panel = await this.getPanel();
@@ -55,11 +59,7 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
currentResultSetName,
fromResultSet,
toResultSet,
] = await this.findCommonResultSetNames(
from,
to,
selectedResultSetName
);
] = await this.findCommonResultSetNames(from, to, selectedResultSetName);
if (currentResultSetName) {
let rows: QueryCompareResult | undefined;
let message: string | undefined;
@@ -70,7 +70,7 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
}
await this.postMessage({
t: 'setComparisons',
t: "setComparisons",
stats: {
fromQuery: {
// since we split the description into several rows
@@ -98,11 +98,11 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
protected getPanelConfig(): WebviewPanelConfig {
return {
viewId: 'compareView',
title: 'Compare CodeQL Query Results',
viewId: "compareView",
title: "Compare CodeQL Query Results",
viewColumn: ViewColumn.Active,
preserveFocus: true,
view: 'compare',
view: "compare",
};
}
@@ -112,19 +112,19 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
protected async onMessage(msg: FromCompareViewMessage): Promise<void> {
switch (msg.t) {
case 'viewLoaded':
case "viewLoaded":
this.onWebViewLoaded();
break;
case 'changeCompare':
case "changeCompare":
await this.changeTable(msg.newResultSetName);
break;
case 'viewSourceFile':
case "viewSourceFile":
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
case 'openQuery':
case "openQuery":
await this.openQuery(msg.kind);
break;
}
@@ -133,34 +133,32 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
private async findCommonResultSetNames(
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo,
selectedResultSetName: string | undefined
selectedResultSetName: string | undefined,
): Promise<[string[], string, RawResultSet, RawResultSet]> {
const fromSchemas = await this.cliServer.bqrsInfo(
from.completedQuery.query.resultsPaths.resultsPath
from.completedQuery.query.resultsPaths.resultsPath,
);
const toSchemas = await this.cliServer.bqrsInfo(
to.completedQuery.query.resultsPaths.resultsPath
to.completedQuery.query.resultsPaths.resultsPath,
);
const fromSchemaNames = fromSchemas['result-sets'].map(
(schema) => schema.name
);
const toSchemaNames = toSchemas['result-sets'].map(
(schema) => schema.name
const fromSchemaNames = fromSchemas["result-sets"].map(
(schema) => schema.name,
);
const toSchemaNames = toSchemas["result-sets"].map((schema) => schema.name);
const commonResultSetNames = fromSchemaNames.filter((name) =>
toSchemaNames.includes(name)
toSchemaNames.includes(name),
);
const currentResultSetName =
selectedResultSetName || commonResultSetNames[0];
const fromResultSet = await this.getResultSet(
fromSchemas,
currentResultSetName,
from.completedQuery.query.resultsPaths.resultsPath
from.completedQuery.query.resultsPaths.resultsPath,
);
const toResultSet = await this.getResultSet(
toSchemas,
currentResultSetName,
to.completedQuery.query.resultsPaths.resultsPath
to.completedQuery.query.resultsPaths.resultsPath,
);
return [
commonResultSetNames,
@@ -177,39 +175,36 @@ export class CompareView extends AbstractWebview<ToCompareViewMessage, FromCompa
await this.showResults(
this.comparePair.from,
this.comparePair.to,
newResultSetName
newResultSetName,
);
}
private async getResultSet(
bqrsInfo: BQRSInfo,
resultSetName: string,
resultsPath: string
resultsPath: string,
): Promise<RawResultSet> {
const schema = bqrsInfo['result-sets'].find(
(schema) => schema.name === resultSetName
const schema = bqrsInfo["result-sets"].find(
(schema) => schema.name === resultSetName,
);
if (!schema) {
throw new Error(`Schema ${resultSetName} not found.`);
}
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
resultSetName
);
const chunk = await this.cliServer.bqrsDecode(resultsPath, resultSetName);
return transformBqrsResultSet(schema, chunk);
}
private compareResults(
fromResults: RawResultSet,
toResults: RawResultSet
toResults: RawResultSet,
): QueryCompareResult {
// Only compare columns that have the same name
return resultsDiff(fromResults, toResults);
}
private async openQuery(kind: 'from' | 'to') {
private async openQuery(kind: "from" | "to") {
const toOpen =
kind === 'from' ? this.comparePair?.from : this.comparePair?.to;
kind === "from" ? this.comparePair?.from : this.comparePair?.to;
if (toOpen) {
await this.showQueryResultsCallback(toOpen);
}

View File

@@ -1,5 +1,5 @@
import { RawResultSet } from '../pure/bqrs-cli-types';
import { QueryCompareResult } from '../pure/interface-types';
import { RawResultSet } from "../pure/bqrs-cli-types";
import { QueryCompareResult } from "../pure/interface-types";
/**
* Compare the rows of two queries. Use deep equality to determine if
@@ -21,19 +21,18 @@ import { QueryCompareResult } from '../pure/interface-types';
*/
export default function resultsDiff(
fromResults: RawResultSet,
toResults: RawResultSet
toResults: RawResultSet,
): QueryCompareResult {
if (fromResults.schema.columns.length !== toResults.schema.columns.length) {
throw new Error('CodeQL Compare: Columns do not match.');
throw new Error("CodeQL Compare: Columns do not match.");
}
if (!fromResults.rows.length) {
throw new Error('CodeQL Compare: Source query has no results.');
throw new Error("CodeQL Compare: Source query has no results.");
}
if (!toResults.rows.length) {
throw new Error('CodeQL Compare: Target query has no results.');
throw new Error("CodeQL Compare: Target query has no results.");
}
const results = {
@@ -45,7 +44,7 @@ export default function resultsDiff(
fromResults.rows.length === results.from.length &&
toResults.rows.length === results.to.length
) {
throw new Error('CodeQL Compare: No overlap between the selected queries.');
throw new Error("CodeQL Compare: No overlap between the selected queries.");
}
return results;

View File

@@ -1,8 +1,14 @@
import { DisposableObject } from './pure/disposable-object';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
import { DistributionManager } from './distribution';
import { logger } from './logging';
import { ONE_DAY_IN_MS } from './pure/time';
import { DisposableObject } from "./pure/disposable-object";
import {
workspace,
Event,
EventEmitter,
ConfigurationChangeEvent,
ConfigurationTarget,
} from "vscode";
import { DistributionManager } from "./distribution";
import { logger } from "./logging";
import { ONE_DAY_IN_MS } from "./pure/time";
export const ALL_SETTINGS: Setting[] = [];
@@ -35,58 +41,86 @@ export class Setting {
getValue<T>(): T {
if (this.parent === undefined) {
throw new Error('Cannot get the value of a root setting.');
throw new Error("Cannot get the value of a root setting.");
}
return workspace.getConfiguration(this.parent.qualifiedName).get<T>(this.name)!;
return workspace
.getConfiguration(this.parent.qualifiedName)
.get<T>(this.name)!;
}
updateValue<T>(value: T, target: ConfigurationTarget): Thenable<void> {
if (this.parent === undefined) {
throw new Error('Cannot update the value of a root setting.');
throw new Error("Cannot update the value of a root setting.");
}
return workspace.getConfiguration(this.parent.qualifiedName).update(this.name, value, target);
return workspace
.getConfiguration(this.parent.qualifiedName)
.update(this.name, value, target);
}
inspect<T>(): InspectionResult<T> | undefined {
if (this.parent === undefined) {
throw new Error('Cannot update the value of a root setting.');
throw new Error("Cannot update the value of a root setting.");
}
return workspace.getConfiguration(this.parent.qualifiedName).inspect(this.name);
return workspace
.getConfiguration(this.parent.qualifiedName)
.inspect(this.name);
}
}
export interface InspectionResult<T> {
globalValue?: T;
workspaceValue?: T,
workspaceFolderValue?: T,
workspaceValue?: T;
workspaceFolderValue?: T;
}
const ROOT_SETTING = new Setting('codeQL');
const ROOT_SETTING = new Setting("codeQL");
// Global configuration
const TELEMETRY_SETTING = new Setting('telemetry', ROOT_SETTING);
const AST_VIEWER_SETTING = new Setting('astViewer', ROOT_SETTING);
const GLOBAL_TELEMETRY_SETTING = new Setting('telemetry');
const LOG_INSIGHTS_SETTING = new Setting('logInsights', ROOT_SETTING);
const TELEMETRY_SETTING = new Setting("telemetry", ROOT_SETTING);
const AST_VIEWER_SETTING = new Setting("astViewer", ROOT_SETTING);
const GLOBAL_TELEMETRY_SETTING = new Setting("telemetry");
const LOG_INSIGHTS_SETTING = new Setting("logInsights", ROOT_SETTING);
export const LOG_TELEMETRY = new Setting('logTelemetry', TELEMETRY_SETTING);
export const ENABLE_TELEMETRY = new Setting('enableTelemetry', TELEMETRY_SETTING);
export const LOG_TELEMETRY = new Setting("logTelemetry", TELEMETRY_SETTING);
export const ENABLE_TELEMETRY = new Setting(
"enableTelemetry",
TELEMETRY_SETTING,
);
export const GLOBAL_ENABLE_TELEMETRY = new Setting('enableTelemetry', GLOBAL_TELEMETRY_SETTING);
export const GLOBAL_ENABLE_TELEMETRY = new Setting(
"enableTelemetry",
GLOBAL_TELEMETRY_SETTING,
);
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
const DISTRIBUTION_SETTING = new Setting("cli", ROOT_SETTING);
export const CUSTOM_CODEQL_PATH_SETTING = new Setting(
"executablePath",
DISTRIBUTION_SETTING,
);
const INCLUDE_PRERELEASE_SETTING = new Setting(
"includePrerelease",
DISTRIBUTION_SETTING,
);
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting(
"personalAccessToken",
DISTRIBUTION_SETTING,
);
// Query History configuration
const QUERY_HISTORY_SETTING = new Setting('queryHistory', ROOT_SETTING);
const QUERY_HISTORY_FORMAT_SETTING = new Setting('format', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_TTL = new Setting('ttl', QUERY_HISTORY_SETTING);
const QUERY_HISTORY_SETTING = new Setting("queryHistory", ROOT_SETTING);
const QUERY_HISTORY_FORMAT_SETTING = new Setting(
"format",
QUERY_HISTORY_SETTING,
);
const QUERY_HISTORY_TTL = new Setting("ttl", QUERY_HISTORY_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING];
const DISTRIBUTION_CHANGE_SETTINGS = [
CUSTOM_CODEQL_PATH_SETTING,
INCLUDE_PRERELEASE_SETTING,
PERSONAL_ACCESS_TOKEN_SETTING,
];
export interface DistributionConfig {
readonly customCodeQlPath?: string;
@@ -99,28 +133,47 @@ export interface DistributionConfig {
}
// Query server configuration
const RUNNING_QUERIES_SETTING = new Setting('runningQueries', ROOT_SETTING);
const NUMBER_OF_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_QUERIES_SETTING);
const SAVE_CACHE_SETTING = new Setting('saveCache', RUNNING_QUERIES_SETTING);
const CACHE_SIZE_SETTING = new Setting('cacheSize', RUNNING_QUERIES_SETTING);
const TIMEOUT_SETTING = new Setting('timeout', RUNNING_QUERIES_SETTING);
const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
const MAX_PATHS = new Setting('maxPaths', RUNNING_QUERIES_SETTING);
const RUNNING_TESTS_SETTING = new Setting('runningTests', ROOT_SETTING);
const RESULTS_DISPLAY_SETTING = new Setting('resultsDisplay', ROOT_SETTING);
const RUNNING_QUERIES_SETTING = new Setting("runningQueries", ROOT_SETTING);
const NUMBER_OF_THREADS_SETTING = new Setting(
"numberOfThreads",
RUNNING_QUERIES_SETTING,
);
const SAVE_CACHE_SETTING = new Setting("saveCache", RUNNING_QUERIES_SETTING);
const CACHE_SIZE_SETTING = new Setting("cacheSize", RUNNING_QUERIES_SETTING);
const TIMEOUT_SETTING = new Setting("timeout", RUNNING_QUERIES_SETTING);
const MEMORY_SETTING = new Setting("memory", RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting("debug", RUNNING_QUERIES_SETTING);
const MAX_PATHS = new Setting("maxPaths", RUNNING_QUERIES_SETTING);
const RUNNING_TESTS_SETTING = new Setting("runningTests", ROOT_SETTING);
const RESULTS_DISPLAY_SETTING = new Setting("resultsDisplay", ROOT_SETTING);
export const ADDITIONAL_TEST_ARGUMENTS_SETTING = new Setting('additionalTestArguments', RUNNING_TESTS_SETTING);
export const NUMBER_OF_TEST_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_TESTS_SETTING);
export const MAX_QUERIES = new Setting('maxQueries', RUNNING_QUERIES_SETTING);
export const AUTOSAVE_SETTING = new Setting('autoSave', RUNNING_QUERIES_SETTING);
export const PAGE_SIZE = new Setting('pageSize', RESULTS_DISPLAY_SETTING);
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting('customLogDirectory', RUNNING_QUERIES_SETTING);
export const ADDITIONAL_TEST_ARGUMENTS_SETTING = new Setting(
"additionalTestArguments",
RUNNING_TESTS_SETTING,
);
export const NUMBER_OF_TEST_THREADS_SETTING = new Setting(
"numberOfThreads",
RUNNING_TESTS_SETTING,
);
export const MAX_QUERIES = new Setting("maxQueries", RUNNING_QUERIES_SETTING);
export const AUTOSAVE_SETTING = new Setting(
"autoSave",
RUNNING_QUERIES_SETTING,
);
export const PAGE_SIZE = new Setting("pageSize", RESULTS_DISPLAY_SETTING);
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting(
"customLogDirectory",
RUNNING_QUERIES_SETTING,
);
/** When these settings change, the running query server should be restarted. */
const QUERY_SERVER_RESTARTING_SETTINGS = [
NUMBER_OF_THREADS_SETTING, SAVE_CACHE_SETTING, CACHE_SIZE_SETTING, MEMORY_SETTING,
DEBUG_SETTING, CUSTOM_LOG_DIRECTORY_SETTING,
NUMBER_OF_THREADS_SETTING,
SAVE_CACHE_SETTING,
CACHE_SIZE_SETTING,
MEMORY_SETTING,
DEBUG_SETTING,
CUSTOM_LOG_DIRECTORY_SETTING,
];
export interface QueryServerConfig {
@@ -136,7 +189,10 @@ export interface QueryServerConfig {
}
/** When these settings change, the query history should be refreshed. */
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING, QUERY_HISTORY_TTL];
const QUERY_HISTORY_SETTINGS = [
QUERY_HISTORY_FORMAT_SETTING,
QUERY_HISTORY_TTL,
];
export interface QueryHistoryConfig {
format: string;
@@ -144,7 +200,12 @@ export interface QueryHistoryConfig {
onDidChangeConfiguration: Event<void>;
}
const CLI_SETTINGS = [ADDITIONAL_TEST_ARGUMENTS_SETTING, NUMBER_OF_TEST_THREADS_SETTING, NUMBER_OF_THREADS_SETTING, MAX_PATHS];
const CLI_SETTINGS = [
ADDITIONAL_TEST_ARGUMENTS_SETTING,
NUMBER_OF_TEST_THREADS_SETTING,
NUMBER_OF_THREADS_SETTING,
MAX_PATHS,
];
export interface CliConfig {
additionalTestArguments: string[];
@@ -154,20 +215,29 @@ export interface CliConfig {
onDidChangeConfiguration?: Event<void>;
}
export abstract class ConfigListener extends DisposableObject {
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
protected readonly _onDidChangeConfiguration = this.push(
new EventEmitter<void>(),
);
constructor() {
super();
this.updateConfiguration();
this.push(workspace.onDidChangeConfiguration(this.handleDidChangeConfiguration, this));
this.push(
workspace.onDidChangeConfiguration(
this.handleDidChangeConfiguration,
this,
),
);
}
/**
* Calls `updateConfiguration` if any of the `relevantSettings` have changed.
*/
protected handleDidChangeConfigurationForRelevantSettings(relevantSettings: Setting[], e: ConfigurationChangeEvent): void {
protected handleDidChangeConfigurationForRelevantSettings(
relevantSettings: Setting[],
e: ConfigurationChangeEvent,
): void {
// Check whether any options that affect query running were changed.
for (const option of relevantSettings) {
// TODO: compare old and new values, only update if there was actually a change?
@@ -178,7 +248,9 @@ export abstract class ConfigListener extends DisposableObject {
}
}
protected abstract handleDidChangeConfiguration(e: ConfigurationChangeEvent): void;
protected abstract handleDidChangeConfiguration(
e: ConfigurationChangeEvent,
): void;
private updateConfiguration(): void {
this._onDidChangeConfiguration.fire(undefined);
}
@@ -188,7 +260,10 @@ export abstract class ConfigListener extends DisposableObject {
}
}
export class DistributionConfigListener extends ConfigListener implements DistributionConfig {
export class DistributionConfigListener
extends ConfigListener
implements DistributionConfig
{
public get customCodeQlPath(): string | undefined {
return CUSTOM_CODEQL_PATH_SETTING.getValue() || undefined;
}
@@ -202,28 +277,43 @@ export class DistributionConfigListener extends ConfigListener implements Distri
}
public async updateCustomCodeQlPath(newPath: string | undefined) {
await CUSTOM_CODEQL_PATH_SETTING.updateValue(newPath, ConfigurationTarget.Global);
await CUSTOM_CODEQL_PATH_SETTING.updateValue(
newPath,
ConfigurationTarget.Global,
);
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(DISTRIBUTION_CHANGE_SETTINGS, e);
this.handleDidChangeConfigurationForRelevantSettings(
DISTRIBUTION_CHANGE_SETTINGS,
e,
);
}
}
export class QueryServerConfigListener extends ConfigListener implements QueryServerConfig {
public constructor(private _codeQlPath = '') {
export class QueryServerConfigListener
extends ConfigListener
implements QueryServerConfig
{
public constructor(private _codeQlPath = "") {
super();
}
public static async createQueryServerConfigListener(distributionManager: DistributionManager): Promise<QueryServerConfigListener> {
const codeQlPath = await distributionManager.getCodeQlPathWithoutVersionCheck();
public static async createQueryServerConfigListener(
distributionManager: DistributionManager,
): Promise<QueryServerConfigListener> {
const codeQlPath =
await distributionManager.getCodeQlPathWithoutVersionCheck();
const config = new QueryServerConfigListener(codeQlPath!);
if (distributionManager.onDidChangeDistribution) {
config.push(distributionManager.onDidChangeDistribution(async () => {
const codeQlPath = await distributionManager.getCodeQlPathWithoutVersionCheck();
config._codeQlPath = codeQlPath!;
config._onDidChangeConfiguration.fire(undefined);
}));
config.push(
distributionManager.onDidChangeDistribution(async () => {
const codeQlPath =
await distributionManager.getCodeQlPathWithoutVersionCheck();
config._codeQlPath = codeQlPath!;
config._onDidChangeConfiguration.fire(undefined);
}),
);
}
return config;
}
@@ -258,8 +348,10 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
if (memory === null) {
return undefined;
}
if (memory == 0 || typeof (memory) !== 'number') {
void logger.log(`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`);
if (memory == 0 || typeof memory !== "number") {
void logger.log(
`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`,
);
return undefined;
}
return memory;
@@ -270,13 +362,22 @@ export class QueryServerConfigListener extends ConfigListener implements QuerySe
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(QUERY_SERVER_RESTARTING_SETTINGS, e);
this.handleDidChangeConfigurationForRelevantSettings(
QUERY_SERVER_RESTARTING_SETTINGS,
e,
);
}
}
export class QueryHistoryConfigListener extends ConfigListener implements QueryHistoryConfig {
export class QueryHistoryConfigListener
extends ConfigListener
implements QueryHistoryConfig
{
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(QUERY_HISTORY_SETTINGS, e);
this.handleDidChangeConfigurationForRelevantSettings(
QUERY_HISTORY_SETTINGS,
e,
);
}
public get format(): string {
@@ -316,13 +417,15 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
/**
* Whether to enable CodeLens for the 'Quick Evaluation' command.
*/
const QUICK_EVAL_CODELENS_SETTING = new Setting('quickEvalCodelens', RUNNING_QUERIES_SETTING);
const QUICK_EVAL_CODELENS_SETTING = new Setting(
"quickEvalCodelens",
RUNNING_QUERIES_SETTING,
);
export function isQuickEvalCodelensEnabled() {
return QUICK_EVAL_CODELENS_SETTING.getValue<boolean>();
}
// Enable experimental features
/**
@@ -335,7 +438,7 @@ export function isQuickEvalCodelensEnabled() {
/**
* Enables canary features of this extension. Recommended for all internal users.
*/
export const CANARY_FEATURES = new Setting('canary', ROOT_SETTING);
export const CANARY_FEATURES = new Setting("canary", ROOT_SETTING);
export function isCanary() {
return !!CANARY_FEATURES.getValue<boolean>();
@@ -344,7 +447,10 @@ export function isCanary() {
/**
* Enables the experimental query server
*/
export const CANARY_QUERY_SERVER = new Setting('canaryQueryServer', ROOT_SETTING);
export const CANARY_QUERY_SERVER = new Setting(
"canaryQueryServer",
ROOT_SETTING,
);
// The default value for this setting is now `true`
export function allowCanaryQueryServer() {
@@ -352,7 +458,10 @@ export function allowCanaryQueryServer() {
return value === undefined ? true : !!value;
}
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting('joinOrderWarningThreshold', LOG_INSIGHTS_SETTING);
export const JOIN_ORDER_WARNING_THRESHOLD = new Setting(
"joinOrderWarningThreshold",
LOG_INSIGHTS_SETTING,
);
export function joinOrderWarningThreshold(): number {
return JOIN_ORDER_WARNING_THRESHOLD.getValue<number>();
@@ -361,10 +470,13 @@ export function joinOrderWarningThreshold(): number {
/**
* Avoids caching in the AST viewer if the user is also a canary user.
*/
export const NO_CACHE_AST_VIEWER = new Setting('disableCache', AST_VIEWER_SETTING);
export const NO_CACHE_AST_VIEWER = new Setting(
"disableCache",
AST_VIEWER_SETTING,
);
// Settings for variant analysis
const REMOTE_QUERIES_SETTING = new Setting('variantAnalysis', ROOT_SETTING);
const REMOTE_QUERIES_SETTING = new Setting("variantAnalysis", ROOT_SETTING);
/**
* Lists of GitHub repositories that you want to query remotely via the "Run Variant Analysis" command.
@@ -373,13 +485,20 @@ const REMOTE_QUERIES_SETTING = new Setting('variantAnalysis', ROOT_SETTING);
* This setting should be a JSON object where each key is a user-specified name (string),
* and the value is an array of GitHub repositories (of the form `<owner>/<repo>`).
*/
const REMOTE_REPO_LISTS = new Setting('repositoryLists', REMOTE_QUERIES_SETTING);
const REMOTE_REPO_LISTS = new Setting(
"repositoryLists",
REMOTE_QUERIES_SETTING,
);
export function getRemoteRepositoryLists(): Record<string, string[]> | undefined {
export function getRemoteRepositoryLists():
| Record<string, string[]>
| undefined {
return REMOTE_REPO_LISTS.getValue<Record<string, string[]>>() || undefined;
}
export async function setRemoteRepositoryLists(lists: Record<string, string[]> | undefined) {
export async function setRemoteRepositoryLists(
lists: Record<string, string[]> | undefined,
) {
await REMOTE_REPO_LISTS.updateValue(lists, ConfigurationTarget.Global);
}
@@ -392,7 +511,10 @@ export async function setRemoteRepositoryLists(lists: Record<string, string[]> |
* user-specified name (string), and the value is an array of GitHub repositories
* (of the form `<owner>/<repo>`).
*/
const REPO_LISTS_PATH = new Setting('repositoryListsPath', REMOTE_QUERIES_SETTING);
const REPO_LISTS_PATH = new Setting(
"repositoryListsPath",
REMOTE_QUERIES_SETTING,
);
export function getRemoteRepositoryListsPath(): string | undefined {
return REPO_LISTS_PATH.getValue<string>() || undefined;
@@ -404,7 +526,10 @@ export function getRemoteRepositoryListsPath(): string | undefined {
*
* This setting should be a GitHub repository of the form `<owner>/<repo>`.
*/
const REMOTE_CONTROLLER_REPO = new Setting('controllerRepo', REMOTE_QUERIES_SETTING);
const REMOTE_CONTROLLER_REPO = new Setting(
"controllerRepo",
REMOTE_QUERIES_SETTING,
);
export function getRemoteControllerRepo(): string | undefined {
return REMOTE_CONTROLLER_REPO.getValue<string>() || undefined;
@@ -419,21 +544,21 @@ export async function setRemoteControllerRepo(repo: string | undefined) {
* Default value is "main".
* Note: This command is only available for internal users.
*/
const ACTION_BRANCH = new Setting('actionBranch', REMOTE_QUERIES_SETTING);
const ACTION_BRANCH = new Setting("actionBranch", REMOTE_QUERIES_SETTING);
export function getActionBranch(): string {
return ACTION_BRANCH.getValue<string>() || 'main';
return ACTION_BRANCH.getValue<string>() || "main";
}
export function isIntegrationTestMode() {
return process.env.INTEGRATION_TEST_MODE === 'true';
return process.env.INTEGRATION_TEST_MODE === "true";
}
/**
* A flag indicating whether to enable the experimental "live results" feature
* for multi-repo variant analyses.
*/
const LIVE_RESULTS = new Setting('liveResults', REMOTE_QUERIES_SETTING);
const LIVE_RESULTS = new Setting("liveResults", REMOTE_QUERIES_SETTING);
export function isVariantAnalysisLiveResultsEnabled(): boolean {
return !!LIVE_RESULTS.getValue<boolean>();
@@ -443,26 +568,32 @@ export function isVariantAnalysisLiveResultsEnabled(): boolean {
* A flag indicating whether to use the new query run experience which involves
* using a new database panel.
*/
const NEW_QUERY_RUN_EXPERIENCE = new Setting('newQueryRunExperience', ROOT_SETTING);
const NEW_QUERY_RUN_EXPERIENCE = new Setting(
"newQueryRunExperience",
ROOT_SETTING,
);
export function isNewQueryRunExperienceEnabled(): boolean {
return !!NEW_QUERY_RUN_EXPERIENCE.getValue<boolean>();
}
// Settings for mocking the GitHub API.
const MOCK_GH_API_SERVER = new Setting('mockGitHubApiServer', ROOT_SETTING);
const MOCK_GH_API_SERVER = new Setting("mockGitHubApiServer", ROOT_SETTING);
/**
* A flag indicating whether to enable a mock GitHub API server.
*/
const MOCK_GH_API_SERVER_ENABLED = new Setting('enabled', MOCK_GH_API_SERVER);
const MOCK_GH_API_SERVER_ENABLED = new Setting("enabled", MOCK_GH_API_SERVER);
/**
* A path to a directory containing test scenarios. If this setting is not set,
* the mock server will a default location for test scenarios in dev mode, and
* will show a menu to select a directory in production mode.
*/
const MOCK_GH_API_SERVER_SCENARIOS_PATH = new Setting('scenariosPath', MOCK_GH_API_SERVER);
const MOCK_GH_API_SERVER_SCENARIOS_PATH = new Setting(
"scenariosPath",
MOCK_GH_API_SERVER,
);
export interface MockGitHubApiConfig {
mockServerEnabled: boolean;
@@ -470,9 +601,15 @@ export interface MockGitHubApiConfig {
onDidChangeConfiguration: Event<void>;
}
export class MockGitHubApiConfigListener extends ConfigListener implements MockGitHubApiConfig {
export class MockGitHubApiConfigListener
extends ConfigListener
implements MockGitHubApiConfig
{
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings([MOCK_GH_API_SERVER], e);
this.handleDidChangeConfigurationForRelevantSettings(
[MOCK_GH_API_SERVER],
e,
);
}
public get mockServerEnabled(): boolean {

View File

@@ -1,24 +1,23 @@
import { CodeQLCliServer } from '../cli';
import { DecodedBqrsChunk, BqrsId, EntityValue } from '../pure/bqrs-cli-types';
import { DatabaseItem } from '../databases';
import { ChildAstItem, AstItem } from '../astViewer';
import fileRangeFromURI from './fileRangeFromURI';
import { Uri } from 'vscode';
import { QueryWithResults } from '../run-queries-shared';
import { CodeQLCliServer } from "../cli";
import { DecodedBqrsChunk, BqrsId, EntityValue } from "../pure/bqrs-cli-types";
import { DatabaseItem } from "../databases";
import { ChildAstItem, AstItem } from "../astViewer";
import fileRangeFromURI from "./fileRangeFromURI";
import { Uri } from "vscode";
import { QueryWithResults } from "../run-queries-shared";
/**
* A class that wraps a tree of QL results from a query that
* has an @kind of graph
*/
export default class AstBuilder {
private roots: AstItem[] | undefined;
private bqrsPath: string;
constructor(
queryResults: QueryWithResults,
private cli: CodeQLCliServer,
public db: DatabaseItem,
public fileName: Uri
public fileName: Uri,
) {
this.bqrsPath = queryResults.query.resultsPaths.resultsPath;
}
@@ -31,15 +30,15 @@ export default class AstBuilder {
}
private async parseRoots(): Promise<AstItem[]> {
const options = { entities: ['id', 'url', 'string'] };
const options = { entities: ["id", "url", "string"] };
const [nodeTuples, edgeTuples, graphProperties] = await Promise.all([
await this.cli.bqrsDecode(this.bqrsPath, 'nodes', options),
await this.cli.bqrsDecode(this.bqrsPath, 'edges', options),
await this.cli.bqrsDecode(this.bqrsPath, 'graphProperties', options),
await this.cli.bqrsDecode(this.bqrsPath, "nodes", options),
await this.cli.bqrsDecode(this.bqrsPath, "edges", options),
await this.cli.bqrsDecode(this.bqrsPath, "graphProperties", options),
]);
if (!this.isValidGraph(graphProperties)) {
throw new Error('AST is invalid');
throw new Error("AST is invalid");
}
const idToItem = new Map<BqrsId, AstItem>();
@@ -50,21 +49,26 @@ export default class AstBuilder {
const roots = [];
// Build up the parent-child relationships
edgeTuples.tuples.forEach(tuple => {
const [source, target, tupleType, value] = tuple as [EntityValue, EntityValue, string, string];
edgeTuples.tuples.forEach((tuple) => {
const [source, target, tupleType, value] = tuple as [
EntityValue,
EntityValue,
string,
string,
];
const sourceId = source.id!;
const targetId = target.id!;
switch (tupleType) {
case 'semmle.order':
case "semmle.order":
astOrder.set(targetId, Number(value));
break;
case 'semmle.label': {
case "semmle.label": {
childToParent.set(targetId, sourceId);
let children = parentToChildren.get(sourceId);
if (!children) {
parentToChildren.set(sourceId, children = []);
parentToChildren.set(sourceId, (children = []));
}
children.push(targetId);
@@ -81,39 +85,43 @@ export default class AstBuilder {
});
// populate parents and children
nodeTuples.tuples.forEach(tuple => {
nodeTuples.tuples.forEach((tuple) => {
const [entity, tupleType, value] = tuple as [EntityValue, string, string];
const id = entity.id!;
switch (tupleType) {
case 'semmle.order':
case "semmle.order":
astOrder.set(id, Number(value));
break;
case 'semmle.label': {
case "semmle.label": {
// If an edge label exists, include it and separate from the node label using ':'
const nodeLabel = value ?? entity.label;
const edgeLabel = edgeLabels.get(id);
const label = [edgeLabel, nodeLabel].filter(e => e).join(': ');
const label = [edgeLabel, nodeLabel].filter((e) => e).join(": ");
const item = {
id,
label,
location: entity.url,
fileLocation: fileRangeFromURI(entity.url, this.db),
children: [] as ChildAstItem[],
order: Number.MAX_SAFE_INTEGER
order: Number.MAX_SAFE_INTEGER,
};
idToItem.set(id, item);
const parent = idToItem.get(childToParent.has(id) ? childToParent.get(id)! : -1);
const parent = idToItem.get(
childToParent.has(id) ? childToParent.get(id)! : -1,
);
if (parent) {
const astItem = item as ChildAstItem;
astItem.parent = parent;
parent.children.push(astItem);
}
const children = parentToChildren.has(id) ? parentToChildren.get(id)! : [];
children.forEach(childId => {
const children = parentToChildren.has(id)
? parentToChildren.get(id)!
: [];
children.forEach((childId) => {
const child = idToItem.get(childId) as ChildAstItem | undefined;
if (child) {
child.parent = item;
@@ -134,7 +142,7 @@ export default class AstBuilder {
? astOrder.get(item.id)!
: Number.MAX_SAFE_INTEGER;
if (!('parent' in item)) {
if (!("parent" in item)) {
roots.push(item);
}
}
@@ -142,7 +150,9 @@ export default class AstBuilder {
}
private isValidGraph(graphProperties: DecodedBqrsChunk) {
const tuple = graphProperties?.tuples?.find(t => t[0] === 'semmle.graphKind');
return tuple?.[1] === 'tree';
const tuple = graphProperties?.tuples?.find(
(t) => t[0] === "semmle.graphKind",
);
return tuple?.[1] === "tree";
}
}

View File

@@ -1,26 +1,30 @@
import * as vscode from 'vscode';
import * as vscode from "vscode";
import { UrlValue, LineColumnLocation } from '../pure/bqrs-cli-types';
import { isEmptyPath } from '../pure/bqrs-utils';
import { DatabaseItem } from '../databases';
import { UrlValue, LineColumnLocation } from "../pure/bqrs-cli-types";
import { isEmptyPath } from "../pure/bqrs-utils";
import { DatabaseItem } from "../databases";
export default function fileRangeFromURI(uri: UrlValue | undefined, db: DatabaseItem): vscode.Location | undefined {
if (!uri || typeof uri === 'string') {
export default function fileRangeFromURI(
uri: UrlValue | undefined,
db: DatabaseItem,
): vscode.Location | undefined {
if (!uri || typeof uri === "string") {
return undefined;
} else if ('startOffset' in uri) {
} else if ("startOffset" in uri) {
return undefined;
} else {
const loc = uri as LineColumnLocation;
if (isEmptyPath(loc.uri)) {
return undefined;
}
const range = new vscode.Range(Math.max(0, (loc.startLine || 0) - 1),
const range = new vscode.Range(
Math.max(0, (loc.startLine || 0) - 1),
Math.max(0, (loc.startColumn || 0) - 1),
Math.max(0, (loc.endLine || 0) - 1),
Math.max(0, (loc.endColumn || 0)));
Math.max(0, loc.endColumn || 0),
);
try {
if (uri.uri.startsWith('file:')) {
if (uri.uri.startsWith("file:")) {
return new vscode.Location(db.resolveSourceFile(uri.uri), range);
}
return undefined;

View File

@@ -1,33 +1,33 @@
export enum KeyType {
DefinitionQuery = 'DefinitionQuery',
ReferenceQuery = 'ReferenceQuery',
PrintAstQuery = 'PrintAstQuery',
PrintCfgQuery = 'PrintCfgQuery',
DefinitionQuery = "DefinitionQuery",
ReferenceQuery = "ReferenceQuery",
PrintAstQuery = "PrintAstQuery",
PrintCfgQuery = "PrintCfgQuery",
}
export function tagOfKeyType(keyType: KeyType): string {
switch (keyType) {
case KeyType.DefinitionQuery:
return 'ide-contextual-queries/local-definitions';
return "ide-contextual-queries/local-definitions";
case KeyType.ReferenceQuery:
return 'ide-contextual-queries/local-references';
return "ide-contextual-queries/local-references";
case KeyType.PrintAstQuery:
return 'ide-contextual-queries/print-ast';
return "ide-contextual-queries/print-ast";
case KeyType.PrintCfgQuery:
return 'ide-contextual-queries/print-cfg';
return "ide-contextual-queries/print-cfg";
}
}
export function nameOfKeyType(keyType: KeyType): string {
switch (keyType) {
case KeyType.DefinitionQuery:
return 'definitions';
return "definitions";
case KeyType.ReferenceQuery:
return 'references';
return "references";
case KeyType.PrintAstQuery:
return 'print AST';
return "print AST";
case KeyType.PrintCfgQuery:
return 'print CFG';
return "print CFG";
}
}
@@ -35,9 +35,9 @@ export function kindOfKeyType(keyType: KeyType): string {
switch (keyType) {
case KeyType.DefinitionQuery:
case KeyType.ReferenceQuery:
return 'definitions';
return "definitions";
case KeyType.PrintAstQuery:
case KeyType.PrintCfgQuery:
return 'graph';
return "graph";
}
}

View File

@@ -1,17 +1,29 @@
import { decodeSourceArchiveUri, encodeArchiveBasePath } from '../archive-filesystem-provider';
import { ColumnKindCode, EntityValue, getResultSetSchema, ResultSetSchema } from '../pure/bqrs-cli-types';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager, DatabaseItem } from '../databases';
import fileRangeFromURI from './fileRangeFromURI';
import { ProgressCallback } from '../commandRunner';
import { KeyType } from './keyType';
import { qlpackOfDatabase, resolveQueries, runContextualQuery } from './queryResolver';
import { CancellationToken, LocationLink, Uri } from 'vscode';
import { QueryWithResults } from '../run-queries-shared';
import { QueryRunner } from '../queryRunner';
import {
decodeSourceArchiveUri,
encodeArchiveBasePath,
} from "../archive-filesystem-provider";
import {
ColumnKindCode,
EntityValue,
getResultSetSchema,
ResultSetSchema,
} from "../pure/bqrs-cli-types";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager, DatabaseItem } from "../databases";
import fileRangeFromURI from "./fileRangeFromURI";
import { ProgressCallback } from "../commandRunner";
import { KeyType } from "./keyType";
import {
qlpackOfDatabase,
resolveQueries,
runContextualQuery,
} from "./queryResolver";
import { CancellationToken, LocationLink, Uri } from "vscode";
import { QueryWithResults } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
export const SELECT_QUERY_NAME = '#select';
export const TEMPLATE_NAME = 'selectedSourceFile';
export const SELECT_QUERY_NAME = "#select";
export const TEMPLATE_NAME = "selectedSourceFile";
export interface FullLocationLink extends LocationLink {
originUri: Uri;
@@ -41,7 +53,7 @@ export async function getLocationsForUriString(
queryStorageDir: string,
progress: ProgressCallback,
token: CancellationToken,
filter: (src: string, dest: string) => boolean
filter: (src: string, dest: string) => boolean,
): Promise<FullLocationLink[]> {
const uri = decodeSourceArchiveUri(Uri.parse(uriString, true));
const sourceArchiveUri = encodeArchiveBasePath(uri.sourceArchiveZipPath);
@@ -56,9 +68,18 @@ export async function getLocationsForUriString(
const links: FullLocationLink[] = [];
for (const query of await resolveQueries(cli, qlpack, keyType)) {
const results = await runContextualQuery(query, db, queryStorageDir, qs, cli, progress, token, templates);
const results = await runContextualQuery(
query,
db,
queryStorageDir,
qs,
cli,
progress,
token,
templates,
);
if (results.successful) {
links.push(...await getLinksFromResults(results, cli, db, filter));
links.push(...(await getLinksFromResults(results, cli, db, filter)));
}
}
return links;
@@ -68,7 +89,7 @@ async function getLinksFromResults(
results: QueryWithResults,
cli: CodeQLCliServer,
db: DatabaseItem,
filter: (srcFile: string, destFile: string) => boolean
filter: (srcFile: string, destFile: string) => boolean,
): Promise<FullLocationLink[]> {
const localLinks: FullLocationLink[] = [];
const bqrsPath = results.query.resultsPaths.resultsPath;
@@ -81,12 +102,16 @@ async function getLinksFromResults(
const [src, dest] = tuple as [EntityValue, EntityValue];
const srcFile = src.url && fileRangeFromURI(src.url, db);
const destFile = dest.url && fileRangeFromURI(dest.url, db);
if (srcFile && destFile && filter(srcFile.uri.toString(), destFile.uri.toString())) {
if (
srcFile &&
destFile &&
filter(srcFile.uri.toString(), destFile.uri.toString())
) {
localLinks.push({
targetRange: destFile.range,
targetUri: destFile.uri,
originSelectionRange: srcFile.range,
originUri: srcFile.uri
originUri: srcFile.uri,
});
}
}
@@ -96,13 +121,16 @@ async function getLinksFromResults(
function createTemplates(path: string): Record<string, string> {
return {
[TEMPLATE_NAME]: path
[TEMPLATE_NAME]: path,
};
}
function isValidSelect(selectInfo: ResultSetSchema | undefined) {
return selectInfo && selectInfo.columns.length == 3
&& selectInfo.columns[0].kind == ColumnKindCode.ENTITY
&& selectInfo.columns[1].kind == ColumnKindCode.ENTITY
&& selectInfo.columns[2].kind == ColumnKindCode.STRING;
return (
selectInfo &&
selectInfo.columns.length == 3 &&
selectInfo.columns[0].kind == ColumnKindCode.ENTITY &&
selectInfo.columns[1].kind == ColumnKindCode.ENTITY &&
selectInfo.columns[2].kind == ColumnKindCode.STRING
);
}

View File

@@ -1,27 +1,25 @@
import * as fs from 'fs-extra';
import * as yaml from 'js-yaml';
import * as tmp from 'tmp-promise';
import * as path from 'path';
import * as fs from "fs-extra";
import * as yaml from "js-yaml";
import * as tmp from "tmp-promise";
import * as path from "path";
import * as helpers from '../helpers';
import {
KeyType,
kindOfKeyType,
nameOfKeyType,
tagOfKeyType
} from './keyType';
import { CodeQLCliServer } from '../cli';
import { DatabaseItem } from '../databases';
import { QlPacksForLanguage } from '../helpers';
import { logger } from '../logging';
import { createInitialQueryInfo } from '../run-queries-shared';
import { CancellationToken, Uri } from 'vscode';
import { ProgressCallback } from '../commandRunner';
import { QueryRunner } from '../queryRunner';
import * as helpers from "../helpers";
import { KeyType, kindOfKeyType, nameOfKeyType, tagOfKeyType } from "./keyType";
import { CodeQLCliServer } from "../cli";
import { DatabaseItem } from "../databases";
import { QlPacksForLanguage } from "../helpers";
import { logger } from "../logging";
import { createInitialQueryInfo } from "../run-queries-shared";
import { CancellationToken, Uri } from "vscode";
import { ProgressCallback } from "../commandRunner";
import { QueryRunner } from "../queryRunner";
export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem): Promise<QlPacksForLanguage> {
export async function qlpackOfDatabase(
cli: CodeQLCliServer,
db: DatabaseItem,
): Promise<QlPacksForLanguage> {
if (db.contents === undefined) {
throw new Error('Database is invalid and cannot infer QLPack.');
throw new Error("Database is invalid and cannot infer QLPack.");
}
const datasetPath = db.contents.datasetUri.fsPath;
const dbscheme = await helpers.getPrimaryDbscheme(datasetPath);
@@ -36,29 +34,43 @@ export async function qlpackOfDatabase(cli: CodeQLCliServer, db: DatabaseItem):
* @param keyType The contextual query key of the query to search for.
* @returns The found queries from the first pack in which any matching queries were found.
*/
async function resolveQueriesFromPacks(cli: CodeQLCliServer, qlpacks: string[], keyType: KeyType): Promise<string[]> {
const suiteFile = (await tmp.file({
postfix: '.qls'
})).path;
async function resolveQueriesFromPacks(
cli: CodeQLCliServer,
qlpacks: string[],
keyType: KeyType,
): Promise<string[]> {
const suiteFile = (
await tmp.file({
postfix: ".qls",
})
).path;
const suiteYaml = [];
for (const qlpack of qlpacks) {
suiteYaml.push({
from: qlpack,
queries: '.',
queries: ".",
include: {
kind: kindOfKeyType(keyType),
'tags contain': tagOfKeyType(keyType)
}
"tags contain": tagOfKeyType(keyType),
},
});
}
await fs.writeFile(suiteFile, yaml.dump(suiteYaml), 'utf8');
await fs.writeFile(suiteFile, yaml.dump(suiteYaml), "utf8");
const queries = await cli.resolveQueriesInSuite(suiteFile, helpers.getOnDiskWorkspaceFolders());
const queries = await cli.resolveQueriesInSuite(
suiteFile,
helpers.getOnDiskWorkspaceFolders(),
);
return queries;
}
export async function resolveQueries(cli: CodeQLCliServer, qlpacks: QlPacksForLanguage, keyType: KeyType): Promise<string[]> {
const cliCanHandleLibraryPack = await cli.cliConstraints.supportsAllowLibraryPacksInResolveQueries();
export async function resolveQueries(
cli: CodeQLCliServer,
qlpacks: QlPacksForLanguage,
keyType: KeyType,
): Promise<string[]> {
const cliCanHandleLibraryPack =
await cli.cliConstraints.supportsAllowLibraryPacksInResolveQueries();
const packsToSearch: string[] = [];
let blameCli: boolean;
@@ -98,20 +110,32 @@ export async function resolveQueries(cli: CodeQLCliServer, qlpacks: QlPacksForLa
}
// No queries found. Determine the correct error message for the various scenarios.
const errorMessage = blameCli ?
`Your current version of the CodeQL CLI, '${(await cli.getVersion()).version}', \
const errorMessage = blameCli
? `Your current version of the CodeQL CLI, '${
(await cli.getVersion()).version
}', \
is unable to use contextual queries from recent versions of the standard CodeQL libraries. \
Please upgrade to the latest version of the CodeQL CLI.`
:
`No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(keyType)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(keyType)} queries are not yet available \
: `No ${nameOfKeyType(keyType)} queries (tagged "${tagOfKeyType(
keyType,
)}") could be found in the current library path. \
Try upgrading the CodeQL libraries. If that doesn't work, then ${nameOfKeyType(
keyType,
)} queries are not yet available \
for this language.`;
void helpers.showAndLogErrorMessage(errorMessage);
throw new Error(`Couldn't find any queries tagged ${tagOfKeyType(keyType)} in any of the following packs: ${packsToSearch.join(', ')}.`);
throw new Error(
`Couldn't find any queries tagged ${tagOfKeyType(
keyType,
)} in any of the following packs: ${packsToSearch.join(", ")}.`,
);
}
async function resolveContextualQuery(cli: CodeQLCliServer, query: string): Promise<{ packPath: string, createdTempLockFile: boolean }> {
async function resolveContextualQuery(
cli: CodeQLCliServer,
query: string,
): Promise<{ packPath: string; createdTempLockFile: boolean }> {
// Contextual queries now live within the standard library packs.
// This simplifies distribution (you don't need the standard query pack to use the AST viewer),
// but if the library pack doesn't have a lockfile, we won't be able to find
@@ -119,57 +143,85 @@ async function resolveContextualQuery(cli: CodeQLCliServer, query: string): Prom
// Work out the enclosing pack.
const packContents = await cli.packPacklist(query, false);
const packFilePath = packContents.find((p) => ['codeql-pack.yml', 'qlpack.yml'].includes(path.basename(p)));
const packFilePath = packContents.find((p) =>
["codeql-pack.yml", "qlpack.yml"].includes(path.basename(p)),
);
if (packFilePath === undefined) {
// Should not happen; we already resolved this query.
throw new Error(`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`);
throw new Error(
`Could not find a CodeQL pack file for the pack enclosing the contextual query ${query}`,
);
}
const packPath = path.dirname(packFilePath);
const lockFilePath = packContents.find((p) => ['codeql-pack.lock.yml', 'qlpack.lock.yml'].includes(path.basename(p)));
const lockFilePath = packContents.find((p) =>
["codeql-pack.lock.yml", "qlpack.lock.yml"].includes(path.basename(p)),
);
let createdTempLockFile = false;
if (!lockFilePath) {
// No lock file, likely because this library pack is in the package cache.
// Create a lock file so that we can resolve dependencies and library path
// for the contextual query.
void logger.log(`Library pack ${packPath} is missing a lock file; creating a temporary lock file`);
void logger.log(
`Library pack ${packPath} is missing a lock file; creating a temporary lock file`,
);
await cli.packResolveDependencies(packPath);
createdTempLockFile = true;
// Clear CLI server pack cache before installing dependencies,
// so that it picks up the new lock file, not the previously cached pack.
void logger.log('Clearing the CodeQL CLI server\'s pack cache');
void logger.log("Clearing the CodeQL CLI server's pack cache");
await cli.clearCache();
// Install dependencies.
void logger.log(`Installing package dependencies for library pack ${packPath}`);
void logger.log(
`Installing package dependencies for library pack ${packPath}`,
);
await cli.packInstall(packPath);
}
return { packPath, createdTempLockFile };
}
async function removeTemporaryLockFile(packPath: string) {
const tempLockFilePath = path.resolve(packPath, 'codeql-pack.lock.yml');
void logger.log(`Deleting temporary package lock file at ${tempLockFilePath}`);
const tempLockFilePath = path.resolve(packPath, "codeql-pack.lock.yml");
void logger.log(
`Deleting temporary package lock file at ${tempLockFilePath}`,
);
// It's fine if the file doesn't exist.
await fs.promises.rm(path.resolve(packPath, 'codeql-pack.lock.yml'), { force: true });
await fs.promises.rm(path.resolve(packPath, "codeql-pack.lock.yml"), {
force: true,
});
}
export async function runContextualQuery(query: string, db: DatabaseItem, queryStorageDir: string, qs: QueryRunner, cli: CodeQLCliServer, progress: ProgressCallback, token: CancellationToken, templates: Record<string, string>) {
const { packPath, createdTempLockFile } = await resolveContextualQuery(cli, query);
export async function runContextualQuery(
query: string,
db: DatabaseItem,
queryStorageDir: string,
qs: QueryRunner,
cli: CodeQLCliServer,
progress: ProgressCallback,
token: CancellationToken,
templates: Record<string, string>,
) {
const { packPath, createdTempLockFile } = await resolveContextualQuery(
cli,
query,
);
const initialInfo = await createInitialQueryInfo(
Uri.file(query),
{
name: db.name,
databaseUri: db.databaseUri.toString(),
},
false
false,
);
void logger.log(
`Running contextual query ${query}; results will be stored in ${queryStorageDir}`,
);
void logger.log(`Running contextual query ${query}; results will be stored in ${queryStorageDir}`);
const queryResult = await qs.compileAndRunQueryAgainstDatabase(
db,
initialInfo,
queryStorageDir,
progress,
token,
templates
templates,
);
if (createdTempLockFile) {
await removeTemporaryLockFile(packPath);

View File

@@ -8,23 +8,33 @@ import {
ReferenceContext,
ReferenceProvider,
TextDocument,
Uri
} from 'vscode';
Uri,
} from "vscode";
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
import { CachedOperation } from '../helpers';
import { ProgressCallback, withProgress } from '../commandRunner';
import AstBuilder from './astBuilder';
import {
KeyType,
} from './keyType';
import { FullLocationLink, getLocationsForUriString, TEMPLATE_NAME } from './locationFinder';
import { qlpackOfDatabase, resolveQueries, runContextualQuery } from './queryResolver';
import { isCanary, NO_CACHE_AST_VIEWER } from '../config';
import { QueryWithResults } from '../run-queries-shared';
import { QueryRunner } from '../queryRunner';
decodeSourceArchiveUri,
encodeArchiveBasePath,
zipArchiveScheme,
} from "../archive-filesystem-provider";
import { CodeQLCliServer } from "../cli";
import { DatabaseManager } from "../databases";
import { CachedOperation } from "../helpers";
import { ProgressCallback, withProgress } from "../commandRunner";
import AstBuilder from "./astBuilder";
import { KeyType } from "./keyType";
import {
FullLocationLink,
getLocationsForUriString,
TEMPLATE_NAME,
} from "./locationFinder";
import {
qlpackOfDatabase,
resolveQueries,
runContextualQuery,
} from "./queryResolver";
import { isCanary, NO_CACHE_AST_VIEWER } from "../config";
import { QueryWithResults } from "../run-queries-shared";
import { QueryRunner } from "../queryRunner";
/**
* Runs templated CodeQL queries to find definitions in
@@ -41,10 +51,16 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
private dbm: DatabaseManager,
private queryStorageDir: string,
) {
this.cache = new CachedOperation<LocationLink[]>(this.getDefinitions.bind(this));
this.cache = new CachedOperation<LocationLink[]>(
this.getDefinitions.bind(this),
);
}
async provideDefinition(document: TextDocument, position: Position, _token: CancellationToken): Promise<LocationLink[]> {
async provideDefinition(
document: TextDocument,
position: Position,
_token: CancellationToken,
): Promise<LocationLink[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: LocationLink[] = [];
for (const link of fileLinks) {
@@ -56,23 +72,26 @@ export class TemplateQueryDefinitionProvider implements DefinitionProvider {
}
private async getDefinitions(uriString: string): Promise<LocationLink[]> {
return withProgress({
location: ProgressLocation.Notification,
cancellable: true,
title: 'Finding definitions'
}, async (progress, token) => {
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
(src, _dest) => src === uriString
);
});
return withProgress(
{
location: ProgressLocation.Notification,
cancellable: true,
title: "Finding definitions",
},
async (progress, token) => {
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
(src, _dest) => src === uriString,
);
},
);
}
}
@@ -91,49 +110,57 @@ export class TemplateQueryReferenceProvider implements ReferenceProvider {
private dbm: DatabaseManager,
private queryStorageDir: string,
) {
this.cache = new CachedOperation<FullLocationLink[]>(this.getReferences.bind(this));
this.cache = new CachedOperation<FullLocationLink[]>(
this.getReferences.bind(this),
);
}
async provideReferences(
document: TextDocument,
position: Position,
_context: ReferenceContext,
_token: CancellationToken
_token: CancellationToken,
): Promise<Location[]> {
const fileLinks = await this.cache.get(document.uri.toString());
const locLinks: Location[] = [];
for (const link of fileLinks) {
if (link.targetRange!.contains(position)) {
locLinks.push({ range: link.originSelectionRange!, uri: link.originUri });
locLinks.push({
range: link.originSelectionRange!,
uri: link.originUri,
});
}
}
return locLinks;
}
private async getReferences(uriString: string): Promise<FullLocationLink[]> {
return withProgress({
location: ProgressLocation.Notification,
cancellable: true,
title: 'Finding references'
}, async (progress, token) => {
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
(src, _dest) => src === uriString
);
});
return withProgress(
{
location: ProgressLocation.Notification,
cancellable: true,
title: "Finding references",
},
async (progress, token) => {
return getLocationsForUriString(
this.cli,
this.qs,
this.dbm,
uriString,
KeyType.DefinitionQuery,
this.queryStorageDir,
progress,
token,
(src, _dest) => src === uriString,
);
},
);
}
}
type QueryWithDb = {
query: QueryWithResults,
dbUri: Uri
query: QueryWithResults;
dbUri: Uri;
};
/**
@@ -155,17 +182,20 @@ export class TemplatePrintAstProvider {
async provideAst(
progress: ProgressCallback,
token: CancellationToken,
fileUri?: Uri
fileUri?: Uri,
): Promise<AstBuilder | undefined> {
if (!fileUri) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
throw new Error(
"Cannot view the AST. Please select a valid source file inside a CodeQL database.",
);
}
const { query, dbUri } = this.shouldCache()
? await this.cache.get(fileUri.toString(), progress, token)
: await this.getAst(fileUri.toString(), progress, token);
return new AstBuilder(
query, this.cli,
query,
this.cli,
this.dbm.findDatabaseItem(dbUri)!,
fileUri,
);
@@ -178,40 +208,56 @@ export class TemplatePrintAstProvider {
private async getAst(
uriString: string,
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<QueryWithDb> {
const uri = Uri.parse(uriString, true);
if (uri.scheme !== zipArchiveScheme) {
throw new Error('Cannot view the AST. Please select a valid source file inside a CodeQL database.');
throw new Error(
"Cannot view the AST. Please select a valid source file inside a CodeQL database.",
);
}
const zippedArchive = decodeSourceArchiveUri(uri);
const sourceArchiveUri = encodeArchiveBasePath(zippedArchive.sourceArchiveZipPath);
const sourceArchiveUri = encodeArchiveBasePath(
zippedArchive.sourceArchiveZipPath,
);
const db = this.dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
if (!db) {
throw new Error('Can\'t infer database from the provided source.');
throw new Error("Can't infer database from the provided source.");
}
const qlpacks = await qlpackOfDatabase(this.cli, db);
const queries = await resolveQueries(this.cli, qlpacks, KeyType.PrintAstQuery);
const queries = await resolveQueries(
this.cli,
qlpacks,
KeyType.PrintAstQuery,
);
if (queries.length > 1) {
throw new Error('Found multiple Print AST queries. Can\'t continue');
throw new Error("Found multiple Print AST queries. Can't continue");
}
if (queries.length === 0) {
throw new Error('Did not find any Print AST queries. Can\'t continue');
throw new Error("Did not find any Print AST queries. Can't continue");
}
const query = queries[0];
const templates: Record<string, string> = {
[TEMPLATE_NAME]:
zippedArchive.pathWithinSourceArchive
[TEMPLATE_NAME]: zippedArchive.pathWithinSourceArchive,
};
const queryResult = await runContextualQuery(query, db, this.queryStorageDir, this.qs, this.cli, progress, token, templates);
const queryResult = await runContextualQuery(
query,
db,
this.queryStorageDir,
this.qs,
this.cli,
progress,
token,
templates,
);
return {
query: queryResult,
dbUri: db.databaseUri
dbUri: db.databaseUri,
};
}
}
@@ -223,50 +269,65 @@ export class TemplatePrintAstProvider {
export class TemplatePrintCfgProvider {
private cache: CachedOperation<[Uri, Record<string, string>] | undefined>;
constructor(
private cli: CodeQLCliServer,
private dbm: DatabaseManager,
) {
this.cache = new CachedOperation<[Uri, Record<string, string>] | undefined>(this.getCfgUri.bind(this));
constructor(private cli: CodeQLCliServer, private dbm: DatabaseManager) {
this.cache = new CachedOperation<[Uri, Record<string, string>] | undefined>(
this.getCfgUri.bind(this),
);
}
async provideCfgUri(document?: TextDocument): Promise<[Uri, Record<string, string>] | undefined> {
async provideCfgUri(
document?: TextDocument,
): Promise<[Uri, Record<string, string>] | undefined> {
if (!document) {
return;
}
return await this.cache.get(document.uri.toString());
}
private async getCfgUri(uriString: string): Promise<[Uri, Record<string, string>]> {
private async getCfgUri(
uriString: string,
): Promise<[Uri, Record<string, string>]> {
const uri = Uri.parse(uriString, true);
if (uri.scheme !== zipArchiveScheme) {
throw new Error('CFG Viewing is only available for databases with zipped source archives.');
throw new Error(
"CFG Viewing is only available for databases with zipped source archives.",
);
}
const zippedArchive = decodeSourceArchiveUri(uri);
const sourceArchiveUri = encodeArchiveBasePath(zippedArchive.sourceArchiveZipPath);
const sourceArchiveUri = encodeArchiveBasePath(
zippedArchive.sourceArchiveZipPath,
);
const db = this.dbm.findDatabaseItemBySourceArchive(sourceArchiveUri);
if (!db) {
throw new Error('Can\'t infer database from the provided source.');
throw new Error("Can't infer database from the provided source.");
}
const qlpack = await qlpackOfDatabase(this.cli, db);
if (!qlpack) {
throw new Error('Can\'t infer qlpack from database source archive.');
throw new Error("Can't infer qlpack from database source archive.");
}
const queries = await resolveQueries(this.cli, qlpack, KeyType.PrintCfgQuery);
const queries = await resolveQueries(
this.cli,
qlpack,
KeyType.PrintCfgQuery,
);
if (queries.length > 1) {
throw new Error(`Found multiple Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
throw new Error(
`Found multiple Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`,
);
}
if (queries.length === 0) {
throw new Error(`Did not find any Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`);
throw new Error(
`Did not find any Print CFG queries. Can't continue. Make sure there is exacly one query with the tag ${KeyType.PrintCfgQuery}`,
);
}
const queryUri = Uri.file(queries[0]);
const templates: Record<string, string> = {
[TEMPLATE_NAME]: zippedArchive.pathWithinSourceArchive
[TEMPLATE_NAME]: zippedArchive.pathWithinSourceArchive,
};
return [queryUri, templates];

View File

@@ -1,30 +1,20 @@
import fetch, { Response } from 'node-fetch';
import { zip } from 'zip-a-folder';
import * as unzipper from 'unzipper';
import {
Uri,
CancellationToken,
commands,
window,
} from 'vscode';
import { CodeQLCliServer } from './cli';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as Octokit from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';
import fetch, { Response } from "node-fetch";
import { zip } from "zip-a-folder";
import * as unzipper from "unzipper";
import { Uri, CancellationToken, commands, window } from "vscode";
import { CodeQLCliServer } from "./cli";
import * as fs from "fs-extra";
import * as path from "path";
import * as Octokit from "@octokit/rest";
import { retry } from "@octokit/plugin-retry";
import { DatabaseManager, DatabaseItem } from './databases';
import {
showAndLogInformationMessage,
} from './helpers';
import {
reportStreamProgress,
ProgressCallback,
} from './commandRunner';
import { logger } from './logging';
import { tmpDir } from './helpers';
import { Credentials } from './authentication';
import { REPO_REGEX, getErrorMessage } from './pure/helpers-pure';
import { DatabaseManager, DatabaseItem } from "./databases";
import { showAndLogInformationMessage } from "./helpers";
import { reportStreamProgress, ProgressCallback } from "./commandRunner";
import { logger } from "./logging";
import { tmpDir } from "./helpers";
import { Credentials } from "./authentication";
import { REPO_REGEX, getErrorMessage } from "./pure/helpers-pure";
/**
* Prompts a user to fetch a database from a remote location. Database is assumed to be an archive file.
@@ -37,10 +27,10 @@ export async function promptImportInternetDatabase(
storagePath: string,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
const databaseUrl = await window.showInputBox({
prompt: 'Enter URL of zipfile of database to download',
prompt: "Enter URL of zipfile of database to download",
});
if (!databaseUrl) {
return;
@@ -56,15 +46,16 @@ export async function promptImportInternetDatabase(
undefined,
progress,
token,
cli
cli,
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
await commands.executeCommand("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
}
return item;
}
/**
@@ -81,16 +72,17 @@ export async function promptImportGithubDatabase(
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
progress({
message: 'Choose repository',
message: "Choose repository",
step: 1,
maxStep: 2
maxStep: 2,
});
const githubRepo = await window.showInputBox({
title: 'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: 'https://github.com/<owner>/<repo> or <owner>/<repo>',
title:
'Enter a GitHub repository URL or "name with owner" (e.g. https://github.com/github/codeql or github/codeql)',
placeHolder: "https://github.com/<owner>/<repo> or <owner>/<repo>",
ignoreFocusOut: true,
});
if (!githubRepo) {
@@ -101,9 +93,15 @@ export async function promptImportGithubDatabase(
throw new Error(`Invalid GitHub repository: ${githubRepo}`);
}
const octokit = credentials ? await credentials.getOctokit(true) : new Octokit.Octokit({ retry });
const octokit = credentials
? await credentials.getOctokit(true)
: new Octokit.Octokit({ retry });
const result = await convertGithubNwoToDatabaseUrl(githubRepo, octokit, progress);
const result = await convertGithubNwoToDatabaseUrl(
githubRepo,
octokit,
progress,
);
if (!result) {
return;
}
@@ -120,20 +118,25 @@ export async function promptImportGithubDatabase(
* }
* We only need the actual token string.
*/
const octokitToken = (await octokit.auth() as { token: string })?.token;
const octokitToken = ((await octokit.auth()) as { token: string })?.token;
const item = await databaseArchiveFetcher(
databaseUrl,
{ 'Accept': 'application/zip', 'Authorization': octokitToken ? `Bearer ${octokitToken}` : '' },
{
Accept: "application/zip",
Authorization: octokitToken ? `Bearer ${octokitToken}` : "",
},
databaseManager,
storagePath,
`${owner}/${name}`,
progress,
token,
cli
cli,
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
await commands.executeCommand("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
return item;
}
return;
@@ -152,16 +155,16 @@ export async function promptImportLgtmDatabase(
storagePath: string,
progress: ProgressCallback,
token: CancellationToken,
cli?: CodeQLCliServer
cli?: CodeQLCliServer,
): Promise<DatabaseItem | undefined> {
progress({
message: 'Choose project',
message: "Choose project",
step: 1,
maxStep: 2
maxStep: 2,
});
const lgtmUrl = await window.showInputBox({
prompt:
'Enter the project slug or URL on LGTM (e.g., g/github/codeql or https://lgtm.com/projects/g/github/codeql)',
"Enter the project slug or URL on LGTM (e.g., g/github/codeql or https://lgtm.com/projects/g/github/codeql)",
});
if (!lgtmUrl) {
return;
@@ -178,11 +181,13 @@ export async function promptImportLgtmDatabase(
undefined,
progress,
token,
cli
cli,
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database downloaded and imported successfully.');
await commands.executeCommand("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database downloaded and imported successfully.",
);
}
return item;
}
@@ -194,7 +199,10 @@ export async function promptImportLgtmDatabase(
export async function retrieveCanonicalRepoName(lgtmUrl: string) {
const givenRepoName = extractProjectSlug(lgtmUrl);
const response = await checkForFailingResponse(await fetch(`https://api.github.com/repos/${givenRepoName}`), 'Failed to locate the repository on github');
const response = await checkForFailingResponse(
await fetch(`https://api.github.com/repos/${givenRepoName}`),
"Failed to locate the repository on github",
);
const repo = await response.json();
if (!repo || !repo.full_name) {
return;
@@ -226,16 +234,20 @@ export async function importArchiveDatabase(
undefined,
progress,
token,
cli
cli,
);
if (item) {
await commands.executeCommand('codeQLDatabases.focus');
void showAndLogInformationMessage('Database unzipped and imported successfully.');
await commands.executeCommand("codeQLDatabases.focus");
void showAndLogInformationMessage(
"Database unzipped and imported successfully.",
);
}
return item;
} catch (e) {
if (getErrorMessage(e).includes('unexpected end of file')) {
throw new Error('Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.');
if (getErrorMessage(e).includes("unexpected end of file")) {
throw new Error(
"Database is corrupt or too large. Try unzipping outside of VS Code and importing the unzipped folder instead.",
);
} else {
// delegate
throw e;
@@ -266,12 +278,12 @@ async function databaseArchiveFetcher(
cli?: CodeQLCliServer,
): Promise<DatabaseItem> {
progress({
message: 'Getting database',
message: "Getting database",
step: 1,
maxStep: 4,
});
if (!storagePath) {
throw new Error('No storage path specified.');
throw new Error("No storage path specified.");
}
await fs.ensureDir(storagePath);
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
@@ -283,7 +295,7 @@ async function databaseArchiveFetcher(
}
progress({
message: 'Opening database',
message: "Opening database",
step: 3,
maxStep: 4,
});
@@ -291,22 +303,27 @@ async function databaseArchiveFetcher(
// find the path to the database. The actual database might be in a sub-folder
const dbPath = await findDirWithFile(
unzipPath,
'.dbinfo',
'codeql-database.yml'
".dbinfo",
"codeql-database.yml",
);
if (dbPath) {
progress({
message: 'Validating and fixing source location',
message: "Validating and fixing source location",
step: 4,
maxStep: 4,
});
await ensureZippedSourceLocation(dbPath);
const item = await databaseManager.openDatabase(progress, token, Uri.file(dbPath), nameOverride);
const item = await databaseManager.openDatabase(
progress,
token,
Uri.file(dbPath),
nameOverride,
);
await databaseManager.setCurrentDatabaseItem(item);
return item;
} else {
throw new Error('Database not found in archive.');
throw new Error("Database not found in archive.");
}
}
@@ -318,7 +335,7 @@ async function getStorageFolder(storagePath: string, urlStr: string) {
// MacOS has a max filename length of 255
// and remove a few extra chars in case we need to add a counter at the end.
let lastName = path.basename(url.path).substring(0, 250);
if (lastName.endsWith('.zip')) {
if (lastName.endsWith(".zip")) {
lastName = lastName.substring(0, lastName.length - 4);
}
@@ -331,7 +348,7 @@ async function getStorageFolder(storagePath: string, urlStr: string) {
counter++;
folderName = path.join(realpath, `${lastName}-${counter}`);
if (counter > 100) {
throw new Error('Could not find a unique name for downloaded database.');
throw new Error("Could not find a unique name for downloaded database.");
}
}
return folderName;
@@ -345,8 +362,8 @@ function validateHttpsUrl(databaseUrl: string) {
throw new Error(`Invalid url: ${databaseUrl}`);
}
if (uri.scheme !== 'https') {
throw new Error('Must use https for downloading a database.');
if (uri.scheme !== "https") {
throw new Error("Must use https for downloading a database.");
}
}
@@ -354,7 +371,7 @@ async function readAndUnzip(
zipUrl: string,
unzipPath: string,
cli?: CodeQLCliServer,
progress?: ProgressCallback
progress?: ProgressCallback,
) {
// TODO: Providing progress as the file is unzipped is currently blocked
// on https://github.com/ZJONSSON/node-unzipper/issues/222
@@ -362,9 +379,9 @@ async function readAndUnzip(
progress?.({
maxStep: 10,
step: 9,
message: `Unzipping into ${path.basename(unzipPath)}`
message: `Unzipping into ${path.basename(unzipPath)}`,
});
if (cli && await cli.cliConstraints.supportsDatabaseUnbundle()) {
if (cli && (await cli.cliConstraints.supportsDatabaseUnbundle())) {
// Use the `database unbundle` command if the installed cli version supports it
await cli.databaseUnbundle(zipFile, unzipPath);
} else {
@@ -381,7 +398,7 @@ async function fetchAndUnzip(
requestHeaders: { [key: string]: string },
unzipPath: string,
cli?: CodeQLCliServer,
progress?: ProgressCallback
progress?: ProgressCallback,
) {
// Although it is possible to download and stream directly to an unzipped directory,
// we need to avoid this for two reasons. The central directory is located at the
@@ -393,33 +410,47 @@ async function fetchAndUnzip(
progress?.({
maxStep: 3,
message: 'Downloading database',
message: "Downloading database",
step: 1,
});
const response = await checkForFailingResponse(
await fetch(databaseUrl, { headers: requestHeaders }),
'Error downloading database'
"Error downloading database",
);
const archiveFileStream = fs.createWriteStream(archivePath);
const contentLength = response.headers.get('content-length');
const contentLength = response.headers.get("content-length");
const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined;
reportStreamProgress(response.body, 'Downloading database', totalNumBytes, progress);
await new Promise((resolve, reject) =>
response.body.pipe(archiveFileStream)
.on('finish', resolve)
.on('error', reject)
reportStreamProgress(
response.body,
"Downloading database",
totalNumBytes,
progress,
);
await readAndUnzip(Uri.file(archivePath).toString(true), unzipPath, cli, progress);
await new Promise((resolve, reject) =>
response.body
.pipe(archiveFileStream)
.on("finish", resolve)
.on("error", reject),
);
await readAndUnzip(
Uri.file(archivePath).toString(true),
unzipPath,
cli,
progress,
);
// remove archivePath eagerly since these archives can be large.
await fs.remove(archivePath);
}
async function checkForFailingResponse(response: Response, errorMessage: string): Promise<Response | never> {
async function checkForFailingResponse(
response: Response,
errorMessage: string,
): Promise<Response | never> {
if (response.ok) {
return response;
}
@@ -429,7 +460,8 @@ async function checkForFailingResponse(response: Response, errorMessage: string)
let msg: string;
try {
const obj = JSON.parse(text);
msg = obj.error || obj.message || obj.reason || JSON.stringify(obj, null, 2);
msg =
obj.error || obj.message || obj.reason || JSON.stringify(obj, null, 2);
} catch (e) {
msg = text;
}
@@ -437,7 +469,7 @@ async function checkForFailingResponse(response: Response, errorMessage: string)
}
function isFile(databaseUrl: string) {
return Uri.parse(databaseUrl).scheme === 'file';
return Uri.parse(databaseUrl).scheme === "file";
}
/**
@@ -481,7 +513,7 @@ export async function findDirWithFile(
* @return true if this looks like a valid GitHub repository URL or NWO
*/
export function looksLikeGithubRepo(
githubRepo: string | undefined
githubRepo: string | undefined,
): githubRepo is string {
if (!githubRepo) {
return false;
@@ -500,13 +532,13 @@ export function looksLikeGithubRepo(
function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
try {
const uri = Uri.parse(githubUrl, true);
if (uri.scheme !== 'https') {
if (uri.scheme !== "https") {
return;
}
if (uri.authority !== 'github.com' && uri.authority !== 'www.github.com') {
if (uri.authority !== "github.com" && uri.authority !== "www.github.com") {
return;
}
const paths = uri.path.split('/').filter((segment: string) => segment);
const paths = uri.path.split("/").filter((segment: string) => segment);
const nwo = `${paths[0]}/${paths[1]}`;
if (REPO_REGEX.test(nwo)) {
return nwo;
@@ -522,16 +554,23 @@ function convertGitHubUrlToNwo(githubUrl: string): string | undefined {
export async function convertGithubNwoToDatabaseUrl(
githubRepo: string,
octokit: Octokit.Octokit,
progress: ProgressCallback): Promise<{
databaseUrl: string,
owner: string,
name: string
} | undefined> {
progress: ProgressCallback,
): Promise<
| {
databaseUrl: string;
owner: string;
name: string;
}
| undefined
> {
try {
const nwo = convertGitHubUrlToNwo(githubRepo) || githubRepo;
const [owner, repo] = nwo.split('/');
const [owner, repo] = nwo.split("/");
const response = await octokit.request('GET /repos/:owner/:repo/code-scanning/codeql/databases', { owner, repo });
const response = await octokit.request(
"GET /repos/:owner/:repo/code-scanning/codeql/databases",
{ owner, repo },
);
const languages = response.data.map((db: any) => db.language);
@@ -543,9 +582,8 @@ export async function convertGithubNwoToDatabaseUrl(
return {
databaseUrl: `https://api.github.com/repos/${owner}/${repo}/code-scanning/codeql/databases/${language}`,
owner,
name: repo
name: repo,
};
} catch (e) {
void logger.log(`Error: ${getErrorMessage(e)}`);
throw new Error(`Unable to get database for '${githubRepo}'`);
@@ -568,7 +606,9 @@ export async function convertGithubNwoToDatabaseUrl(
* @return true if this looks like an LGTM project url
*/
// exported for testing
export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string {
export function looksLikeLgtmUrl(
lgtmUrl: string | undefined,
): lgtmUrl is string {
if (!lgtmUrl) {
return false;
}
@@ -579,16 +619,16 @@ export function looksLikeLgtmUrl(lgtmUrl: string | undefined): lgtmUrl is string
try {
const uri = Uri.parse(lgtmUrl, true);
if (uri.scheme !== 'https') {
if (uri.scheme !== "https") {
return false;
}
if (uri.authority !== 'lgtm.com' && uri.authority !== 'www.lgtm.com') {
if (uri.authority !== "lgtm.com" && uri.authority !== "www.lgtm.com") {
return false;
}
const paths = uri.path.split('/').filter((segment: string) => segment);
return paths.length >= 4 && paths[0] === 'projects';
const paths = uri.path.split("/").filter((segment: string) => segment);
return paths.length >= 4 && paths[0] === "projects";
} catch (e) {
return false;
}
@@ -598,8 +638,8 @@ function convertRawLgtmSlug(maybeSlug: string): string | undefined {
if (!maybeSlug) {
return;
}
const segments = maybeSlug.split('/');
const providers = ['g', 'gl', 'b', 'git'];
const segments = maybeSlug.split("/");
const providers = ["g", "gl", "b", "git"];
if (segments.length === 3 && providers.includes(segments[0])) {
return `https://lgtm.com/projects/${maybeSlug}`;
}
@@ -608,7 +648,7 @@ function convertRawLgtmSlug(maybeSlug: string): string | undefined {
function extractProjectSlug(lgtmUrl: string): string | undefined {
// Only matches the '/g/' provider (github)
const re = new RegExp('https://lgtm.com/projects/g/(.*[^/])');
const re = new RegExp("https://lgtm.com/projects/g/(.*[^/])");
const match = lgtmUrl.match(re);
if (!match) {
return;
@@ -619,7 +659,8 @@ function extractProjectSlug(lgtmUrl: string): string | undefined {
// exported for testing
export async function convertLgtmUrlToDatabaseUrl(
lgtmUrl: string,
progress: ProgressCallback) {
progress: ProgressCallback,
) {
try {
lgtmUrl = convertRawLgtmSlug(lgtmUrl) || lgtmUrl;
let projectJson = await downloadLgtmProjectMetadata(lgtmUrl);
@@ -634,23 +675,26 @@ export async function convertLgtmUrlToDatabaseUrl(
canonicalName = convertRawLgtmSlug(`g/${canonicalName}`);
projectJson = await downloadLgtmProjectMetadata(canonicalName);
if (projectJson.code === 404) {
throw new Error('Failed to download project from LGTM.');
throw new Error("Failed to download project from LGTM.");
}
}
const languages = projectJson?.languages?.map((lang: { language: string }) => lang.language) || [];
const languages =
projectJson?.languages?.map(
(lang: { language: string }) => lang.language,
) || [];
const language = await promptForLanguage(languages, progress);
if (!language) {
return;
}
return `https://lgtm.com/${[
'api',
'v1.0',
'snapshots',
"api",
"v1.0",
"snapshots",
projectJson.id,
language,
].join('/')}`;
].join("/")}`;
} catch (e) {
void logger.log(`Error: ${getErrorMessage(e)}`);
throw new Error(`Invalid LGTM URL: ${lgtmUrl}`);
@@ -659,37 +703,34 @@ export async function convertLgtmUrlToDatabaseUrl(
async function downloadLgtmProjectMetadata(lgtmUrl: string): Promise<any> {
const uri = Uri.parse(lgtmUrl, true);
const paths = ['api', 'v1.0'].concat(
uri.path.split('/').filter((segment: string) => segment)
).slice(0, 6);
const projectUrl = `https://lgtm.com/${paths.join('/')}`;
const paths = ["api", "v1.0"]
.concat(uri.path.split("/").filter((segment: string) => segment))
.slice(0, 6);
const projectUrl = `https://lgtm.com/${paths.join("/")}`;
const projectResponse = await fetch(projectUrl);
return projectResponse.json();
}
async function promptForLanguage(
languages: string[],
progress: ProgressCallback
progress: ProgressCallback,
): Promise<string | undefined> {
progress({
message: 'Choose language',
message: "Choose language",
step: 2,
maxStep: 2
maxStep: 2,
});
if (!languages.length) {
throw new Error('No databases found');
throw new Error("No databases found");
}
if (languages.length === 1) {
return languages[0];
}
return await window.showQuickPick(
languages,
{
placeHolder: 'Select the database language to download:',
ignoreFocusOut: true,
}
);
return await window.showQuickPick(languages, {
placeHolder: "Select the database language to download:",
ignoreFocusOut: true,
});
}
/**
@@ -704,10 +745,13 @@ async function promptForLanguage(
* @param databasePath The full path to the unzipped database
*/
async function ensureZippedSourceLocation(databasePath: string): Promise<void> {
const srcFolderPath = path.join(databasePath, 'src');
const srcZipPath = srcFolderPath + '.zip';
const srcFolderPath = path.join(databasePath, "src");
const srcZipPath = srcFolderPath + ".zip";
if ((await fs.pathExists(srcFolderPath)) && !(await fs.pathExists(srcZipPath))) {
if (
(await fs.pathExists(srcFolderPath)) &&
!(await fs.pathExists(srcZipPath))
) {
await zip(srcFolderPath, srcZipPath);
await fs.remove(srcFolderPath);
}

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { DisposableObject } from './pure/disposable-object';
import * as path from "path";
import { DisposableObject } from "./pure/disposable-object";
import {
Event,
EventEmitter,
@@ -9,36 +9,36 @@ import {
Uri,
window,
env,
} from 'vscode';
import * as fs from 'fs-extra';
} from "vscode";
import * as fs from "fs-extra";
import {
DatabaseChangedEvent,
DatabaseItem,
DatabaseManager,
} from './databases';
} from "./databases";
import {
commandRunner,
commandRunnerWithProgress,
ProgressCallback,
} from './commandRunner';
} from "./commandRunner";
import {
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder,
showAndLogErrorMessage
} from './helpers';
import { logger } from './logging';
showAndLogErrorMessage,
} from "./helpers";
import { logger } from "./logging";
import {
importArchiveDatabase,
promptImportGithubDatabase,
promptImportInternetDatabase,
promptImportLgtmDatabase,
} from './databaseFetcher';
import { CancellationToken } from 'vscode';
import { asyncFilter, getErrorMessage } from './pure/helpers-pure';
import { Credentials } from './authentication';
import { QueryRunner } from './queryRunner';
import { isCanary } from './config';
} from "./databaseFetcher";
import { CancellationToken } from "vscode";
import { asyncFilter, getErrorMessage } from "./pure/helpers-pure";
import { Credentials } from "./authentication";
import { QueryRunner } from "./queryRunner";
import { isCanary } from "./config";
type ThemableIconPath = { light: string; dark: string } | string;
@@ -46,20 +46,20 @@ type ThemableIconPath = { light: string; dark: string } | string;
* Path to icons to display next to currently selected database.
*/
const SELECTED_DATABASE_ICON: ThemableIconPath = {
light: 'media/light/check.svg',
dark: 'media/dark/check.svg',
light: "media/light/check.svg",
dark: "media/dark/check.svg",
};
/**
* Path to icon to display next to an invalid database.
*/
const INVALID_DATABASE_ICON: ThemableIconPath = 'media/red-x.svg';
const INVALID_DATABASE_ICON: ThemableIconPath = "media/red-x.svg";
function joinThemableIconPath(
base: string,
iconPath: ThemableIconPath
iconPath: ThemableIconPath,
): ThemableIconPath {
if (typeof iconPath == 'object')
if (typeof iconPath == "object")
return {
light: path.join(base, iconPath.light),
dark: path.join(base, iconPath.dark),
@@ -68,25 +68,29 @@ function joinThemableIconPath(
}
enum SortOrder {
NameAsc = 'NameAsc',
NameDesc = 'NameDesc',
DateAddedAsc = 'DateAddedAsc',
DateAddedDesc = 'DateAddedDesc',
NameAsc = "NameAsc",
NameDesc = "NameDesc",
DateAddedAsc = "DateAddedAsc",
DateAddedDesc = "DateAddedDesc",
}
/**
* Tree data provider for the databases view.
*/
class DatabaseTreeDataProvider extends DisposableObject
implements TreeDataProvider<DatabaseItem> {
class DatabaseTreeDataProvider
extends DisposableObject
implements TreeDataProvider<DatabaseItem>
{
private _sortOrder = SortOrder.NameAsc;
private readonly _onDidChangeTreeData = this.push(new EventEmitter<DatabaseItem | undefined>());
private readonly _onDidChangeTreeData = this.push(
new EventEmitter<DatabaseItem | undefined>(),
);
private currentDatabaseItem: DatabaseItem | undefined;
constructor(
private databaseManager: DatabaseManager,
private readonly extensionPath: string
private readonly extensionPath: string,
) {
super();
@@ -94,13 +98,13 @@ class DatabaseTreeDataProvider extends DisposableObject
this.push(
this.databaseManager.onDidChangeDatabaseItem(
this.handleDidChangeDatabaseItem
)
this.handleDidChangeDatabaseItem,
),
);
this.push(
this.databaseManager.onDidChangeCurrentDatabaseItem(
this.handleDidChangeCurrentDatabaseItem
)
this.handleDidChangeCurrentDatabaseItem,
),
);
}
@@ -118,7 +122,7 @@ class DatabaseTreeDataProvider extends DisposableObject
};
private handleDidChangeCurrentDatabaseItem = (
event: DatabaseChangedEvent
event: DatabaseChangedEvent,
): void => {
if (this.currentDatabaseItem) {
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
@@ -134,13 +138,13 @@ class DatabaseTreeDataProvider extends DisposableObject
if (element === this.currentDatabaseItem) {
item.iconPath = joinThemableIconPath(
this.extensionPath,
SELECTED_DATABASE_ICON
SELECTED_DATABASE_ICON,
);
item.contextValue = 'currentDatabase';
item.contextValue = "currentDatabase";
} else if (element.error !== undefined) {
item.iconPath = joinThemableIconPath(
this.extensionPath,
INVALID_DATABASE_ICON
INVALID_DATABASE_ICON,
);
}
item.tooltip = element.databaseUri.fsPath;
@@ -204,11 +208,11 @@ function getFirst(list: Uri[] | undefined): Uri | undefined {
*/
async function chooseDatabaseDir(byFolder: boolean): Promise<Uri | undefined> {
const chosen = await window.showOpenDialog({
openLabel: byFolder ? 'Choose Database folder' : 'Choose Database archive',
openLabel: byFolder ? "Choose Database folder" : "Choose Database archive",
canSelectFiles: !byFolder,
canSelectFolders: byFolder,
canSelectMany: false,
filters: byFolder ? {} : { Archives: ['zip'] },
filters: byFolder ? {} : { Archives: ["zip"] },
});
return getFirst(chosen);
}
@@ -221,173 +225,165 @@ export class DatabaseUI extends DisposableObject {
private readonly queryServer: QueryRunner | undefined,
private readonly storagePath: string,
readonly extensionPath: string,
private readonly getCredentials: () => Promise<Credentials>
private readonly getCredentials: () => Promise<Credentials>,
) {
super();
this.treeDataProvider = this.push(
new DatabaseTreeDataProvider(databaseManager, extensionPath)
new DatabaseTreeDataProvider(databaseManager, extensionPath),
);
this.push(
window.createTreeView('codeQLDatabases', {
window.createTreeView("codeQLDatabases", {
treeDataProvider: this.treeDataProvider,
canSelectMany: true,
})
}),
);
}
init() {
void logger.log('Registering database panel commands.');
void logger.log("Registering database panel commands.");
this.push(
commandRunnerWithProgress(
'codeQL.setCurrentDatabase',
"codeQL.setCurrentDatabase",
this.handleSetCurrentDatabase,
{
title: 'Importing database from archive',
}
)
title: "Importing database from archive",
},
),
);
this.push(
commandRunnerWithProgress(
'codeQL.upgradeCurrentDatabase',
"codeQL.upgradeCurrentDatabase",
this.handleUpgradeCurrentDatabase,
{
title: 'Upgrading current database',
title: "Upgrading current database",
cancellable: true,
}
)
},
),
);
this.push(
commandRunnerWithProgress(
'codeQL.clearCache',
this.handleClearCache,
{
title: 'Clearing Cache',
})
commandRunnerWithProgress("codeQL.clearCache", this.handleClearCache, {
title: "Clearing Cache",
}),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseFolder',
"codeQLDatabases.chooseDatabaseFolder",
this.handleChooseDatabaseFolder,
{
title: 'Adding database from folder',
}
)
title: "Adding database from folder",
},
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseArchive',
"codeQLDatabases.chooseDatabaseArchive",
this.handleChooseDatabaseArchive,
{
title: 'Adding database from archive',
}
)
title: "Adding database from archive",
},
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseInternet',
"codeQLDatabases.chooseDatabaseInternet",
this.handleChooseDatabaseInternet,
{
title: 'Adding database from URL',
}
)
title: "Adding database from URL",
},
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseGithub',
async (
progress: ProgressCallback,
token: CancellationToken
) => {
const credentials = isCanary() ? await this.getCredentials() : undefined;
"codeQLDatabases.chooseDatabaseGithub",
async (progress: ProgressCallback, token: CancellationToken) => {
const credentials = isCanary()
? await this.getCredentials()
: undefined;
await this.handleChooseDatabaseGithub(credentials, progress, token);
},
{
title: 'Adding database from GitHub',
})
title: "Adding database from GitHub",
},
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.chooseDatabaseLgtm',
"codeQLDatabases.chooseDatabaseLgtm",
this.handleChooseDatabaseLgtm,
{
title: 'Adding database from LGTM',
})
title: "Adding database from LGTM",
},
),
);
this.push(
commandRunner(
'codeQLDatabases.setCurrentDatabase',
this.handleMakeCurrentDatabase
)
"codeQLDatabases.setCurrentDatabase",
this.handleMakeCurrentDatabase,
),
);
this.push(
commandRunner("codeQLDatabases.sortByName", this.handleSortByName),
);
this.push(
commandRunner(
'codeQLDatabases.sortByName',
this.handleSortByName
)
);
this.push(
commandRunner(
'codeQLDatabases.sortByDateAdded',
this.handleSortByDateAdded
)
"codeQLDatabases.sortByDateAdded",
this.handleSortByDateAdded,
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.removeDatabase',
"codeQLDatabases.removeDatabase",
this.handleRemoveDatabase,
{
title: 'Removing database',
cancellable: false
}
)
title: "Removing database",
cancellable: false,
},
),
);
this.push(
commandRunnerWithProgress(
'codeQLDatabases.upgradeDatabase',
"codeQLDatabases.upgradeDatabase",
this.handleUpgradeDatabase,
{
title: 'Upgrading database',
title: "Upgrading database",
cancellable: true,
}
)
},
),
);
this.push(
commandRunner(
'codeQLDatabases.renameDatabase',
this.handleRenameDatabase
)
"codeQLDatabases.renameDatabase",
this.handleRenameDatabase,
),
);
this.push(
commandRunner(
'codeQLDatabases.openDatabaseFolder',
this.handleOpenFolder
)
"codeQLDatabases.openDatabaseFolder",
this.handleOpenFolder,
),
);
this.push(
commandRunner("codeQLDatabases.addDatabaseSource", this.handleAddSource),
);
this.push(
commandRunner(
'codeQLDatabases.addDatabaseSource',
this.handleAddSource
)
);
this.push(
commandRunner(
'codeQLDatabases.removeOrphanedDatabases',
this.handleRemoveOrphanedDatabases
)
"codeQLDatabases.removeOrphanedDatabases",
this.handleRemoveOrphanedDatabases,
),
);
}
private handleMakeCurrentDatabase = async (
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Promise<void> => {
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
};
handleChooseDatabaseFolder = async (
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<void> => {
try {
await this.chooseAndSetDatabase(true, progress, token);
@@ -397,14 +393,16 @@ export class DatabaseUI extends DisposableObject {
};
handleRemoveOrphanedDatabases = async (): Promise<void> => {
void logger.log('Removing orphaned databases from workspace storage.');
void logger.log("Removing orphaned databases from workspace storage.");
let dbDirs = undefined;
if (
!(await fs.pathExists(this.storagePath)) ||
!(await fs.stat(this.storagePath)).isDirectory()
) {
void logger.log('Missing or invalid storage directory. Not trying to remove orphaned databases.');
void logger.log(
"Missing or invalid storage directory. Not trying to remove orphaned databases.",
);
return;
}
@@ -412,49 +410,51 @@ export class DatabaseUI extends DisposableObject {
// read directory
(await fs.readdir(this.storagePath, { withFileTypes: true }))
// remove non-directories
.filter(dirent => dirent.isDirectory())
.filter((dirent) => dirent.isDirectory())
// get the full path
.map(dirent => path.join(this.storagePath, dirent.name))
.map((dirent) => path.join(this.storagePath, dirent.name))
// remove databases still in workspace
.filter(dbDir => {
.filter((dbDir) => {
const dbUri = Uri.file(dbDir);
return this.databaseManager.databaseItems.every(item => item.databaseUri.fsPath !== dbUri.fsPath);
return this.databaseManager.databaseItems.every(
(item) => item.databaseUri.fsPath !== dbUri.fsPath,
);
});
// remove non-databases
dbDirs = await asyncFilter(dbDirs, isLikelyDatabaseRoot);
if (!dbDirs.length) {
void logger.log('No orphaned databases found.');
void logger.log("No orphaned databases found.");
return;
}
// delete
const failures = [] as string[];
await Promise.all(
dbDirs.map(async dbDir => {
dbDirs.map(async (dbDir) => {
try {
void logger.log(`Deleting orphaned database '${dbDir}'.`);
await fs.remove(dbDir);
} catch (e) {
failures.push(`${path.basename(dbDir)}`);
}
})
}),
);
if (failures.length) {
const dirname = path.dirname(failures[0]);
void showAndLogErrorMessage(
`Failed to delete unused databases (${failures.join(', ')
}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`
`Failed to delete unused databases (${failures.join(
", ",
)}).\nTo delete unused databases, please remove them manually from the storage folder ${dirname}.`,
);
}
};
handleChooseDatabaseArchive = async (
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<void> => {
try {
await this.chooseAndSetDatabase(false, progress, token);
@@ -465,21 +465,21 @@ export class DatabaseUI extends DisposableObject {
handleChooseDatabaseInternet = async (
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<DatabaseItem | undefined> => {
return await promptImportInternetDatabase(
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer
this.queryServer?.cliServer,
);
};
handleChooseDatabaseGithub = async (
credentials: Credentials | undefined,
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<DatabaseItem | undefined> => {
return await promptImportGithubDatabase(
this.databaseManager,
@@ -487,26 +487,26 @@ export class DatabaseUI extends DisposableObject {
credentials,
progress,
token,
this.queryServer?.cliServer
this.queryServer?.cliServer,
);
};
handleChooseDatabaseLgtm = async (
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<DatabaseItem | undefined> => {
return await promptImportLgtmDatabase(
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer
this.queryServer?.cliServer,
);
};
async tryUpgradeCurrentDatabase(
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
) {
await this.handleUpgradeCurrentDatabase(progress, token);
}
@@ -532,9 +532,10 @@ export class DatabaseUI extends DisposableObject {
token: CancellationToken,
): Promise<void> => {
await this.handleUpgradeDatabase(
progress, token,
progress,
token,
this.databaseManager.currentDatabaseItem,
[]
[],
);
};
@@ -546,27 +547,29 @@ export class DatabaseUI extends DisposableObject {
): Promise<void> => {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => this.handleUpgradeDatabase(progress, token, dbItem, []))
multiSelect.map((dbItem) =>
this.handleUpgradeDatabase(progress, token, dbItem, []),
),
);
}
if (this.queryServer === undefined) {
throw new Error(
'Received request to upgrade database, but there is no running query server.'
"Received request to upgrade database, but there is no running query server.",
);
}
if (databaseItem === undefined) {
throw new Error(
'Received request to upgrade database, but no database was provided.'
"Received request to upgrade database, but no database was provided.",
);
}
if (databaseItem.contents === undefined) {
throw new Error(
'Received request to upgrade database, but database contents could not be found.'
"Received request to upgrade database, but database contents could not be found.",
);
}
if (databaseItem.contents.dbSchemeUri === undefined) {
throw new Error(
'Received request to upgrade database, but database has no schema.'
"Received request to upgrade database, but database has no schema.",
);
}
@@ -575,7 +578,7 @@ export class DatabaseUI extends DisposableObject {
await this.queryServer.upgradeDatabaseExplicit(
databaseItem,
progress,
token
token,
);
};
@@ -590,7 +593,7 @@ export class DatabaseUI extends DisposableObject {
await this.queryServer.clearCacheInDatabase(
this.databaseManager.currentDatabaseItem,
progress,
token
token,
);
}
};
@@ -602,14 +605,14 @@ export class DatabaseUI extends DisposableObject {
): Promise<void> => {
try {
// Assume user has selected an archive if the file has a .zip extension
if (uri.path.endsWith('.zip')) {
if (uri.path.endsWith(".zip")) {
await importArchiveDatabase(
uri.toString(true),
this.databaseManager,
this.storagePath,
progress,
token,
this.queryServer?.cliServer
this.queryServer?.cliServer,
);
} else {
await this.setCurrentDatabase(progress, token, uri);
@@ -617,7 +620,9 @@ export class DatabaseUI extends DisposableObject {
} catch (e) {
// rethrow and let this be handled by default error handling.
throw new Error(
`Could not set database to ${path.basename(uri.fsPath)}. Reason: ${getErrorMessage(e)}`
`Could not set database to ${path.basename(
uri.fsPath,
)}. Reason: ${getErrorMessage(e)}`,
);
}
};
@@ -626,25 +631,31 @@ export class DatabaseUI extends DisposableObject {
progress: ProgressCallback,
token: CancellationToken,
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
multiSelect: DatabaseItem[] | undefined,
): Promise<void> => {
if (multiSelect?.length) {
await Promise.all(multiSelect.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem)
));
await Promise.all(
multiSelect.map((dbItem) =>
this.databaseManager.removeDatabaseItem(progress, token, dbItem),
),
);
} else {
await this.databaseManager.removeDatabaseItem(progress, token, databaseItem);
await this.databaseManager.removeDatabaseItem(
progress,
token,
databaseItem,
);
}
};
private handleRenameDatabase = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
multiSelect: DatabaseItem[] | undefined,
): Promise<void> => {
this.assertSingleDatabase(multiSelect);
const newName = await window.showInputBox({
prompt: 'Choose new database name',
prompt: "Choose new database name",
value: databaseItem.name,
});
@@ -655,11 +666,11 @@ export class DatabaseUI extends DisposableObject {
private handleOpenFolder = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
multiSelect: DatabaseItem[] | undefined,
): Promise<void> => {
if (multiSelect?.length) {
await Promise.all(
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri))
multiSelect.map((dbItem) => env.openExternal(dbItem.databaseUri)),
);
} else {
await env.openExternal(databaseItem.databaseUri);
@@ -673,7 +684,7 @@ export class DatabaseUI extends DisposableObject {
*/
private handleAddSource = async (
databaseItem: DatabaseItem,
multiSelect: DatabaseItem[] | undefined
multiSelect: DatabaseItem[] | undefined,
): Promise<void> => {
if (multiSelect?.length) {
for (const dbItem of multiSelect) {
@@ -691,7 +702,7 @@ export class DatabaseUI extends DisposableObject {
*/
public async getDatabaseItem(
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<DatabaseItem | undefined> {
if (this.databaseManager.currentDatabaseItem === undefined) {
await this.chooseAndSetDatabase(false, progress, token);
@@ -703,11 +714,15 @@ export class DatabaseUI extends DisposableObject {
private async setCurrentDatabase(
progress: ProgressCallback,
token: CancellationToken,
uri: Uri
uri: Uri,
): Promise<DatabaseItem | undefined> {
let databaseItem = this.databaseManager.findDatabaseItem(uri);
if (databaseItem === undefined) {
databaseItem = await this.databaseManager.openDatabase(progress, token, uri);
databaseItem = await this.databaseManager.openDatabase(
progress,
token,
uri,
);
}
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
@@ -741,7 +756,7 @@ export class DatabaseUI extends DisposableObject {
this.storagePath,
progress,
token,
this.queryServer?.cliServer
this.queryServer?.cliServer,
);
}
}
@@ -771,7 +786,7 @@ export class DatabaseUI extends DisposableObject {
private assertSingleDatabase(
multiSelect: DatabaseItem[] = [],
message = 'Please select a single database.'
message = "Please select a single database.",
) {
if (multiSelect.length > 1) {
throw new Error(message);

View File

@@ -1,24 +1,26 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as path from 'path';
import * as vscode from 'vscode';
import * as cli from './cli';
import { ExtensionContext } from 'vscode';
import * as fs from "fs-extra";
import * as glob from "glob-promise";
import * as path from "path";
import * as vscode from "vscode";
import * as cli from "./cli";
import { ExtensionContext } from "vscode";
import {
showAndLogErrorMessage,
showAndLogWarningMessage,
showAndLogInformationMessage,
isLikelyDatabaseRoot
} from './helpers';
isLikelyDatabaseRoot,
} from "./helpers";
import { ProgressCallback, withProgress } from "./commandRunner";
import {
ProgressCallback,
withProgress
} from './commandRunner';
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from './pure/disposable-object';
import { Logger, logger } from './logging';
import { getErrorMessage } from './pure/helpers-pure';
import { QueryRunner } from './queryRunner';
zipArchiveScheme,
encodeArchiveBasePath,
decodeSourceArchiveUri,
encodeSourceArchiveUri,
} from "./archive-filesystem-provider";
import { DisposableObject } from "./pure/disposable-object";
import { Logger, logger } from "./logging";
import { getErrorMessage } from "./pure/helpers-pure";
import { QueryRunner } from "./queryRunner";
/**
* databases.ts
@@ -34,13 +36,13 @@ import { QueryRunner } from './queryRunner';
* The name of the key in the workspaceState dictionary in which we
* persist the current database across sessions.
*/
const CURRENT_DB = 'currentDatabase';
const CURRENT_DB = "currentDatabase";
/**
* The name of the key in the workspaceState dictionary in which we
* persist the list of databases across sessions.
*/
const DB_LIST = 'databaseList';
const DB_LIST = "databaseList";
export interface DatabaseOptions {
displayName?: string;
@@ -67,7 +69,7 @@ export enum DatabaseKind {
/** A CodeQL database */
Database,
/** A raw QL dataset */
RawDataset
RawDataset,
}
export interface DatabaseContents {
@@ -89,33 +91,35 @@ export interface DatabaseContents {
* An error thrown when we cannot find a valid database in a putative
* database directory.
*/
class InvalidDatabaseError extends Error {
}
class InvalidDatabaseError extends Error {}
async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
/*
* Look directly in the root
*/
let dbRelativePaths = await glob('db-*/', {
cwd: parentDirectory
let dbRelativePaths = await glob("db-*/", {
cwd: parentDirectory,
});
if (dbRelativePaths.length === 0) {
/*
* Check If they are in the old location
*/
dbRelativePaths = await glob('working/db-*/', {
cwd: parentDirectory
dbRelativePaths = await glob("working/db-*/", {
cwd: parentDirectory,
});
}
if (dbRelativePaths.length === 0) {
throw new InvalidDatabaseError(`'${parentDirectory}' does not contain a dataset directory.`);
throw new InvalidDatabaseError(
`'${parentDirectory}' does not contain a dataset directory.`,
);
}
const dbAbsolutePath = path.join(parentDirectory, dbRelativePaths[0]);
if (dbRelativePaths.length > 1) {
void showAndLogWarningMessage(`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`);
void showAndLogWarningMessage(
`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`,
);
}
return vscode.Uri.file(dbAbsolutePath);
@@ -123,13 +127,13 @@ async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
// exported for testing
export async function findSourceArchive(
databasePath: string
databasePath: string,
): Promise<vscode.Uri | undefined> {
const relativePaths = ['src', 'output/src_archive'];
const relativePaths = ["src", "output/src_archive"];
for (const relativePath of relativePaths) {
const basePath = path.join(databasePath, relativePath);
const zipPath = basePath + '.zip';
const zipPath = basePath + ".zip";
// Prefer using a zip archive over a directory.
if (await fs.pathExists(zipPath)) {
@@ -140,7 +144,7 @@ export async function findSourceArchive(
}
void showAndLogInformationMessage(
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`
`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`,
);
return undefined;
}
@@ -148,7 +152,6 @@ export async function findSourceArchive(
async function resolveDatabase(
databasePath: string,
): Promise<DatabaseContents> {
const name = path.basename(databasePath);
// Look for dataset and source archive.
@@ -159,30 +162,36 @@ async function resolveDatabase(
kind: DatabaseKind.Database,
name,
datasetUri,
sourceArchiveUri
sourceArchiveUri,
};
}
/** Gets the relative paths of all `.dbscheme` files in the given directory. */
async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
return await glob('*.dbscheme', { cwd: dbDirectory });
return await glob("*.dbscheme", { cwd: dbDirectory });
}
async function resolveDatabaseContents(
uri: vscode.Uri,
): Promise<DatabaseContents> {
if (uri.scheme !== 'file') {
throw new Error(`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`);
if (uri.scheme !== "file") {
throw new Error(
`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`,
);
}
const databasePath = uri.fsPath;
if (!await fs.pathExists(databasePath)) {
throw new InvalidDatabaseError(`Database '${databasePath}' does not exist.`);
if (!(await fs.pathExists(databasePath))) {
throw new InvalidDatabaseError(
`Database '${databasePath}' does not exist.`,
);
}
const contents = await resolveDatabase(databasePath);
if (contents === undefined) {
throw new InvalidDatabaseError(`'${databasePath}' is not a valid database.`);
throw new InvalidDatabaseError(
`'${databasePath}' is not a valid database.`,
);
}
// Look for a single dbscheme file within the database.
@@ -190,12 +199,17 @@ async function resolveDatabaseContents(
const dbPath = contents.datasetUri.fsPath;
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
if (dbSchemeFiles.length === 0) {
throw new InvalidDatabaseError(`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`);
}
else if (dbSchemeFiles.length > 1) {
throw new InvalidDatabaseError(`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`);
throw new InvalidDatabaseError(
`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`,
);
} else if (dbSchemeFiles.length > 1) {
throw new InvalidDatabaseError(
`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`,
);
} else {
contents.dbSchemeUri = vscode.Uri.file(path.resolve(dbPath, dbSchemeFiles[0]));
contents.dbSchemeUri = vscode.Uri.file(
path.resolve(dbPath, dbSchemeFiles[0]),
);
}
return contents;
}
@@ -283,16 +297,16 @@ export interface DatabaseItem {
}
export enum DatabaseEventKind {
Add = 'Add',
Remove = 'Remove',
Add = "Add",
Remove = "Remove",
// Fired when databases are refreshed from persisted state
Refresh = 'Refresh',
Refresh = "Refresh",
// Fired when the current database changes
Change = 'Change',
Change = "Change",
Rename = 'Rename'
Rename = "Rename",
}
export interface DatabaseChangedEvent {
@@ -311,7 +325,7 @@ export class DatabaseItemImpl implements DatabaseItem {
public readonly databaseUri: vscode.Uri,
contents: DatabaseContents | undefined,
private options: FullDatabaseOptions,
private readonly onChanged: (event: DatabaseChangedEvent) => void
private readonly onChanged: (event: DatabaseChangedEvent) => void,
) {
this._contents = contents;
}
@@ -319,11 +333,9 @@ export class DatabaseItemImpl implements DatabaseItem {
public get name(): string {
if (this.options.displayName) {
return this.options.displayName;
}
else if (this._contents) {
} else if (this._contents) {
return this._contents.name;
}
else {
} else {
return path.basename(this.databaseUri.fsPath);
}
}
@@ -333,7 +345,7 @@ export class DatabaseItemImpl implements DatabaseItem {
}
public get sourceArchive(): vscode.Uri | undefined {
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
if (this.options.ignoreSourceArchive || this._contents === undefined) {
return undefined;
} else {
return this._contents.sourceArchiveUri;
@@ -365,7 +377,7 @@ export class DatabaseItemImpl implements DatabaseItem {
} finally {
this.onChanged({
kind: DatabaseEventKind.Refresh,
item: this
item: this,
});
}
}
@@ -373,8 +385,10 @@ export class DatabaseItemImpl implements DatabaseItem {
public resolveSourceFile(uriStr: string | undefined): vscode.Uri {
const sourceArchive = this.sourceArchive;
const uri = uriStr ? vscode.Uri.parse(uriStr, true) : undefined;
if (uri && uri.scheme !== 'file') {
throw new Error(`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`);
if (uri && uri.scheme !== "file") {
throw new Error(
`Invalid uri scheme in ${uriStr}. Only 'file' is allowed.`,
);
}
if (!sourceArchive) {
if (uri) {
@@ -385,28 +399,29 @@ export class DatabaseItemImpl implements DatabaseItem {
}
if (uri) {
const relativeFilePath = decodeURI(uri.path).replace(':', '_').replace(/^\/*/, '');
const relativeFilePath = decodeURI(uri.path)
.replace(":", "_")
.replace(/^\/*/, "");
if (sourceArchive.scheme === zipArchiveScheme) {
const zipRef = decodeSourceArchiveUri(sourceArchive);
const pathWithinSourceArchive = zipRef.pathWithinSourceArchive === '/'
? relativeFilePath
: zipRef.pathWithinSourceArchive + '/' + relativeFilePath;
const pathWithinSourceArchive =
zipRef.pathWithinSourceArchive === "/"
? relativeFilePath
: zipRef.pathWithinSourceArchive + "/" + relativeFilePath;
return encodeSourceArchiveUri({
pathWithinSourceArchive,
sourceArchiveZipPath: zipRef.sourceArchiveZipPath,
});
} else {
let newPath = sourceArchive.path;
if (!newPath.endsWith('/')) {
if (!newPath.endsWith("/")) {
// Ensure a trailing slash.
newPath += '/';
newPath += "/";
}
newPath += relativeFilePath;
return sourceArchive.with({ path: newPath });
}
} else {
return sourceArchive;
}
@@ -418,7 +433,7 @@ export class DatabaseItemImpl implements DatabaseItem {
public getPersistedState(): PersistedDatabaseItem {
return {
uri: this.databaseUri.toString(true),
options: this.options
options: this.options,
};
}
@@ -443,7 +458,9 @@ export class DatabaseItemImpl implements DatabaseItem {
* Returns `sourceLocationPrefix` of database. Requires that the database
* has a `.dbinfo` file, which is the source of the prefix.
*/
public async getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string> {
public async getSourceLocationPrefix(
server: cli.CodeQLCliServer,
): Promise<string> {
const dbInfo = await this.getDbInfo(server);
return dbInfo.sourceLocationPrefix;
}
@@ -457,7 +474,7 @@ export class DatabaseItemImpl implements DatabaseItem {
}
public get language() {
return this.options.language || '';
return this.options.language || "";
}
/**
@@ -465,7 +482,7 @@ export class DatabaseItemImpl implements DatabaseItem {
*/
public getSourceArchiveExplorerUri(): vscode.Uri {
const sourceArchive = this.sourceArchive;
if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith('.zip')) {
if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith(".zip")) {
throw new Error(this.verifyZippedSources());
}
return encodeArchiveBasePath(sourceArchive.fsPath);
@@ -477,7 +494,7 @@ export class DatabaseItemImpl implements DatabaseItem {
return `${this.name} has no source archive.`;
}
if (!sourceArchive.fsPath.endsWith('.zip')) {
if (!sourceArchive.fsPath.endsWith(".zip")) {
return `${this.name} has a source folder that is unzipped.`;
}
return;
@@ -487,26 +504,28 @@ export class DatabaseItemImpl implements DatabaseItem {
* Holds if `uri` belongs to this database's source archive.
*/
public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean {
if (this.sourceArchive === undefined)
return false;
return uri.scheme === zipArchiveScheme &&
decodeSourceArchiveUri(uri).sourceArchiveZipPath === this.sourceArchive.fsPath;
if (this.sourceArchive === undefined) return false;
return (
uri.scheme === zipArchiveScheme &&
decodeSourceArchiveUri(uri).sourceArchiveZipPath ===
this.sourceArchive.fsPath
);
}
public async isAffectedByTest(testPath: string): Promise<boolean> {
const databasePath = this.databaseUri.fsPath;
if (!databasePath.endsWith('.testproj')) {
if (!databasePath.endsWith(".testproj")) {
return false;
}
try {
const stats = await fs.stat(testPath);
if (stats.isDirectory()) {
return !path.relative(testPath, databasePath).startsWith('..');
return !path.relative(testPath, databasePath).startsWith("..");
} else {
// database for /one/two/three/test.ql is at /one/two/three/three.testproj
const testdir = path.dirname(testPath);
const testdirbase = path.basename(testdir);
return databasePath == path.join(testdir, testdirbase + '.testproj');
return databasePath == path.join(testdir, testdirbase + ".testproj");
}
} catch {
// No information available for test path - assume database is unaffected.
@@ -520,14 +539,19 @@ export class DatabaseItemImpl implements DatabaseItem {
* `event` fires. If waiting for the event takes too long (by default
* >1000ms) log a warning, and resolve to undefined.
*/
function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | undefined> {
function eventFired<T>(
event: vscode.Event<T>,
timeoutMs = 1000,
): Promise<T | undefined> {
return new Promise((res, _rej) => {
const timeout = setTimeout(() => {
void logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
void logger.log(
`Waiting for event ${event} timed out after ${timeoutMs}ms`,
);
res(undefined);
dispose();
}, timeoutMs);
const disposable = event(e => {
const disposable = event((e) => {
res(e);
dispose();
});
@@ -539,12 +563,17 @@ function eventFired<T>(event: vscode.Event<T>, timeoutMs = 1000): Promise<T | un
}
export class DatabaseManager extends DisposableObject {
private readonly _onDidChangeDatabaseItem = this.push(new vscode.EventEmitter<DatabaseChangedEvent>());
private readonly _onDidChangeDatabaseItem = this.push(
new vscode.EventEmitter<DatabaseChangedEvent>(),
);
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
private readonly _onDidChangeCurrentDatabaseItem = this.push(new vscode.EventEmitter<DatabaseChangedEvent>());
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
private readonly _onDidChangeCurrentDatabaseItem = this.push(
new vscode.EventEmitter<DatabaseChangedEvent>(),
);
readonly onDidChangeCurrentDatabaseItem =
this._onDidChangeCurrentDatabaseItem.event;
private readonly _databaseItems: DatabaseItem[] = [];
private _currentDatabaseItem: DatabaseItem | undefined = undefined;
@@ -553,7 +582,7 @@ export class DatabaseManager extends DisposableObject {
private readonly ctx: ExtensionContext,
private readonly qs: QueryRunner,
private readonly cli: cli.CodeQLCliServer,
public logger: Logger
public logger: Logger,
) {
super();
@@ -564,21 +593,26 @@ export class DatabaseManager extends DisposableObject {
progress: ProgressCallback,
token: vscode.CancellationToken,
uri: vscode.Uri,
displayName?: string
displayName?: string,
): Promise<DatabaseItem> {
const contents = await resolveDatabaseContents(uri);
// Ignore the source archive for QLTest databases by default.
const isQLTestDatabase = path.extname(uri.fsPath) === '.testproj';
const isQLTestDatabase = path.extname(uri.fsPath) === ".testproj";
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: isQLTestDatabase,
// If a displayName is not passed in, the basename of folder containing the database is used.
displayName,
dateAdded: Date.now(),
language: await this.getPrimaryLanguage(uri.fsPath)
language: await this.getPrimaryLanguage(uri.fsPath),
};
const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (event) => {
this._onDidChangeDatabaseItem.fire(event);
});
const databaseItem = new DatabaseItemImpl(
uri,
contents,
fullOptions,
(event) => {
this._onDidChangeDatabaseItem.fire(event);
},
);
await this.addDatabaseItem(progress, token, databaseItem);
await this.addDatabaseSourceArchiveFolder(databaseItem);
@@ -588,18 +622,20 @@ export class DatabaseManager extends DisposableObject {
private async reregisterDatabases(
progress: ProgressCallback,
token: vscode.CancellationToken
token: vscode.CancellationToken,
) {
let completed = 0;
await Promise.all(this._databaseItems.map(async (databaseItem) => {
await this.registerDatabase(progress, token, databaseItem);
completed++;
progress({
maxStep: this._databaseItems.length,
step: completed,
message: 'Re-registering databases'
});
}));
await Promise.all(
this._databaseItems.map(async (databaseItem) => {
await this.registerDatabase(progress, token, databaseItem);
completed++;
progress({
maxStep: this._databaseItems.length,
step: completed,
message: "Re-registering databases",
});
}),
);
}
public async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
@@ -626,12 +662,16 @@ export class DatabaseManager extends DisposableObject {
}
const uri = item.getSourceArchiveExplorerUri();
void logger.log(`Adding workspace folder for ${item.name} source archive at index ${end}`);
void logger.log(
`Adding workspace folder for ${item.name} source archive at index ${end}`,
);
if ((vscode.workspace.workspaceFolders || []).length < 2) {
// Adding this workspace folder makes the workspace
// multi-root, which may surprise the user. Let them know
// we're doing this.
void vscode.window.showInformationMessage(`Adding workspace folder for source archive of database ${item.name}.`);
void vscode.window.showInformationMessage(
`Adding workspace folder for source archive of database ${item.name}.`,
);
}
vscode.workspace.updateWorkspaceFolders(end, 0, {
name: `[${item.name} source archive]`,
@@ -646,21 +686,20 @@ export class DatabaseManager extends DisposableObject {
private async createDatabaseItemFromPersistedState(
progress: ProgressCallback,
token: vscode.CancellationToken,
state: PersistedDatabaseItem
state: PersistedDatabaseItem,
): Promise<DatabaseItem> {
let displayName: string | undefined = undefined;
let ignoreSourceArchive = false;
let dateAdded = undefined;
let language = undefined;
if (state.options) {
if (typeof state.options.displayName === 'string') {
if (typeof state.options.displayName === "string") {
displayName = state.options.displayName;
}
if (typeof state.options.ignoreSourceArchive === 'boolean') {
if (typeof state.options.ignoreSourceArchive === "boolean") {
ignoreSourceArchive = state.options.ignoreSourceArchive;
}
if (typeof state.options.dateAdded === 'number') {
if (typeof state.options.dateAdded === "number") {
dateAdded = state.options.dateAdded;
}
language = state.options.language;
@@ -676,12 +715,16 @@ export class DatabaseManager extends DisposableObject {
ignoreSourceArchive,
displayName,
dateAdded,
language
language,
};
const item = new DatabaseItemImpl(dbBaseUri, undefined, fullOptions,
const item = new DatabaseItemImpl(
dbBaseUri,
undefined,
fullOptions,
(event) => {
this._onDidChangeDatabaseItem.fire(event);
});
},
);
// Avoid persisting the database state after adding since that should happen only after
// all databases have been added.
@@ -690,49 +733,72 @@ export class DatabaseManager extends DisposableObject {
}
public async loadPersistedState(): Promise<void> {
return withProgress({
location: vscode.ProgressLocation.Notification
},
return withProgress(
{
location: vscode.ProgressLocation.Notification,
},
async (progress, token) => {
const currentDatabaseUri = this.ctx.workspaceState.get<string>(CURRENT_DB);
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(DB_LIST, []);
const currentDatabaseUri =
this.ctx.workspaceState.get<string>(CURRENT_DB);
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
DB_LIST,
[],
);
let step = 0;
progress({
maxStep: databases.length,
message: 'Loading persisted databases',
step
message: "Loading persisted databases",
step,
});
try {
void this.logger.log(`Found ${databases.length} persisted databases: ${databases.map(db => db.uri).join(', ')}`);
void this.logger.log(
`Found ${databases.length} persisted databases: ${databases
.map((db) => db.uri)
.join(", ")}`,
);
for (const database of databases) {
progress({
maxStep: databases.length,
message: `Loading ${database.options?.displayName || 'databases'}`,
step: ++step
message: `Loading ${
database.options?.displayName || "databases"
}`,
step: ++step,
});
const databaseItem = await this.createDatabaseItemFromPersistedState(progress, token, database);
const databaseItem =
await this.createDatabaseItemFromPersistedState(
progress,
token,
database,
);
try {
await databaseItem.refresh();
await this.registerDatabase(progress, token, databaseItem);
if (currentDatabaseUri === database.uri) {
await this.setCurrentDatabaseItem(databaseItem, true);
}
void this.logger.log(`Loaded database ${databaseItem.name} at URI ${database.uri}.`);
void this.logger.log(
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
);
} catch (e) {
// When loading from persisted state, leave invalid databases in the list. They will be
// marked as invalid, and cannot be set as the current database.
void this.logger.log(`Error loading database ${database.uri}: ${e}.`);
void this.logger.log(
`Error loading database ${database.uri}: ${e}.`,
);
}
}
await this.updatePersistedDatabaseList();
} catch (e) {
// database list had an unexpected type - nothing to be done?
void showAndLogErrorMessage(`Database list loading failed: ${getErrorMessage(e)}`);
void showAndLogErrorMessage(
`Database list loading failed: ${getErrorMessage(e)}`,
);
}
void this.logger.log('Finished loading persisted databases.');
});
void this.logger.log("Finished loading persisted databases.");
},
);
}
public get databaseItems(): readonly DatabaseItem[] {
@@ -745,21 +811,24 @@ export class DatabaseManager extends DisposableObject {
public async setCurrentDatabaseItem(
item: DatabaseItem | undefined,
skipRefresh = false
skipRefresh = false,
): Promise<void> {
if (!skipRefresh && (item !== undefined)) {
await item.refresh(); // Will throw on invalid database.
if (!skipRefresh && item !== undefined) {
await item.refresh(); // Will throw on invalid database.
}
if (this._currentDatabaseItem !== item) {
this._currentDatabaseItem = item;
this.updatePersistedCurrentDatabaseItem();
await vscode.commands.executeCommand('setContext', 'codeQL.currentDatabaseItem', item?.name);
await vscode.commands.executeCommand(
"setContext",
"codeQL.currentDatabaseItem",
item?.name,
);
this._onDidChangeCurrentDatabaseItem.fire({
item,
kind: DatabaseEventKind.Change
kind: DatabaseEventKind.Change,
});
}
}
@@ -769,25 +838,33 @@ export class DatabaseManager extends DisposableObject {
* if there is one, and -1 otherwise.
*/
private getDatabaseWorkspaceFolderIndex(item: DatabaseItem): number {
return (vscode.workspace.workspaceFolders || [])
.findIndex(folder => item.belongsToSourceArchiveExplorerUri(folder.uri));
return (vscode.workspace.workspaceFolders || []).findIndex((folder) =>
item.belongsToSourceArchiveExplorerUri(folder.uri),
);
}
public findDatabaseItem(uri: vscode.Uri): DatabaseItem | undefined {
const uriString = uri.toString(true);
return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);
return this._databaseItems.find(
(item) => item.databaseUri.toString(true) === uriString,
);
}
public findDatabaseItemBySourceArchive(uri: vscode.Uri): DatabaseItem | undefined {
public findDatabaseItemBySourceArchive(
uri: vscode.Uri,
): DatabaseItem | undefined {
const uriString = uri.toString(true);
return this._databaseItems.find(item => item.sourceArchive && item.sourceArchive.toString(true) === uriString);
return this._databaseItems.find(
(item) =>
item.sourceArchive && item.sourceArchive.toString(true) === uriString,
);
}
private async addDatabaseItem(
progress: ProgressCallback,
token: vscode.CancellationToken,
item: DatabaseItem,
updatePersistedState = true
updatePersistedState = true,
) {
this._databaseItems.push(item);
@@ -804,7 +881,7 @@ export class DatabaseManager extends DisposableObject {
// note that we use undefined as the item in order to reset the entire tree
this._onDidChangeDatabaseItem.fire({
item: undefined,
kind: DatabaseEventKind.Add
kind: DatabaseEventKind.Add,
});
}
@@ -814,19 +891,21 @@ export class DatabaseManager extends DisposableObject {
this._onDidChangeDatabaseItem.fire({
// pass undefined so that the entire tree is rebuilt in order to re-sort
item: undefined,
kind: DatabaseEventKind.Rename
kind: DatabaseEventKind.Rename,
});
}
public async removeDatabaseItem(
progress: ProgressCallback,
token: vscode.CancellationToken,
item: DatabaseItem
item: DatabaseItem,
) {
if (this._currentDatabaseItem == item) {
this._currentDatabaseItem = undefined;
}
const index = this.databaseItems.findIndex(searchItem => searchItem === item);
const index = this.databaseItems.findIndex(
(searchItem) => searchItem === item,
);
if (index >= 0) {
this._databaseItems.splice(index, 1);
}
@@ -834,7 +913,7 @@ export class DatabaseManager extends DisposableObject {
// Delete folder from workspace, if it is still there
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(
folder => item.belongsToSourceArchiveExplorerUri(folder.uri)
(folder) => item.belongsToSourceArchiveExplorerUri(folder.uri),
);
if (folderIndex >= 0) {
void logger.log(`Removing workspace folder at index ${folderIndex}`);
@@ -846,16 +925,22 @@ export class DatabaseManager extends DisposableObject {
// Delete folder from file system only if it is controlled by the extension
if (this.isExtensionControlledLocation(item.databaseUri)) {
void logger.log('Deleting database from filesystem.');
void logger.log("Deleting database from filesystem.");
fs.remove(item.databaseUri.fsPath).then(
() => void logger.log(`Deleted '${item.databaseUri.fsPath}'`),
e => void logger.log(`Failed to delete '${item.databaseUri.fsPath}'. Reason: ${getErrorMessage(e)}`));
(e) =>
void logger.log(
`Failed to delete '${
item.databaseUri.fsPath
}'. Reason: ${getErrorMessage(e)}`,
),
);
}
// note that we use undefined as the item in order to reset the entire tree
this._onDidChangeDatabaseItem.fire({
item: undefined,
kind: DatabaseEventKind.Remove
kind: DatabaseEventKind.Remove,
});
}
@@ -875,12 +960,19 @@ export class DatabaseManager extends DisposableObject {
}
private updatePersistedCurrentDatabaseItem(): void {
void this.ctx.workspaceState.update(CURRENT_DB, this._currentDatabaseItem ?
this._currentDatabaseItem.databaseUri.toString(true) : undefined);
void this.ctx.workspaceState.update(
CURRENT_DB,
this._currentDatabaseItem
? this._currentDatabaseItem.databaseUri.toString(true)
: undefined,
);
}
private async updatePersistedDatabaseList(): Promise<void> {
await this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
await this.ctx.workspaceState.update(
DB_LIST,
this._databaseItems.map((item) => item.getPersistedState()),
);
}
private isExtensionControlledLocation(uri: vscode.Uri) {
@@ -902,7 +994,7 @@ export class DatabaseManager extends DisposableObject {
return undefined;
}
const dbInfo = await this.cli.resolveDatabase(dbPath);
return dbInfo.languages?.[0] || '';
return dbInfo.languages?.[0] || "";
}
}
@@ -911,7 +1003,9 @@ export class DatabaseManager extends DisposableObject {
* scripts returned by the cli's upgrade resolution.
*/
export function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {
const parentDirs = scripts.map(dir => path.dirname(dir));
const parentDirs = scripts.map((dir) => path.dirname(dir));
const uniqueParentDirs = new Set(parentDirs);
return Array.from(uniqueParentDirs).map(filePath => vscode.Uri.file(filePath));
return Array.from(uniqueParentDirs).map((filePath) =>
vscode.Uri.file(filePath),
);
}

View File

@@ -1,12 +1,12 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { cloneDbConfig, DbConfig } from './db-config';
import * as chokidar from 'chokidar';
import { DisposableObject } from '../../pure/disposable-object';
import { DbConfigValidator } from './db-config-validator';
import { ValueResult } from '../../common/value-result';
import { App } from '../../common/app';
import { AppEvent, AppEventEmitter } from '../../common/events';
import * as fs from "fs-extra";
import * as path from "path";
import { cloneDbConfig, DbConfig } from "./db-config";
import * as chokidar from "chokidar";
import { DisposableObject } from "../../pure/disposable-object";
import { DbConfigValidator } from "./db-config-validator";
import { ValueResult } from "../../common/value-result";
import { App } from "../../common/app";
import { AppEvent, AppEventEmitter } from "../../common/events";
export class DbConfigStore extends DisposableObject {
public readonly onDidChangeConfig: AppEvent<void>;
@@ -23,7 +23,7 @@ export class DbConfigStore extends DisposableObject {
super();
const storagePath = app.workspaceStoragePath || app.globalStoragePath;
this.configPath = path.join(storagePath, 'workspace-databases.json');
this.configPath = path.join(storagePath, "workspace-databases.json");
this.config = this.createEmptyConfig();
this.configErrors = [];
@@ -56,8 +56,10 @@ export class DbConfigStore extends DisposableObject {
}
private async loadConfig(): Promise<void> {
if (!await fs.pathExists(this.configPath)) {
await fs.writeJSON(this.configPath, this.createEmptyConfig(), { spaces: 2 });
if (!(await fs.pathExists(this.configPath))) {
await fs.writeJSON(this.configPath, this.createEmptyConfig(), {
spaces: 2,
});
}
await this.readConfig();
@@ -96,7 +98,7 @@ export class DbConfigStore extends DisposableObject {
}
private watchConfig(): void {
this.configWatcher = chokidar.watch(this.configPath).on('change', () => {
this.configWatcher = chokidar.watch(this.configPath).on("change", () => {
this.readConfigSync();
});
}
@@ -113,7 +115,7 @@ export class DbConfigStore extends DisposableObject {
lists: [],
databases: [],
},
}
},
};
}
}

View File

@@ -1,13 +1,16 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import Ajv from 'ajv';
import { DbConfig } from './db-config';
import * as fs from "fs-extra";
import * as path from "path";
import Ajv from "ajv";
import { DbConfig } from "./db-config";
export class DbConfigValidator {
private readonly schema: any;
constructor(extensionPath: string) {
const schemaPath = path.resolve(extensionPath, 'workspace-databases-schema.json');
const schemaPath = path.resolve(
extensionPath,
"workspace-databases-schema.json",
);
this.schema = fs.readJsonSync(schemaPath);
}
@@ -16,7 +19,9 @@ export class DbConfigValidator {
ajv.validate(this.schema, dbConfig);
if (ajv.errors) {
return ajv.errors.map((error) => `${error.instancePath} ${error.message}`);
return ajv.errors.map(
(error) => `${error.instancePath} ${error.message}`,
);
}
return [];

View File

@@ -16,8 +16,8 @@ export interface SelectedDbItem {
}
export enum SelectedDbItemKind {
ConfigDefined = 'configDefined',
RemoteSystemDefinedList = 'remoteSystemDefinedList',
ConfigDefined = "configDefined",
RemoteSystemDefinedList = "remoteSystemDefinedList",
}
export interface RemoteDbConfig {
@@ -52,10 +52,12 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
return {
databases: {
remote: {
repositoryLists: config.databases.remote.repositoryLists.map((list) => ({
name: list.name,
repositories: [...list.repositories],
})),
repositoryLists: config.databases.remote.repositoryLists.map(
(list) => ({
name: list.name,
repositories: [...list.repositories],
}),
),
owners: [...config.databases.remote.owners],
repositories: [...config.databases.remote.repositories],
},
@@ -67,9 +69,11 @@ export function cloneDbConfig(config: DbConfig): DbConfig {
databases: config.databases.local.databases.map((db) => ({ ...db })),
},
},
selected: config.selected ? {
kind: config.selected.kind,
value: config.selected.value,
} : undefined
selected: config.selected
? {
kind: config.selected.kind,
value: config.selected.value,
}
: undefined,
};
}

View File

@@ -1,14 +1,14 @@
// This file contains models that are used to represent the databases.
export enum DbItemKind {
RootLocal = 'RootLocal',
LocalList = 'LocalList',
LocalDatabase = 'LocalDatabase',
RootRemote = 'RootRemote',
RemoteSystemDefinedList = 'RemoteSystemDefinedList',
RemoteUserDefinedList = 'RemoteUserDefinedList',
RemoteOwner = 'RemoteOwner',
RemoteRepo = 'RemoteRepo'
RootLocal = "RootLocal",
LocalList = "LocalList",
LocalDatabase = "LocalDatabase",
RootRemote = "RootRemote",
RemoteSystemDefinedList = "RemoteSystemDefinedList",
RemoteUserDefinedList = "RemoteUserDefinedList",
RemoteOwner = "RemoteOwner",
RemoteRepo = "RemoteRepo",
}
export interface RootLocalDbItem {
@@ -16,9 +16,7 @@ export interface RootLocalDbItem {
children: LocalDbItem[];
}
export type LocalDbItem =
| LocalListDbItem
| LocalDatabaseDbItem;
export type LocalDbItem = LocalListDbItem | LocalDatabaseDbItem;
export interface LocalListDbItem {
kind: DbItemKind.LocalList;

View File

@@ -1,18 +1,15 @@
import { App } from '../common/app';
import { AppEvent, AppEventEmitter } from '../common/events';
import { ValueResult } from '../common/value-result';
import { DbConfigStore } from './config/db-config-store';
import { DbItem } from './db-item';
import { createLocalTree, createRemoteTree } from './db-tree-creator';
import { App } from "../common/app";
import { AppEvent, AppEventEmitter } from "../common/events";
import { ValueResult } from "../common/value-result";
import { DbConfigStore } from "./config/db-config-store";
import { DbItem } from "./db-item";
import { createLocalTree, createRemoteTree } from "./db-tree-creator";
export class DbManager {
public readonly onDbItemsChanged: AppEvent<void>;
private readonly onDbItemsChangesEventEmitter: AppEventEmitter<void>;
constructor(
app: App,
private readonly dbConfigStore: DbConfigStore
) {
constructor(app: App, private readonly dbConfigStore: DbConfigStore) {
this.onDbItemsChangesEventEmitter = app.createEventEmitter<void>();
this.onDbItemsChanged = this.onDbItemsChangesEventEmitter.event;
@@ -29,7 +26,7 @@ export class DbManager {
return ValueResult.ok([
createRemoteTree(configResult.value),
createLocalTree(configResult.value)
createLocalTree(configResult.value),
]);
}

View File

@@ -1,23 +1,25 @@
import { App, AppMode } from '../common/app';
import { isCanary, isNewQueryRunExperienceEnabled } from '../config';
import { logger } from '../logging';
import { DisposableObject } from '../pure/disposable-object';
import { DbConfigStore } from './config/db-config-store';
import { DbManager } from './db-manager';
import { DbPanel } from './ui/db-panel';
import { App, AppMode } from "../common/app";
import { isCanary, isNewQueryRunExperienceEnabled } from "../config";
import { logger } from "../logging";
import { DisposableObject } from "../pure/disposable-object";
import { DbConfigStore } from "./config/db-config-store";
import { DbManager } from "./db-manager";
import { DbPanel } from "./ui/db-panel";
export class DbModule extends DisposableObject {
public async initialize(app: App): Promise<void> {
if (app.mode !== AppMode.Development ||
if (
app.mode !== AppMode.Development ||
!isCanary() ||
!isNewQueryRunExperienceEnabled()) {
!isNewQueryRunExperienceEnabled()
) {
// Currently, we only want to expose the new database panel when we
// are in development and canary mode and the developer has enabled the
// are in development and canary mode and the developer has enabled the
// new query run experience.
return;
}
void logger.log('Initializing database module');
void logger.log("Initializing database module");
const dbConfigStore = new DbConfigStore(app);
await dbConfigStore.initialize();

View File

@@ -1,4 +1,9 @@
import { DbConfig, LocalDatabase, LocalList, RemoteRepositoryList } from './config/db-config';
import {
DbConfig,
LocalDatabase,
LocalList,
RemoteRepositoryList,
} from "./config/db-config";
import {
DbItemKind,
LocalDatabaseDbItem,
@@ -8,17 +13,19 @@ import {
RemoteSystemDefinedListDbItem,
RemoteUserDefinedListDbItem,
RootLocalDbItem,
RootRemoteDbItem
} from './db-item';
RootRemoteDbItem,
} from "./db-item";
export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
const systemDefinedLists = [
createSystemDefinedList(10),
createSystemDefinedList(100),
createSystemDefinedList(1000)
createSystemDefinedList(1000),
];
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map(createUserDefinedList);
const userDefinedRepoLists = dbConfig.databases.remote.repositoryLists.map(
createUserDefinedList,
);
const owners = dbConfig.databases.remote.owners.map(createOwnerItem);
const repos = dbConfig.databases.remote.repositories.map(createRepoItem);
@@ -28,8 +35,8 @@ export function createRemoteTree(dbConfig: DbConfig): RootRemoteDbItem {
...systemDefinedLists,
...owners,
...userDefinedRepoLists,
...repos
]
...repos,
],
};
}
@@ -39,10 +46,7 @@ export function createLocalTree(dbConfig: DbConfig): RootLocalDbItem {
return {
kind: DbItemKind.RootLocal,
children: [
...localLists,
...localDbs
]
children: [...localLists, ...localDbs],
};
}
@@ -51,29 +55,31 @@ function createSystemDefinedList(n: number): RemoteSystemDefinedListDbItem {
kind: DbItemKind.RemoteSystemDefinedList,
listName: `top_${n}`,
listDisplayName: `Top ${n} repositories`,
listDescription: `Top ${n} repositories of a language`
listDescription: `Top ${n} repositories of a language`,
};
}
function createUserDefinedList(list: RemoteRepositoryList): RemoteUserDefinedListDbItem {
function createUserDefinedList(
list: RemoteRepositoryList,
): RemoteUserDefinedListDbItem {
return {
kind: DbItemKind.RemoteUserDefinedList,
listName: list.name,
repos: list.repositories.map((r) => createRepoItem(r))
repos: list.repositories.map((r) => createRepoItem(r)),
};
}
function createOwnerItem(owner: string): RemoteOwnerDbItem {
return {
kind: DbItemKind.RemoteOwner,
ownerName: owner
ownerName: owner,
};
}
function createRepoItem(repo: string): RemoteRepoDbItem {
return {
kind: DbItemKind.RemoteRepo,
repoFullName: repo
repoFullName: repo,
};
}
@@ -81,7 +87,7 @@ function createLocalList(list: LocalList): LocalListDbItem {
return {
kind: DbItemKind.LocalList,
listName: list.name,
databases: list.databases.map(createLocalDb)
databases: list.databases.map(createLocalDb),
};
}
@@ -91,6 +97,6 @@ function createLocalDb(db: LocalDatabase): LocalDatabaseDbItem {
databaseName: db.name,
dateAdded: db.dateAdded,
language: db.language,
storagePath: db.storagePath
storagePath: db.storagePath,
};
}

View File

@@ -1,4 +1,4 @@
import { DbItem, DbItemKind } from '../db-item';
import { DbItem, DbItemKind } from "../db-item";
import {
createDbTreeViewItemLocalDatabase,
createDbTreeViewItemOwner,
@@ -6,57 +6,59 @@ import {
createDbTreeViewItemRoot,
createDbTreeViewItemSystemDefinedList,
createDbTreeViewItemUserDefinedList,
DbTreeViewItem
} from './db-tree-view-item';
DbTreeViewItem,
} from "./db-tree-view-item";
export function mapDbItemToTreeViewItem(dbItem: DbItem): DbTreeViewItem {
switch (dbItem.kind) {
case DbItemKind.RootLocal:
return createDbTreeViewItemRoot(
dbItem,
'local',
'Local databases',
dbItem.children.map(c => mapDbItemToTreeViewItem(c)));
"local",
"Local databases",
dbItem.children.map((c) => mapDbItemToTreeViewItem(c)),
);
case DbItemKind.RootRemote:
return createDbTreeViewItemRoot(
dbItem,
'remote',
'Remote databases',
dbItem.children.map(c => mapDbItemToTreeViewItem(c)));
"remote",
"Remote databases",
dbItem.children.map((c) => mapDbItemToTreeViewItem(c)),
);
case DbItemKind.RemoteSystemDefinedList:
return createDbTreeViewItemSystemDefinedList(
dbItem,
dbItem.listDisplayName,
dbItem.listDescription);
dbItem.listDescription,
);
case DbItemKind.RemoteUserDefinedList:
return createDbTreeViewItemUserDefinedList(
dbItem,
dbItem.listName,
dbItem.repos.map(mapDbItemToTreeViewItem));
dbItem.repos.map(mapDbItemToTreeViewItem),
);
case DbItemKind.RemoteOwner:
return createDbTreeViewItemOwner(
dbItem,
dbItem.ownerName);
return createDbTreeViewItemOwner(dbItem, dbItem.ownerName);
case DbItemKind.RemoteRepo:
return createDbTreeViewItemRepo(
dbItem,
dbItem.repoFullName);
return createDbTreeViewItemRepo(dbItem, dbItem.repoFullName);
case DbItemKind.LocalList:
return createDbTreeViewItemUserDefinedList(
dbItem,
dbItem.listName,
dbItem.databases.map(mapDbItemToTreeViewItem));
dbItem.databases.map(mapDbItemToTreeViewItem),
);
case DbItemKind.LocalDatabase:
return createDbTreeViewItemLocalDatabase(
dbItem,
dbItem.databaseName,
dbItem.language);
dbItem.language,
);
}
}

View File

@@ -1,33 +1,33 @@
import * as vscode from 'vscode';
import { commandRunner } from '../../commandRunner';
import { DisposableObject } from '../../pure/disposable-object';
import { DbManager } from '../db-manager';
import { DbTreeDataProvider } from './db-tree-data-provider';
import * as vscode from "vscode";
import { commandRunner } from "../../commandRunner";
import { DisposableObject } from "../../pure/disposable-object";
import { DbManager } from "../db-manager";
import { DbTreeDataProvider } from "./db-tree-data-provider";
export class DbPanel extends DisposableObject {
private readonly dataProvider: DbTreeDataProvider;
public constructor(
private readonly dbManager: DbManager
) {
public constructor(private readonly dbManager: DbManager) {
super();
this.dataProvider = new DbTreeDataProvider(dbManager);
const treeView = vscode.window.createTreeView('codeQLDatabasesExperimental', {
treeDataProvider: this.dataProvider,
canSelectMany: false
});
const treeView = vscode.window.createTreeView(
"codeQLDatabasesExperimental",
{
treeDataProvider: this.dataProvider,
canSelectMany: false,
},
);
this.push(treeView);
}
public async initialize(): Promise<void> {
this.push(
commandRunner(
'codeQLDatabasesExperimental.openConfigFile',
() => this.openConfigFile(),
)
commandRunner("codeQLDatabasesExperimental.openConfigFile", () =>
this.openConfigFile(),
),
);
}

View File

@@ -1,21 +1,29 @@
import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
import { createDbTreeViewItemError, DbTreeViewItem } from './db-tree-view-item';
import { DbManager } from '../db-manager';
import { mapDbItemToTreeViewItem } from './db-item-mapper';
import { DisposableObject } from '../../pure/disposable-object';
export class DbTreeDataProvider extends DisposableObject implements TreeDataProvider<DbTreeViewItem> {
import {
Event,
EventEmitter,
ProviderResult,
TreeDataProvider,
TreeItem,
} from "vscode";
import { createDbTreeViewItemError, DbTreeViewItem } from "./db-tree-view-item";
import { DbManager } from "../db-manager";
import { mapDbItemToTreeViewItem } from "./db-item-mapper";
import { DisposableObject } from "../../pure/disposable-object";
export class DbTreeDataProvider
extends DisposableObject
implements TreeDataProvider<DbTreeViewItem>
{
// This is an event to signal that there's been a change in the tree which
// will case the view to refresh. It is part of the TreeDataProvider interface.
public readonly onDidChangeTreeData: Event<DbTreeViewItem | undefined>;
private _onDidChangeTreeData = this.push(new EventEmitter<DbTreeViewItem | undefined>());
private _onDidChangeTreeData = this.push(
new EventEmitter<DbTreeViewItem | undefined>(),
);
private dbTreeItems: DbTreeViewItem[];
public constructor(
private readonly dbManager: DbManager
) {
public constructor(private readonly dbManager: DbManager) {
super();
this.dbTreeItems = this.createTree();
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
@@ -54,8 +62,8 @@ export class DbTreeDataProvider extends DisposableObject implements TreeDataProv
if (dbItemsResult.isFailure) {
const errorTreeViewItem = createDbTreeViewItemError(
'Error when reading databases config',
'Please open your databases config and address errors'
"Error when reading databases config",
"Please open your databases config and address errors",
);
return [errorTreeViewItem];

View File

@@ -1,4 +1,4 @@
import * as vscode from 'vscode';
import * as vscode from "vscode";
import {
DbItem,
LocalDatabaseDbItem,
@@ -8,11 +8,11 @@ import {
RemoteSystemDefinedListDbItem,
RemoteUserDefinedListDbItem,
RootLocalDbItem,
RootRemoteDbItem
} from '../db-item';
RootRemoteDbItem,
} from "../db-item";
/**
* Represents an item in the database tree view. This item could be
* Represents an item in the database tree view. This item could be
* representing an actual database item or a warning.
*/
export class DbTreeViewItem extends vscode.TreeItem {
@@ -25,20 +25,26 @@ export class DbTreeViewItem extends vscode.TreeItem {
public readonly label: string,
public readonly tooltip: string | undefined,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly children: DbTreeViewItem[]
public readonly children: DbTreeViewItem[],
) {
super(label, collapsibleState);
}
}
export function createDbTreeViewItemError(label: string, tooltip: string): DbTreeViewItem {
export function createDbTreeViewItemError(
label: string,
tooltip: string,
): DbTreeViewItem {
return new DbTreeViewItem(
undefined,
new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground')),
new vscode.ThemeIcon(
"error",
new vscode.ThemeColor("problemsErrorIcon.foreground"),
),
label,
tooltip,
vscode.TreeItemCollapsibleState.None,
[]
[],
);
}
@@ -46,7 +52,7 @@ export function createDbTreeViewItemRoot(
dbItem: RootLocalDbItem | RootRemoteDbItem,
label: string,
tooltip: string,
children: DbTreeViewItem[]
children: DbTreeViewItem[],
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
@@ -54,27 +60,29 @@ export function createDbTreeViewItemRoot(
label,
tooltip,
vscode.TreeItemCollapsibleState.Collapsed,
children);
children,
);
}
export function createDbTreeViewItemSystemDefinedList(
dbItem: RemoteSystemDefinedListDbItem,
label: string,
tooltip: string
tooltip: string,
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
new vscode.ThemeIcon('github'),
new vscode.ThemeIcon("github"),
label,
tooltip,
vscode.TreeItemCollapsibleState.None,
[]);
[],
);
}
export function createDbTreeViewItemUserDefinedList(
dbItem: LocalListDbItem | RemoteUserDefinedListDbItem,
listName: string,
children: DbTreeViewItem[]
children: DbTreeViewItem[],
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
@@ -82,7 +90,8 @@ export function createDbTreeViewItemUserDefinedList(
listName,
undefined,
vscode.TreeItemCollapsibleState.Collapsed,
children);
children,
);
}
export function createDbTreeViewItemOwner(
@@ -91,11 +100,12 @@ export function createDbTreeViewItemOwner(
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
new vscode.ThemeIcon('organization'),
new vscode.ThemeIcon("organization"),
ownerName,
undefined,
vscode.TreeItemCollapsibleState.None,
[]);
[],
);
}
export function createDbTreeViewItemRepo(
@@ -104,23 +114,25 @@ export function createDbTreeViewItemRepo(
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
new vscode.ThemeIcon('database'),
new vscode.ThemeIcon("database"),
repoName,
undefined,
vscode.TreeItemCollapsibleState.None,
[]);
[],
);
}
export function createDbTreeViewItemLocalDatabase(
dbItem: LocalDatabaseDbItem,
databaseName: string,
language: string
language: string,
): DbTreeViewItem {
return new DbTreeViewItem(
dbItem,
new vscode.ThemeIcon('database'),
new vscode.ThemeIcon("database"),
databaseName,
`Language: ${language}`,
vscode.TreeItemCollapsibleState.None,
[]);
[],
);
}

View File

@@ -1,5 +1,5 @@
import { DisposableObject } from './pure/disposable-object';
import { logger } from './logging';
import { DisposableObject } from "./pure/disposable-object";
import { logger } from "./logging";
/**
* Base class for "discovery" operations, which scan the file system to find specific kinds of
@@ -38,8 +38,7 @@ export abstract class Discovery<T> extends DisposableObject {
if (this.discoveryInProgress) {
// There's already a discovery operation in progress. Tell it to restart when it's done.
this.retry = true;
}
else {
} else {
// No discovery in progress, so start one now.
this.discoveryInProgress = true;
this.launchDiscovery();
@@ -53,15 +52,16 @@ export abstract class Discovery<T> extends DisposableObject {
*/
private launchDiscovery(): void {
const discoveryPromise = this.discover();
discoveryPromise.then(results => {
if (!this.retry) {
// Update any listeners with the results of the discovery.
this.discoveryInProgress = false;
this.update(results);
}
})
discoveryPromise
.then((results) => {
if (!this.retry) {
// Update any listeners with the results of the discovery.
this.discoveryInProgress = false;
this.update(results);
}
})
.catch(err => {
.catch((err) => {
void logger.log(`${this.name} failed. Reason: ${err.message}`);
})

View File

@@ -1,21 +1,21 @@
import * as fetch from 'node-fetch';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as unzipper from 'unzipper';
import * as url from 'url';
import { ExtensionContext, Event } from 'vscode';
import { DistributionConfig } from './config';
import * as fetch from "node-fetch";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as semver from "semver";
import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
showAndLogErrorMessage,
showAndLogWarningMessage
} from './helpers';
import { logger } from './logging';
import { getCodeQlCliVersion } from './cli-version';
import { ProgressCallback, reportStreamProgress } from './commandRunner';
showAndLogWarningMessage,
} from "./helpers";
import { logger } from "./logging";
import { getCodeQlCliVersion } from "./cli-version";
import { ProgressCallback, reportStreamProgress } from "./commandRunner";
/**
* distribution.ts
@@ -30,7 +30,7 @@ import { ProgressCallback, reportStreamProgress } from './commandRunner';
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_OWNER_NAME = 'github';
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
/**
* Default value for the repository name of the extension-managed distribution on GitHub.
@@ -38,14 +38,15 @@ const DEFAULT_DISTRIBUTION_OWNER_NAME = 'github';
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = 'codeql-cli-binaries';
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
/**
* Range of versions of the CLI that are compatible with the extension.
*
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range = new semver.Range('2.x');
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: semver.Range =
new semver.Range("2.x");
export interface DistributionProvider {
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
@@ -54,35 +55,39 @@ export interface DistributionProvider {
}
export class DistributionManager implements DistributionProvider {
/**
* Get the name of the codeql cli installation we prefer to install, based on our current platform.
*/
public static getRequiredAssetName(): string {
switch (os.platform()) {
case 'linux':
return 'codeql-linux64.zip';
case 'darwin':
return 'codeql-osx64.zip';
case 'win32':
return 'codeql-win64.zip';
case "linux":
return "codeql-linux64.zip";
case "darwin":
return "codeql-osx64.zip";
case "win32":
return "codeql-win64.zip";
default:
return 'codeql.zip';
return "codeql.zip";
}
}
constructor(
public readonly config: DistributionConfig,
private readonly versionRange: semver.Range,
extensionContext: ExtensionContext
extensionContext: ExtensionContext,
) {
this._onDidChangeDistribution = config.onDidChangeConfiguration;
this.extensionSpecificDistributionManager =
new ExtensionSpecificDistributionManager(config, versionRange, extensionContext);
new ExtensionSpecificDistributionManager(
config,
versionRange,
extensionContext,
);
this.updateCheckRateLimiter = new InvocationRateLimiter(
extensionContext,
'extensionSpecificDistributionUpdateCheck',
() => this.extensionSpecificDistributionManager.checkForUpdatesToDistribution()
"extensionSpecificDistributionUpdateCheck",
() =>
this.extensionSpecificDistributionManager.checkForUpdatesToDistribution(),
);
}
@@ -120,7 +125,9 @@ export class DistributionManager implements DistributionProvider {
* - If the user is using an extension-managed CLI, then prereleases are only accepted when the
* includePrerelease config option is set.
*/
const includePrerelease = distribution.kind !== DistributionKind.ExtensionManaged || this.config.includePrerelease;
const includePrerelease =
distribution.kind !== DistributionKind.ExtensionManaged ||
this.config.includePrerelease;
if (!semver.satisfies(version, this.versionRange, { includePrerelease })) {
return {
@@ -132,7 +139,7 @@ export class DistributionManager implements DistributionProvider {
return {
distribution,
kind: FindDistributionResultKind.CompatibleDistribution,
version
version,
};
}
@@ -149,49 +156,58 @@ export class DistributionManager implements DistributionProvider {
/**
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
*/
async getDistributionWithoutVersionCheck(): Promise<Distribution | undefined> {
async getDistributionWithoutVersionCheck(): Promise<
Distribution | undefined
> {
// Check config setting, then extension specific distribution, then PATH.
if (this.config.customCodeQlPath) {
if (!await fs.pathExists(this.config.customCodeQlPath)) {
void showAndLogErrorMessage(`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
'by a configuration setting, but a CodeQL executable could not be found at that path. Please check ' +
'that a CodeQL executable exists at the specified path or remove the setting.');
if (!(await fs.pathExists(this.config.customCodeQlPath))) {
void showAndLogErrorMessage(
`The CodeQL executable path is specified as "${this.config.customCodeQlPath}" ` +
"by a configuration setting, but a CodeQL executable could not be found at that path. Please check " +
"that a CodeQL executable exists at the specified path or remove the setting.",
);
return undefined;
}
// emit a warning if using a deprecated launcher and a non-deprecated launcher exists
if (
deprecatedCodeQlLauncherName() &&
this.config.customCodeQlPath.endsWith(deprecatedCodeQlLauncherName()!) &&
await this.hasNewLauncherName()
this.config.customCodeQlPath.endsWith(
deprecatedCodeQlLauncherName()!,
) &&
(await this.hasNewLauncherName())
) {
warnDeprecatedLauncher();
}
return {
codeQlPath: this.config.customCodeQlPath,
kind: DistributionKind.CustomPathConfig
kind: DistributionKind.CustomPathConfig,
};
}
const extensionSpecificCodeQlPath = await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
const extensionSpecificCodeQlPath =
await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (extensionSpecificCodeQlPath !== undefined) {
return {
codeQlPath: extensionSpecificCodeQlPath,
kind: DistributionKind.ExtensionManaged
kind: DistributionKind.ExtensionManaged,
};
}
if (process.env.PATH) {
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
const expectedLauncherPath = await getExecutableFromDirectory(searchDirectory);
const expectedLauncherPath = await getExecutableFromDirectory(
searchDirectory,
);
if (expectedLauncherPath) {
return {
codeQlPath: expectedLauncherPath,
kind: DistributionKind.PathEnvironmentVariable
kind: DistributionKind.PathEnvironmentVariable,
};
}
}
void logger.log('INFO: Could not find CodeQL on path.');
void logger.log("INFO: Could not find CodeQL on path.");
}
return undefined;
@@ -204,14 +220,19 @@ export class DistributionManager implements DistributionProvider {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToExtensionManagedDistribution(
minSecondsSinceLastUpdateCheck: number): Promise<DistributionUpdateCheckResult> {
minSecondsSinceLastUpdateCheck: number,
): Promise<DistributionUpdateCheckResult> {
const distribution = await this.getDistributionWithoutVersionCheck();
const extensionManagedCodeQlPath = await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
const extensionManagedCodeQlPath =
await this.extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (distribution?.codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidLocationResult();
}
const updateCheckResult = await this.updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(minSecondsSinceLastUpdateCheck);
const updateCheckResult =
await this.updateCheckRateLimiter.invokeFunctionIfIntervalElapsed(
minSecondsSinceLastUpdateCheck,
);
switch (updateCheckResult.kind) {
case InvocationRateLimiterResultKind.Invoked:
return updateCheckResult.result;
@@ -227,9 +248,12 @@ export class DistributionManager implements DistributionProvider {
*/
public installExtensionManagedDistributionRelease(
release: Release,
progressCallback?: ProgressCallback
progressCallback?: ProgressCallback,
): Promise<void> {
return this.extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
return this.extensionSpecificDistributionManager.installDistributionRelease(
release,
progressCallback,
);
}
public get onDidChangeDistribution(): Event<void> | undefined {
@@ -260,7 +284,7 @@ class ExtensionSpecificDistributionManager {
constructor(
private readonly config: DistributionConfig,
private readonly versionRange: semver.Range,
private readonly extensionContext: ExtensionContext
private readonly extensionContext: ExtensionContext,
) {
/**/
}
@@ -268,7 +292,10 @@ class ExtensionSpecificDistributionManager {
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
if (this.getInstalledRelease() !== undefined) {
// An extension specific distribution has been installed.
const expectedLauncherPath = await getExecutableFromDirectory(this.getDistributionRootPath(), true);
const expectedLauncherPath = await getExecutableFromDirectory(
this.getDistributionRootPath(),
true,
);
if (expectedLauncherPath) {
return expectedLauncherPath;
}
@@ -276,8 +303,10 @@ class ExtensionSpecificDistributionManager {
try {
await this.removeDistribution();
} catch (e) {
void logger.log('WARNING: Tried to remove corrupted CodeQL CLI at ' +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
void logger.log(
"WARNING: Tried to remove corrupted CodeQL CLI at " +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`,
);
}
}
return undefined;
@@ -309,53 +338,80 @@ class ExtensionSpecificDistributionManager {
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
progressCallback?: ProgressCallback): Promise<void> {
public async installDistributionRelease(
release: Release,
progressCallback?: ProgressCallback,
): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
await this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
progressCallback?: ProgressCallback): Promise<void> {
private async downloadDistribution(
release: Release,
progressCallback?: ProgressCallback,
): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {
void logger.log(`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
`but encountered an error: ${e}.`);
void logger.log(
`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
`but encountered an error: ${e}.`,
);
}
// Filter assets to the unique one that we require.
const requiredAssetName = DistributionManager.getRequiredAssetName();
const assets = release.assets.filter(asset => asset.name === requiredAssetName);
const assets = release.assets.filter(
(asset) => asset.name === requiredAssetName,
);
if (assets.length === 0) {
throw new Error(`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`);
throw new Error(
`Invariant violation: chose a release to install that didn't have ${requiredAssetName}`,
);
}
if (assets.length > 1) {
void logger.log('WARNING: chose a release with more than one asset to install, found ' +
assets.map(asset => asset.name).join(', '));
void logger.log(
"WARNING: chose a release with more than one asset to install, found " +
assets.map((asset) => asset.name).join(", "),
);
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(assets[0]);
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-codeql'));
const assetStream =
await this.createReleasesApiConsumer().streamBinaryContentOfAsset(
assets[0],
);
const tmpDirectory = await fs.mkdtemp(
path.join(os.tmpdir(), "vscode-codeql"),
);
try {
const archivePath = path.join(tmpDirectory, 'distributionDownload.zip');
const archivePath = path.join(tmpDirectory, "distributionDownload.zip");
const archiveFile = fs.createWriteStream(archivePath);
const contentLength = assetStream.headers.get('content-length');
const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined;
reportStreamProgress(assetStream.body, `Downloading CodeQL CLI ${release.name}`, totalNumBytes, progressCallback);
const contentLength = assetStream.headers.get("content-length");
const totalNumBytes = contentLength
? parseInt(contentLength, 10)
: undefined;
reportStreamProgress(
assetStream.body,
`Downloading CodeQL CLI ${release.name}`,
totalNumBytes,
progressCallback,
);
await new Promise((resolve, reject) =>
assetStream.body.pipe(archiveFile)
.on('finish', resolve)
.on('error', reject)
assetStream.body
.pipe(archiveFile)
.on("finish", resolve)
.on("error", reject),
);
await this.bumpDistributionFolderIndex();
void logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
void logger.log(
`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`,
);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
} finally {
await fs.remove(tmpDirectory);
@@ -376,111 +432,167 @@ class ExtensionSpecificDistributionManager {
private async getLatestRelease(): Promise<Release> {
const requiredAssetName = DistributionManager.getRequiredAssetName();
void logger.log(`Searching for latest release including ${requiredAssetName}.`);
void logger.log(
`Searching for latest release including ${requiredAssetName}.`,
);
return this.createReleasesApiConsumer().getLatestRelease(
this.versionRange,
this.config.includePrerelease,
release => {
const matchingAssets = release.assets.filter(asset => asset.name === requiredAssetName);
(release) => {
const matchingAssets = release.assets.filter(
(asset) => asset.name === requiredAssetName,
);
if (matchingAssets.length === 0) {
// For example, this could be a release with no platform-specific assets.
void logger.log(`INFO: Ignoring a release with no assets named ${requiredAssetName}`);
void logger.log(
`INFO: Ignoring a release with no assets named ${requiredAssetName}`,
);
return false;
}
if (matchingAssets.length > 1) {
void logger.log(`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`);
void logger.log(
`WARNING: Ignoring a release with more than one asset named ${requiredAssetName}`,
);
return false;
}
return true;
}
},
);
}
private createReleasesApiConsumer(): ReleasesApiConsumer {
const ownerName = this.config.ownerName ? this.config.ownerName : DEFAULT_DISTRIBUTION_OWNER_NAME;
const repositoryName = this.config.repositoryName ? this.config.repositoryName : DEFAULT_DISTRIBUTION_REPOSITORY_NAME;
return new ReleasesApiConsumer(ownerName, repositoryName, this.config.personalAccessToken);
const ownerName = this.config.ownerName
? this.config.ownerName
: DEFAULT_DISTRIBUTION_OWNER_NAME;
const repositoryName = this.config.repositoryName
? this.config.repositoryName
: DEFAULT_DISTRIBUTION_REPOSITORY_NAME;
return new ReleasesApiConsumer(
ownerName,
repositoryName,
this.config.personalAccessToken,
);
}
private async bumpDistributionFolderIndex(): Promise<void> {
const index = this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey,
0,
);
await this.extensionContext.globalState.update(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey,
index + 1,
);
}
private getDistributionStoragePath(): string {
// Use an empty string for the initial distribution for backwards compatibility.
const distributionFolderIndex = this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || '';
return path.join(this.extensionContext.globalStoragePath,
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName + distributionFolderIndex);
const distributionFolderIndex =
this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey,
0,
) || "";
return path.join(
this.extensionContext.globalStoragePath,
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName +
distributionFolderIndex,
);
}
private getDistributionRootPath(): string {
return path.join(this.getDistributionStoragePath(),
ExtensionSpecificDistributionManager._codeQlExtractedFolderName);
return path.join(
this.getDistributionStoragePath(),
ExtensionSpecificDistributionManager._codeQlExtractedFolderName,
);
}
private getInstalledRelease(): Release | undefined {
return this.extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
return this.extensionContext.globalState.get(
ExtensionSpecificDistributionManager._installedReleaseStateKey,
);
}
private async storeInstalledRelease(release: Release | undefined): Promise<void> {
await this.extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
private async storeInstalledRelease(
release: Release | undefined,
): Promise<void> {
await this.extensionContext.globalState.update(
ExtensionSpecificDistributionManager._installedReleaseStateKey,
release,
);
}
private static readonly _currentDistributionFolderBaseName = 'distribution';
private static readonly _currentDistributionFolderIndexStateKey = 'distributionFolderIndex';
private static readonly _installedReleaseStateKey = 'distributionRelease';
private static readonly _codeQlExtractedFolderName = 'codeql';
private static readonly _currentDistributionFolderBaseName = "distribution";
private static readonly _currentDistributionFolderIndexStateKey =
"distributionFolderIndex";
private static readonly _installedReleaseStateKey = "distributionRelease";
private static readonly _codeQlExtractedFolderName = "codeql";
}
export class ReleasesApiConsumer {
constructor(ownerName: string, repoName: string, personalAccessToken?: string) {
constructor(
ownerName: string,
repoName: string,
personalAccessToken?: string,
) {
// Specify version of the GitHub API
this._defaultHeaders['accept'] = 'application/vnd.github.v3+json';
this._defaultHeaders["accept"] = "application/vnd.github.v3+json";
if (personalAccessToken) {
this._defaultHeaders['authorization'] = `token ${personalAccessToken}`;
this._defaultHeaders["authorization"] = `token ${personalAccessToken}`;
}
this._ownerName = ownerName;
this._repoName = repoName;
}
public async getLatestRelease(versionRange: semver.Range, includePrerelease = false, additionalCompatibilityCheck?: (release: GithubRelease) => boolean): Promise<Release> {
public async getLatestRelease(
versionRange: semver.Range,
includePrerelease = false,
additionalCompatibilityCheck?: (release: GithubRelease) => boolean,
): Promise<Release> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
const compatibleReleases = allReleases.filter(release => {
const allReleases: GithubRelease[] = await (
await this.makeApiCall(apiPath)
).json();
const compatibleReleases = allReleases.filter((release) => {
if (release.prerelease && !includePrerelease) {
return false;
}
const version = semver.parse(release.tag_name);
if (version === null || !semver.satisfies(version, versionRange, { includePrerelease })) {
if (
version === null ||
!semver.satisfies(version, versionRange, { includePrerelease })
) {
return false;
}
return !additionalCompatibilityCheck || additionalCompatibilityCheck(release);
return (
!additionalCompatibilityCheck || additionalCompatibilityCheck(release)
);
});
// Tag names must all be parsable to semvers due to the previous filtering step.
const latestRelease = compatibleReleases.sort((a, b) => {
const versionComparison = semver.compare(semver.parse(b.tag_name)!, semver.parse(a.tag_name)!);
const versionComparison = semver.compare(
semver.parse(b.tag_name)!,
semver.parse(a.tag_name)!,
);
if (versionComparison !== 0) {
return versionComparison;
}
return b.created_at.localeCompare(a.created_at, 'en-US');
return b.created_at.localeCompare(a.created_at, "en-US");
})[0];
if (latestRelease === undefined) {
throw new Error('No compatible CodeQL CLI releases were found. ' +
'Please check that the CodeQL extension is up to date.');
throw new Error(
"No compatible CodeQL CLI releases were found. " +
"Please check that the CodeQL extension is up to date.",
);
}
const assets: ReleaseAsset[] = latestRelease.assets.map(asset => {
const assets: ReleaseAsset[] = latestRelease.assets.map((asset) => {
return {
id: asset.id,
name: asset.name,
size: asset.size
size: asset.size,
};
});
@@ -488,29 +600,42 @@ export class ReleasesApiConsumer {
assets,
createdAt: latestRelease.created_at,
id: latestRelease.id,
name: latestRelease.name
name: latestRelease.name,
};
}
public async streamBinaryContentOfAsset(asset: ReleaseAsset): Promise<fetch.Response> {
public async streamBinaryContentOfAsset(
asset: ReleaseAsset,
): Promise<fetch.Response> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
return await this.makeApiCall(apiPath, {
'accept': 'application/octet-stream'
accept: "application/octet-stream",
});
}
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
const response = await this.makeRawRequest(ReleasesApiConsumer._apiBase + apiPath,
Object.assign({}, this._defaultHeaders, additionalHeaders));
protected async makeApiCall(
apiPath: string,
additionalHeaders: { [key: string]: string } = {},
): Promise<fetch.Response> {
const response = await this.makeRawRequest(
ReleasesApiConsumer._apiBase + apiPath,
Object.assign({}, this._defaultHeaders, additionalHeaders),
);
if (!response.ok) {
// Check for rate limiting
const rateLimitResetValue = response.headers.get('X-RateLimit-Reset');
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
if (response.status === 403 && rateLimitResetValue) {
const secondsToMillisecondsFactor = 1000;
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
const rateLimitResetDate = new Date(
parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor,
);
throw new GithubRateLimitedError(
response.status,
await response.text(),
rateLimitResetDate,
);
}
throw new GithubApiError(response.status, await response.text());
}
@@ -520,24 +645,29 @@ export class ReleasesApiConsumer {
private async makeRawRequest(
requestUrl: string,
headers: { [key: string]: string },
redirectCount = 0): Promise<fetch.Response> {
redirectCount = 0,
): Promise<fetch.Response> {
const response = await fetch.default(requestUrl, {
headers,
redirect: 'manual'
redirect: "manual",
});
const redirectUrl = response.headers.get('location');
if (isRedirectStatusCode(response.status) && redirectUrl && redirectCount < ReleasesApiConsumer._maxRedirects) {
const redirectUrl = response.headers.get("location");
if (
isRedirectStatusCode(response.status) &&
redirectUrl &&
redirectCount < ReleasesApiConsumer._maxRedirects
) {
const parsedRedirectUrl = url.parse(redirectUrl);
if (parsedRedirectUrl.protocol != 'https:') {
throw new Error('Encountered a non-https redirect, rejecting');
if (parsedRedirectUrl.protocol != "https:") {
throw new Error("Encountered a non-https redirect, rejecting");
}
if (parsedRedirectUrl.host != 'api.github.com') {
if (parsedRedirectUrl.host != "api.github.com") {
// Remove authorization header if we are redirected outside of the GitHub API.
//
// This is necessary to stream release assets since AWS fails if more than one auth
// mechanism is provided.
delete headers['authorization'];
delete headers["authorization"];
}
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1);
}
@@ -549,37 +679,51 @@ export class ReleasesApiConsumer {
private readonly _ownerName: string;
private readonly _repoName: string;
private static readonly _apiBase = 'https://api.github.com';
private static readonly _apiBase = "https://api.github.com";
private static readonly _maxRedirects = 20;
}
export async function extractZipArchive(archivePath: string, outPath: string): Promise<void> {
export async function extractZipArchive(
archivePath: string,
outPath: string,
): Promise<void> {
const archive = await unzipper.Open.file(archivePath);
await archive.extract({
concurrency: 4,
path: outPath
path: outPath,
});
// Set file permissions for extracted files
await Promise.all(archive.files.map(async file => {
// Only change file permissions if within outPath (path.join normalises the path)
const extractedPath = path.join(outPath, file.path);
if (extractedPath.indexOf(outPath) !== 0 || !(await fs.pathExists(extractedPath))) {
return Promise.resolve();
}
return fs.chmod(extractedPath, file.externalFileAttributes >>> 16);
}));
await Promise.all(
archive.files.map(async (file) => {
// Only change file permissions if within outPath (path.join normalises the path)
const extractedPath = path.join(outPath, file.path);
if (
extractedPath.indexOf(outPath) !== 0 ||
!(await fs.pathExists(extractedPath))
) {
return Promise.resolve();
}
return fs.chmod(extractedPath, file.externalFileAttributes >>> 16);
}),
);
}
export function codeQlLauncherName(): string {
return (os.platform() === 'win32') ? 'codeql.exe' : 'codeql';
return os.platform() === "win32" ? "codeql.exe" : "codeql";
}
function deprecatedCodeQlLauncherName(): string | undefined {
return (os.platform() === 'win32') ? 'codeql.cmd' : undefined;
return os.platform() === "win32" ? "codeql.cmd" : undefined;
}
function isRedirectStatusCode(statusCode: number): boolean {
return statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308;
return (
statusCode === 301 ||
statusCode === 302 ||
statusCode === 303 ||
statusCode === 307 ||
statusCode === 308
);
}
/*
@@ -589,7 +733,7 @@ function isRedirectStatusCode(statusCode: number): boolean {
export enum DistributionKind {
CustomPathConfig,
ExtensionManaged,
PathEnvironmentVariable
PathEnvironmentVariable,
}
export interface Distribution {
@@ -601,7 +745,7 @@ export enum FindDistributionResultKind {
CompatibleDistribution,
UnknownCompatibilityDistribution,
IncompatibleDistribution,
NoDistribution
NoDistribution,
}
export type FindDistributionResult =
@@ -641,7 +785,7 @@ export enum DistributionUpdateCheckResultKind {
AlreadyCheckedRecentlyResult,
AlreadyUpToDate,
InvalidLocation,
UpdateAvailable
UpdateAvailable,
}
type DistributionUpdateCheckResult =
@@ -672,43 +816,55 @@ export interface UpdateAvailableResult {
function createAlreadyCheckedRecentlyResult(): AlreadyCheckedRecentlyResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult,
};
}
function createAlreadyUpToDateResult(): AlreadyUpToDateResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate,
};
}
function createInvalidLocationResult(): InvalidLocationResult {
return {
kind: DistributionUpdateCheckResultKind.InvalidLocation
kind: DistributionUpdateCheckResultKind.InvalidLocation,
};
}
function createUpdateAvailableResult(updatedRelease: Release): UpdateAvailableResult {
function createUpdateAvailableResult(
updatedRelease: Release,
): UpdateAvailableResult {
return {
kind: DistributionUpdateCheckResultKind.UpdateAvailable,
updatedRelease
updatedRelease,
};
}
// Exported for testing
export async function getExecutableFromDirectory(directory: string, warnWhenNotFound = false): Promise<string | undefined> {
export async function getExecutableFromDirectory(
directory: string,
warnWhenNotFound = false,
): Promise<string | undefined> {
const expectedLauncherPath = path.join(directory, codeQlLauncherName());
const deprecatedLauncherName = deprecatedCodeQlLauncherName();
const alternateExpectedLauncherPath = deprecatedLauncherName ? path.join(directory, deprecatedLauncherName) : undefined;
const alternateExpectedLauncherPath = deprecatedLauncherName
? path.join(directory, deprecatedLauncherName)
: undefined;
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
} else if (alternateExpectedLauncherPath && (await fs.pathExists(alternateExpectedLauncherPath))) {
} else if (
alternateExpectedLauncherPath &&
(await fs.pathExists(alternateExpectedLauncherPath))
) {
warnDeprecatedLauncher();
return alternateExpectedLauncherPath;
}
if (warnWhenNotFound) {
void logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
'Will try PATH.');
void logger.log(
`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
"Will try PATH.",
);
}
return undefined;
}
@@ -716,7 +872,7 @@ export async function getExecutableFromDirectory(directory: string, warnWhenNotF
function warnDeprecatedLauncher() {
void showAndLogWarningMessage(
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`,
);
}
@@ -762,7 +918,6 @@ export interface ReleaseAsset {
size: number;
}
/**
* The json returned from github for a release.
*/
@@ -822,7 +977,11 @@ export class GithubApiError extends Error {
}
export class GithubRateLimitedError extends GithubApiError {
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
constructor(
public status: number,
public body: string,
public rateLimitResetDate: Date,
) {
super(status, body);
}
}

View File

@@ -1,5 +1,5 @@
import { ChildEvalLogTreeItem, EvalLogTreeItem } from './eval-log-viewer';
import { EvalLogData as EvalLogData } from './pure/log-summary-parser';
import { ChildEvalLogTreeItem, EvalLogTreeItem } from "./eval-log-viewer";
import { EvalLogData as EvalLogData } from "./pure/log-summary-parser";
/** Builds the tree data for the evaluator log viewer for a single query run. */
export default class EvalLogTreeBuilder {
@@ -22,40 +22,40 @@ export default class EvalLogTreeBuilder {
// level. For now, there will always be one root (the one query being shown).
const queryItem: EvalLogTreeItem = {
label: this.queryName,
children: [] // Will assign predicate items as children shortly.
children: [], // Will assign predicate items as children shortly.
};
// Display descriptive message when no data exists
// Display descriptive message when no data exists
if (this.evalLogDataItems.length === 0) {
const noResultsItem: ChildEvalLogTreeItem = {
label: 'No predicates evaluated in this query run.',
label: "No predicates evaluated in this query run.",
parent: queryItem,
children: [],
};
queryItem.children.push(noResultsItem);
}
// For each predicate, create a TreeItem object with appropriate parents/children
this.evalLogDataItems.forEach(logDataItem => {
// For each predicate, create a TreeItem object with appropriate parents/children
this.evalLogDataItems.forEach((logDataItem) => {
const predicateLabel = `${logDataItem.predicateName} (${logDataItem.resultSize} tuples, ${logDataItem.millis} ms)`;
const predicateItem: ChildEvalLogTreeItem = {
label: predicateLabel,
parent: queryItem,
children: [] // Will assign pipeline items as children shortly.
children: [], // Will assign pipeline items as children shortly.
};
for (const [pipelineName, steps] of Object.entries(logDataItem.ra)) {
const pipelineLabel = `Pipeline: ${pipelineName}`;
const pipelineItem: ChildEvalLogTreeItem = {
label: pipelineLabel,
parent: predicateItem,
children: [] // Will assign step items as children shortly.
children: [], // Will assign step items as children shortly.
};
predicateItem.children.push(pipelineItem);
pipelineItem.children = steps.map((step: string) => ({
label: step,
parent: pipelineItem,
children: []
children: [],
}));
}
queryItem.children.push(predicateItem);

View File

@@ -1,7 +1,16 @@
import { window, TreeDataProvider, TreeView, TreeItem, ProviderResult, Event, EventEmitter, TreeItemCollapsibleState } from 'vscode';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
import {
window,
TreeDataProvider,
TreeView,
TreeItem,
ProviderResult,
Event,
EventEmitter,
TreeItemCollapsibleState,
} from "vscode";
import { commandRunner } from "./commandRunner";
import { DisposableObject } from "./pure/disposable-object";
import { showAndLogErrorMessage } from "./helpers";
export interface EvalLogTreeItem {
label?: string;
@@ -13,11 +22,18 @@ export interface ChildEvalLogTreeItem extends EvalLogTreeItem {
}
/** Provides data from parsed CodeQL evaluator logs to be rendered in a tree view. */
class EvalLogDataProvider extends DisposableObject implements TreeDataProvider<EvalLogTreeItem> {
class EvalLogDataProvider
extends DisposableObject
implements TreeDataProvider<EvalLogTreeItem>
{
public roots: EvalLogTreeItem[] = [];
private _onDidChangeTreeData: EventEmitter<EvalLogTreeItem | undefined | null | void> = new EventEmitter<EvalLogTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: Event<EvalLogTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: EventEmitter<
EvalLogTreeItem | undefined | null | void
> = new EventEmitter<EvalLogTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: Event<
EvalLogTreeItem | undefined | null | void
> = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire();
@@ -27,7 +43,7 @@ class EvalLogDataProvider extends DisposableObject implements TreeDataProvider<E
const state = element.children.length
? TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None;
const treeItem = new TreeItem(element.label || '', state);
const treeItem = new TreeItem(element.label || "", state);
treeItem.tooltip = `${treeItem.label} || ''}`;
return treeItem;
}
@@ -55,17 +71,17 @@ export class EvalLogViewer extends DisposableObject {
super();
this.treeDataProvider = new EvalLogDataProvider();
this.treeView = window.createTreeView('codeQLEvalLogViewer', {
this.treeView = window.createTreeView("codeQLEvalLogViewer", {
treeDataProvider: this.treeDataProvider,
showCollapseAll: true
showCollapseAll: true,
});
this.push(this.treeView);
this.push(this.treeDataProvider);
this.push(
commandRunner('codeQLEvalLogViewer.clear', async () => {
commandRunner("codeQLEvalLogViewer.clear", async () => {
this.clear();
})
}),
);
}
@@ -80,13 +96,15 @@ export class EvalLogViewer extends DisposableObject {
this.treeDataProvider.roots = roots;
this.treeDataProvider.refresh();
this.treeView.message = 'Viewer for query run:'; // Currently only one query supported at a time.
this.treeView.message = "Viewer for query run:"; // Currently only one query supported at a time.
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
() => {
/**/
},
(err) => showAndLogErrorMessage(err),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,27 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as yaml from 'js-yaml';
import * as path from 'path';
import * as tmp from 'tmp-promise';
import * as fs from "fs-extra";
import * as glob from "glob-promise";
import * as yaml from "js-yaml";
import * as path from "path";
import * as tmp from "tmp-promise";
import {
ExtensionContext,
Uri,
window as Window,
workspace,
env
} from 'vscode';
import { CodeQLCliServer, QlpacksInfo } from './cli';
import { UserCancellationException } from './commandRunner';
import { logger } from './logging';
import { QueryMetadata } from './pure/interface-types';
env,
} from "vscode";
import { CodeQLCliServer, QlpacksInfo } from "./cli";
import { UserCancellationException } from "./commandRunner";
import { logger } from "./logging";
import { QueryMetadata } from "./pure/interface-types";
// Shared temporary folder for the extension.
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
export const upgradesTmpDir = path.join(tmpDir.name, 'upgrades');
export const tmpDir = tmp.dirSync({
prefix: "queries_",
keep: false,
unsafeCleanup: true,
});
export const upgradesTmpDir = path.join(tmpDir.name, "upgrades");
fs.ensureDirSync(upgradesTmpDir);
export const tmpDirDisposal = {
@@ -25,9 +29,11 @@ export const tmpDirDisposal = {
try {
tmpDir.removeCallback();
} catch (e) {
void logger.log(`Failed to remove temporary directory ${tmpDir.name}: ${e}`);
void logger.log(
`Failed to remove temporary directory ${tmpDir.name}: ${e}`,
);
}
}
},
};
/**
@@ -42,16 +48,25 @@ export const tmpDirDisposal = {
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogErrorMessage(message: string, {
outputLogger = logger,
items = [] as string[],
fullMessage = undefined as (string | undefined)
} = {}): Promise<string | undefined> {
return internalShowAndLog(dropLinesExceptInitial(message), items, outputLogger, Window.showErrorMessage, fullMessage);
export async function showAndLogErrorMessage(
message: string,
{
outputLogger = logger,
items = [] as string[],
fullMessage = undefined as string | undefined,
} = {},
): Promise<string | undefined> {
return internalShowAndLog(
dropLinesExceptInitial(message),
items,
outputLogger,
Window.showErrorMessage,
fullMessage,
);
}
function dropLinesExceptInitial(message: string, n = 2) {
return message.toString().split(/\r?\n/).slice(0, n).join('\n');
return message.toString().split(/\r?\n/).slice(0, n).join("\n");
}
/**
@@ -63,11 +78,16 @@ function dropLinesExceptInitial(message: string, n = 2) {
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogWarningMessage(message: string, {
outputLogger = logger,
items = [] as string[]
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showWarningMessage);
export async function showAndLogWarningMessage(
message: string,
{ outputLogger = logger, items = [] as string[] } = {},
): Promise<string | undefined> {
return internalShowAndLog(
message,
items,
outputLogger,
Window.showWarningMessage,
);
}
/**
* Show an information message and log it to the console
@@ -78,24 +98,32 @@ export async function showAndLogWarningMessage(message: string, {
*
* @return A promise that resolves to the selected item or undefined when being dismissed.
*/
export async function showAndLogInformationMessage(message: string, {
outputLogger = logger,
items = [] as string[],
fullMessage = ''
} = {}): Promise<string | undefined> {
return internalShowAndLog(message, items, outputLogger, Window.showInformationMessage, fullMessage);
export async function showAndLogInformationMessage(
message: string,
{ outputLogger = logger, items = [] as string[], fullMessage = "" } = {},
): Promise<string | undefined> {
return internalShowAndLog(
message,
items,
outputLogger,
Window.showInformationMessage,
fullMessage,
);
}
type ShowMessageFn = (message: string, ...items: string[]) => Thenable<string | undefined>;
type ShowMessageFn = (
message: string,
...items: string[]
) => Thenable<string | undefined>;
async function internalShowAndLog(
message: string,
items: string[],
outputLogger = logger,
fn: ShowMessageFn,
fullMessage?: string
fullMessage?: string,
): Promise<string | undefined> {
const label = 'Show Log';
const label = "Show Log";
void outputLogger.log(fullMessage || message);
const result = await fn(message, label, ...items);
if (result === label) {
@@ -118,10 +146,20 @@ async function internalShowAndLog(
* `false` if the user clicks 'No' or cancels the dialog,
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showBinaryChoiceDialog(message: string, modal = true, yesTitle = 'Yes', noTitle = 'No'): Promise<boolean | undefined> {
export async function showBinaryChoiceDialog(
message: string,
modal = true,
yesTitle = "Yes",
noTitle = "No",
): Promise<boolean | undefined> {
const yesItem = { title: yesTitle, isCloseAffordance: false };
const noItem = { title: noTitle, isCloseAffordance: true };
const chosenItem = await Window.showInformationMessage(message, { modal }, yesItem, noItem);
const chosenItem = await Window.showInformationMessage(
message,
{ modal },
yesItem,
noItem,
);
if (!chosenItem) {
return undefined;
}
@@ -140,17 +178,26 @@ export async function showBinaryChoiceDialog(message: string, modal = true, yesT
* `false` if the user clicks 'No' or cancels the dialog,
* `undefined` if the dialog is closed without the user making a choice.
*/
export async function showBinaryChoiceWithUrlDialog(message: string, url: string): Promise<boolean | undefined> {
const urlItem = { title: 'More Information', isCloseAffordance: false };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true };
export async function showBinaryChoiceWithUrlDialog(
message: string,
url: string,
): Promise<boolean | undefined> {
const urlItem = { title: "More Information", isCloseAffordance: false };
const yesItem = { title: "Yes", isCloseAffordance: false };
const noItem = { title: "No", isCloseAffordance: true };
let chosenItem;
// Keep the dialog open as long as the user is clicking the 'more information' option.
// To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled
let count = 0;
do {
chosenItem = await Window.showInformationMessage(message, { modal: true }, urlItem, yesItem, noItem);
chosenItem = await Window.showInformationMessage(
message,
{ modal: true },
urlItem,
yesItem,
noItem,
);
if (chosenItem === urlItem) {
await env.openExternal(Uri.parse(url, true));
}
@@ -170,7 +217,10 @@ export async function showBinaryChoiceWithUrlDialog(message: string, url: string
*
* @return `true` if the user clicks the action, `false` if the user cancels the dialog.
*/
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
export async function showInformationMessageWithAction(
message: string,
actionMessage: string,
): Promise<boolean> {
const actionItem = { title: actionMessage, isCloseAffordance: false };
const chosenItem = await Window.showInformationMessage(message, actionItem);
return chosenItem === actionItem;
@@ -181,7 +231,7 @@ export function getOnDiskWorkspaceFolders() {
const workspaceFolders = workspace.workspaceFolders || [];
const diskWorkspaceFolders: string[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === 'file')
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath);
}
return diskWorkspaceFolders;
@@ -196,7 +246,9 @@ export class InvocationRateLimiter<T> {
extensionContext: ExtensionContext,
funcIdentifier: string,
func: () => Promise<T>,
createDate: (dateString?: string) => Date = s => s ? new Date(s) : new Date()) {
createDate: (dateString?: string) => Date = (s) =>
s ? new Date(s) : new Date(),
) {
this._createDate = createDate;
this._extensionContext = extensionContext;
this._func = func;
@@ -206,14 +258,17 @@ export class InvocationRateLimiter<T> {
/**
* Invoke the function if `minSecondsSinceLastInvocation` seconds have elapsed since the last invocation.
*/
public async invokeFunctionIfIntervalElapsed(minSecondsSinceLastInvocation: number): Promise<InvocationRateLimiterResult<T>> {
public async invokeFunctionIfIntervalElapsed(
minSecondsSinceLastInvocation: number,
): Promise<InvocationRateLimiterResult<T>> {
const updateCheckStartDate = this._createDate();
const lastInvocationDate = this.getLastInvocationDate();
if (
minSecondsSinceLastInvocation &&
lastInvocationDate &&
lastInvocationDate <= updateCheckStartDate &&
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 > updateCheckStartDate.getTime()
lastInvocationDate.getTime() + minSecondsSinceLastInvocation * 1000 >
updateCheckStartDate.getTime()
) {
return createRateLimitedResult();
}
@@ -224,12 +279,18 @@ export class InvocationRateLimiter<T> {
private getLastInvocationDate(): Date | undefined {
const maybeDateString: string | undefined =
this._extensionContext.globalState.get(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier);
this._extensionContext.globalState.get(
InvocationRateLimiter._invocationRateLimiterPrefix +
this._funcIdentifier,
);
return maybeDateString ? this._createDate(maybeDateString) : undefined;
}
private async setLastInvocationDate(date: Date): Promise<void> {
return await this._extensionContext.globalState.update(InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier, date);
return await this._extensionContext.globalState.update(
InvocationRateLimiter._invocationRateLimiterPrefix + this._funcIdentifier,
date,
);
}
private readonly _createDate: (dateString?: string) => Date;
@@ -237,12 +298,13 @@ export class InvocationRateLimiter<T> {
private readonly _func: () => Promise<T>;
private readonly _funcIdentifier: string;
private static readonly _invocationRateLimiterPrefix = 'invocationRateLimiter_lastInvocationDate_';
private static readonly _invocationRateLimiterPrefix =
"invocationRateLimiter_lastInvocationDate_";
}
export enum InvocationRateLimiterResultKind {
Invoked,
RateLimited
RateLimited,
}
/**
@@ -265,13 +327,13 @@ type InvocationRateLimiterResult<T> = InvokedResult<T> | RateLimitedResult;
function createInvokedResult<T>(result: T): InvokedResult<T> {
return {
kind: InvocationRateLimiterResultKind.Invoked,
result
result,
};
}
function createRateLimitedResult(): RateLimitedResult {
return {
kind: InvocationRateLimiterResultKind.RateLimited
kind: InvocationRateLimiterResultKind.RateLimited,
};
}
@@ -292,14 +354,22 @@ interface QlPackWithPath {
packDir: string | undefined;
}
async function findDbschemePack(packs: QlPackWithPath[], dbschemePath: string): Promise<{ name: string; isLibraryPack: boolean; }> {
async function findDbschemePack(
packs: QlPackWithPath[],
dbschemePath: string,
): Promise<{ name: string; isLibraryPack: boolean }> {
for (const { packDir, packName } of packs) {
if (packDir !== undefined) {
const qlpack = yaml.load(await fs.readFile(path.join(packDir, 'qlpack.yml'), 'utf8')) as { dbscheme?: string; library?: boolean; };
if (qlpack.dbscheme !== undefined && path.basename(qlpack.dbscheme) === path.basename(dbschemePath)) {
const qlpack = yaml.load(
await fs.readFile(path.join(packDir, "qlpack.yml"), "utf8"),
) as { dbscheme?: string; library?: boolean };
if (
qlpack.dbscheme !== undefined &&
path.basename(qlpack.dbscheme) === path.basename(dbschemePath)
) {
return {
name: packName,
isLibraryPack: qlpack.library === true
isLibraryPack: qlpack.library === true,
};
}
}
@@ -307,7 +377,10 @@ async function findDbschemePack(packs: QlPackWithPath[], dbschemePath: string):
throw new Error(`Could not find qlpack file for dbscheme ${dbschemePath}`);
}
function findStandardQueryPack(qlpacks: QlpacksInfo, dbschemePackName: string): string | undefined {
function findStandardQueryPack(
qlpacks: QlpacksInfo,
dbschemePackName: string,
): string | undefined {
const matches = dbschemePackName.match(/^codeql\/(?<language>[a-z]+)-all$/);
if (matches) {
const queryPackName = `codeql/${matches.groups!.language}-queries`;
@@ -321,43 +394,59 @@ function findStandardQueryPack(qlpacks: QlpacksInfo, dbschemePackName: string):
return undefined;
}
export async function getQlPackForDbscheme(cliServer: CodeQLCliServer, dbschemePath: string): Promise<QlPacksForLanguage> {
export async function getQlPackForDbscheme(
cliServer: CodeQLCliServer,
dbschemePath: string,
): Promise<QlPacksForLanguage> {
const qlpacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
const packs: QlPackWithPath[] =
Object.entries(qlpacks).map(([packName, dirs]) => {
const packs: QlPackWithPath[] = Object.entries(qlpacks).map(
([packName, dirs]) => {
if (dirs.length < 1) {
void logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`);
void logger.log(
`In getQlPackFor ${dbschemePath}, qlpack ${packName} has no directories`,
);
return { packName, packDir: undefined };
}
if (dirs.length > 1) {
void logger.log(`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`);
void logger.log(
`In getQlPackFor ${dbschemePath}, qlpack ${packName} has more than one directory; arbitrarily choosing the first`,
);
}
return {
packName,
packDir: dirs[0]
packDir: dirs[0],
};
});
},
);
const dbschemePack = await findDbschemePack(packs, dbschemePath);
const queryPack = dbschemePack.isLibraryPack ? findStandardQueryPack(qlpacks, dbschemePack.name) : undefined;
const queryPack = dbschemePack.isLibraryPack
? findStandardQueryPack(qlpacks, dbschemePack.name)
: undefined;
return {
dbschemePack: dbschemePack.name,
dbschemePackIsLibraryPack: dbschemePack.isLibraryPack,
queryPack
queryPack,
};
}
export async function getPrimaryDbscheme(datasetFolder: string): Promise<string> {
const dbschemes = await glob(path.join(datasetFolder, '*.dbscheme'));
export async function getPrimaryDbscheme(
datasetFolder: string,
): Promise<string> {
const dbschemes = await glob(path.join(datasetFolder, "*.dbscheme"));
if (dbschemes.length < 1) {
throw new Error(`Can't find dbscheme for current database in ${datasetFolder}`);
throw new Error(
`Can't find dbscheme for current database in ${datasetFolder}`,
);
}
dbschemes.sort();
const dbscheme = dbschemes[0];
if (dbschemes.length > 1) {
void Window.showErrorMessage(`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`);
void Window.showErrorMessage(
`Found multiple dbschemes in ${datasetFolder} during quick query; arbitrarily choosing the first, ${dbscheme}, to decide what library to use.`,
);
}
return dbscheme;
}
@@ -369,12 +458,21 @@ export class CachedOperation<U> {
private readonly operation: (t: string, ...args: any[]) => Promise<U>;
private readonly cached: Map<string, U>;
private readonly lru: string[];
private readonly inProgressCallbacks: Map<string, [(u: U) => void, (reason?: any) => void][]>;
private readonly inProgressCallbacks: Map<
string,
[(u: U) => void, (reason?: any) => void][]
>;
constructor(operation: (t: string, ...args: any[]) => Promise<U>, private cacheSize = 100) {
constructor(
operation: (t: string, ...args: any[]) => Promise<U>,
private cacheSize = 100,
) {
this.operation = operation;
this.lru = [];
this.inProgressCallbacks = new Map<string, [(u: U) => void, (reason?: any) => void][]>();
this.inProgressCallbacks = new Map<
string,
[(u: U) => void, (reason?: any) => void][]
>();
this.cached = new Map<string, U>();
}
@@ -383,7 +481,12 @@ export class CachedOperation<U> {
const fromCache = this.cached.get(t);
if (fromCache !== undefined) {
// Move to end of lru list
this.lru.push(this.lru.splice(this.lru.findIndex(v => v === t), 1)[0]);
this.lru.push(
this.lru.splice(
this.lru.findIndex((v) => v === t),
1,
)[0],
);
return fromCache;
}
// Otherwise check if in progress
@@ -400,7 +503,7 @@ export class CachedOperation<U> {
this.inProgressCallbacks.set(t, callbacks);
try {
const result = await this.operation(t, ...args);
callbacks.forEach(f => f[0](result));
callbacks.forEach((f) => f[0](result));
this.inProgressCallbacks.delete(t);
if (this.lru.length > this.cacheSize) {
const toRemove = this.lru.shift()!;
@@ -411,7 +514,7 @@ export class CachedOperation<U> {
return result;
} catch (e) {
// Rethrow error on all callbacks
callbacks.forEach(f => f[1](e));
callbacks.forEach((f) => f[1](e));
throw e;
} finally {
this.inProgressCallbacks.delete(t);
@@ -419,8 +522,6 @@ export class CachedOperation<U> {
}
}
/**
* The following functions al heuristically determine metadata about databases.
*/
@@ -436,20 +537,22 @@ export class CachedOperation<U> {
* @see cli.CodeQLCliServer.resolveDatabase
*/
export const dbSchemeToLanguage = {
'semmlecode.javascript.dbscheme': 'javascript',
'semmlecode.cpp.dbscheme': 'cpp',
'semmlecode.dbscheme': 'java',
'semmlecode.python.dbscheme': 'python',
'semmlecode.csharp.dbscheme': 'csharp',
'go.dbscheme': 'go',
'ruby.dbscheme': 'ruby'
"semmlecode.javascript.dbscheme": "javascript",
"semmlecode.cpp.dbscheme": "cpp",
"semmlecode.dbscheme": "java",
"semmlecode.python.dbscheme": "python",
"semmlecode.csharp.dbscheme": "csharp",
"go.dbscheme": "go",
"ruby.dbscheme": "ruby",
};
export const languageToDbScheme = Object.entries(dbSchemeToLanguage).reduce((acc, [k, v]) => {
acc[v] = k;
return acc;
}, {} as { [k: string]: string });
export const languageToDbScheme = Object.entries(dbSchemeToLanguage).reduce(
(acc, [k, v]) => {
acc[v] = k;
return acc;
},
{} as { [k: string]: string },
);
/**
* Returns the initial contents for an empty query, based on the language of the selected
@@ -465,13 +568,13 @@ export const languageToDbScheme = Object.entries(dbSchemeToLanguage).reduce((acc
*/
export function getInitialQueryContents(language: string, dbscheme: string) {
if (!language) {
const dbschemeBase = path.basename(dbscheme) as keyof typeof dbSchemeToLanguage;
const dbschemeBase = path.basename(
dbscheme,
) as keyof typeof dbSchemeToLanguage;
language = dbSchemeToLanguage[dbschemeBase];
}
return language
? `import ${language}\n\nselect ""`
: 'select ""';
return language ? `import ${language}\n\nselect ""` : 'select ""';
}
/**
@@ -481,23 +584,26 @@ export function getInitialQueryContents(language: string, dbscheme: string) {
* contains a folder starting with `db-`.
*/
export async function isLikelyDatabaseRoot(maybeRoot: string) {
const [a, b, c] = (await Promise.all([
const [a, b, c] = await Promise.all([
// databases can have either .dbinfo or codeql-database.yml.
fs.pathExists(path.join(maybeRoot, '.dbinfo')),
fs.pathExists(path.join(maybeRoot, 'codeql-database.yml')),
fs.pathExists(path.join(maybeRoot, ".dbinfo")),
fs.pathExists(path.join(maybeRoot, "codeql-database.yml")),
// they *must* have a db-{language} folder
glob('db-*/', { cwd: maybeRoot })
]));
glob("db-*/", { cwd: maybeRoot }),
]);
return ((a || b) && c.length > 0);
return (a || b) && c.length > 0;
}
/**
* A language folder is any folder starting with `db-` that is itself not a database root.
*/
export async function isLikelyDbLanguageFolder(dbPath: string) {
return path.basename(dbPath).startsWith('db-') && !(await isLikelyDatabaseRoot(dbPath));
return (
path.basename(dbPath).startsWith("db-") &&
!(await isLikelyDatabaseRoot(dbPath))
);
}
/**
@@ -506,17 +612,22 @@ export async function isLikelyDbLanguageFolder(dbPath: string) {
*/
export async function findLanguage(
cliServer: CodeQLCliServer,
queryUri: Uri | undefined
queryUri: Uri | undefined,
): Promise<string | undefined> {
const uri = queryUri || Window.activeTextEditor?.document.uri;
if (uri !== undefined) {
try {
const queryInfo = await cliServer.resolveQueryByLanguage(getOnDiskWorkspaceFolders(), uri);
const language = (Object.keys(queryInfo.byLanguage))[0];
const queryInfo = await cliServer.resolveQueryByLanguage(
getOnDiskWorkspaceFolders(),
uri,
);
const language = Object.keys(queryInfo.byLanguage)[0];
void logger.log(`Detected query language: ${language}`);
return language;
} catch (e) {
void logger.log('Could not autodetect query language. Select language manually.');
void logger.log(
"Could not autodetect query language. Select language manually.",
);
}
}
@@ -524,17 +635,25 @@ export async function findLanguage(
return await askForLanguage(cliServer, false);
}
export async function askForLanguage(cliServer: CodeQLCliServer, throwOnEmpty = true): Promise<string | undefined> {
export async function askForLanguage(
cliServer: CodeQLCliServer,
throwOnEmpty = true,
): Promise<string | undefined> {
const language = await Window.showQuickPick(
await cliServer.getSupportedLanguages(),
{ placeHolder: 'Select target language for your query', ignoreFocusOut: true }
{
placeHolder: "Select target language for your query",
ignoreFocusOut: true,
},
);
if (!language) {
// This only happens if the user cancels the quick pick.
if (throwOnEmpty) {
throw new UserCancellationException('Cancelled.');
throw new UserCancellationException("Cancelled.");
} else {
void showAndLogErrorMessage('Language not found. Language must be specified manually.');
void showAndLogErrorMessage(
"Language not found. Language must be specified manually.",
);
}
}
return language;
@@ -546,7 +665,10 @@ export async function askForLanguage(cliServer: CodeQLCliServer, throwOnEmpty =
* @param queryPath The path to the query.
* @returns A promise that resolves to the query metadata, if available.
*/
export async function tryGetQueryMetadata(cliServer: CodeQLCliServer, queryPath: string): Promise<QueryMetadata | undefined> {
export async function tryGetQueryMetadata(
cliServer: CodeQLCliServer,
queryPath: string,
): Promise<QueryMetadata | undefined> {
try {
return await cliServer.resolveMetadata(queryPath);
} catch (e) {
@@ -564,12 +686,11 @@ export async function tryGetQueryMetadata(cliServer: CodeQLCliServer, queryPath:
* It does not need to exist.
*/
export async function createTimestampFile(storagePath: string) {
const timestampPath = path.join(storagePath, 'timestamp');
const timestampPath = path.join(storagePath, "timestamp");
await fs.ensureDir(storagePath);
await fs.writeFile(timestampPath, Date.now().toString(), 'utf8');
await fs.writeFile(timestampPath, Date.now().toString(), "utf8");
}
/**
* Recursively walk a directory and return the full path to all files found.
* Symbolic links are ignored.
@@ -578,7 +699,9 @@ export async function createTimestampFile(storagePath: string) {
*
* @return An iterator of the full path to all files recursively found in the directory.
*/
export async function* walkDirectory(dir: string): AsyncIterableIterator<string> {
export async function* walkDirectory(
dir: string,
): AsyncIterableIterator<string> {
const seenFiles = new Set<string>();
for await (const d of await fs.opendir(dir)) {
const entry = path.join(dir, d.name);

View File

@@ -1,13 +1,17 @@
import { env } from 'vscode';
import * as path from 'path';
import { QueryHistoryConfig } from './config';
import { LocalQueryInfo } from './query-results';
import { buildRepoLabel, getRawQueryName, QueryHistoryInfo } from './query-history-info';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { VariantAnalysisHistoryItem } from './remote-queries/variant-analysis-history-item';
import { assertNever } from './pure/helpers-pure';
import { pluralize } from './pure/word';
import { humanizeQueryStatus } from './query-status';
import { env } from "vscode";
import * as path from "path";
import { QueryHistoryConfig } from "./config";
import { LocalQueryInfo } from "./query-results";
import {
buildRepoLabel,
getRawQueryName,
QueryHistoryInfo,
} from "./query-history-info";
import { RemoteQueryHistoryItem } from "./remote-queries/remote-query-history-item";
import { VariantAnalysisHistoryItem } from "./remote-queries/variant-analysis-history-item";
import { assertNever } from "./pure/helpers-pure";
import { pluralize } from "./pure/word";
import { humanizeQueryStatus } from "./query-status";
interface InterpolateReplacements {
t: string; // Start time
@@ -16,7 +20,7 @@ interface InterpolateReplacements {
r: string; // Result count/Empty
s: string; // Status
f: string; // Query file name
'%': '%'; // Percent sign
"%": "%"; // Percent sign
}
export class HistoryItemLabelProvider {
@@ -27,20 +31,20 @@ export class HistoryItemLabelProvider {
getLabel(item: QueryHistoryInfo) {
let replacements: InterpolateReplacements;
switch (item.t) {
case 'local':
case "local":
replacements = this.getLocalInterpolateReplacements(item);
break;
case 'remote':
case "remote":
replacements = this.getRemoteInterpolateReplacements(item);
break;
case 'variant-analysis':
case "variant-analysis":
replacements = this.getVariantAnalysisInterpolateReplacements(item);
break;
default:
assertNever(item);
}
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || '%q');
const rawLabel = item.userSpecifiedLabel ?? (this.config.format || "%q");
return this.interpolate(rawLabel, replacements);
}
@@ -57,18 +61,26 @@ export class HistoryItemLabelProvider {
: getRawQueryName(item);
}
private interpolate(
rawLabel: string,
replacements: InterpolateReplacements,
): string {
const label = rawLabel.replace(
/%(.)/g,
(match, key: keyof InterpolateReplacements) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
},
);
private interpolate(rawLabel: string, replacements: InterpolateReplacements): string {
const label = rawLabel.replace(/%(.)/g, (match, key: keyof InterpolateReplacements) => {
const replacement = replacements[key];
return replacement !== undefined ? replacement : match;
});
return label.replace(/\s+/g, ' ');
return label.replace(/\s+/g, " ");
}
private getLocalInterpolateReplacements(item: LocalQueryInfo): InterpolateReplacements {
const { resultCount = 0, statusString = 'in progress' } = item.completedQuery || {};
private getLocalInterpolateReplacements(
item: LocalQueryInfo,
): InterpolateReplacements {
const { resultCount = 0, statusString = "in progress" } =
item.completedQuery || {};
return {
t: item.startTime,
q: item.getQueryName(),
@@ -76,33 +88,45 @@ export class HistoryItemLabelProvider {
r: `(${resultCount} results)`,
s: statusString,
f: item.getQueryFileName(),
'%': '%',
"%": "%",
};
}
private getRemoteInterpolateReplacements(item: RemoteQueryHistoryItem): InterpolateReplacements {
const resultCount = item.resultCount ? `(${pluralize(item.resultCount, 'result', 'results')})` : '';
private getRemoteInterpolateReplacements(
item: RemoteQueryHistoryItem,
): InterpolateReplacements {
const resultCount = item.resultCount
? `(${pluralize(item.resultCount, "result", "results")})`
: "";
return {
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(env.language),
t: new Date(item.remoteQuery.executionStartTime).toLocaleString(
env.language,
),
q: `${item.remoteQuery.queryName} (${item.remoteQuery.language})`,
d: buildRepoLabel(item),
r: resultCount,
s: humanizeQueryStatus(item.status),
f: path.basename(item.remoteQuery.queryFilePath),
'%': '%'
"%": "%",
};
}
private getVariantAnalysisInterpolateReplacements(item: VariantAnalysisHistoryItem): InterpolateReplacements {
const resultCount = item.resultCount ? `(${pluralize(item.resultCount, 'result', 'results')})` : '';
private getVariantAnalysisInterpolateReplacements(
item: VariantAnalysisHistoryItem,
): InterpolateReplacements {
const resultCount = item.resultCount
? `(${pluralize(item.resultCount, "result", "results")})`
: "";
return {
t: new Date(item.variantAnalysis.executionStartTime).toLocaleString(env.language),
t: new Date(item.variantAnalysis.executionStartTime).toLocaleString(
env.language,
),
q: `${item.variantAnalysis.query.name} (${item.variantAnalysis.query.language})`,
d: buildRepoLabel(item),
r: resultCount,
s: humanizeQueryStatus(item.status),
f: path.basename(item.variantAnalysis.query.filePath),
'%': '%',
"%": "%",
};
}
}

View File

@@ -1,30 +1,39 @@
import { ProgressLocation, window } from 'vscode';
import { StreamInfo } from 'vscode-languageclient';
import * as cli from './cli';
import { QueryServerConfig } from './config';
import { ideServerLogger } from './logging';
import { ProgressLocation, window } from "vscode";
import { StreamInfo } from "vscode-languageclient";
import * as cli from "./cli";
import { QueryServerConfig } from "./config";
import { ideServerLogger } from "./logging";
/**
* Managing the language server for CodeQL.
*/
/** Starts a new CodeQL language server process, sending progress messages to the status bar. */
export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamInfo> {
return window.withProgress({ title: 'CodeQL language server', location: ProgressLocation.Window }, async (progressReporter, _) => {
const args = ['--check-errors', 'ON_CHANGE'];
if (cli.shouldDebugIdeServer()) {
args.push('-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=n,quiet=y');
}
const child = cli.spawnServer(
config.codeQlPath,
'CodeQL language server',
['execute', 'language-server'],
args,
ideServerLogger,
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
progressReporter
);
return { writer: child.stdin!, reader: child.stdout! };
});
export async function spawnIdeServer(
config: QueryServerConfig,
): Promise<StreamInfo> {
return window.withProgress(
{ title: "CodeQL language server", location: ProgressLocation.Window },
async (progressReporter, _) => {
const args = ["--check-errors", "ON_CHANGE"];
if (cli.shouldDebugIdeServer()) {
args.push(
"-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9009,server=y,suspend=n,quiet=y",
);
}
const child = cli.spawnServer(
config.codeQlPath,
"CodeQL language server",
["execute", "language-server"],
args,
ideServerLogger,
(data) =>
ideServerLogger.log(data.toString(), { trailingNewline: false }),
(data) =>
ideServerLogger.log(data.toString(), { trailingNewline: false }),
progressReporter,
);
return { writer: child.stdin!, reader: child.stdout! };
},
);
}

View File

@@ -1,5 +1,5 @@
import * as crypto from 'crypto';
import * as os from 'os';
import * as crypto from "crypto";
import * as os from "os";
import {
Uri,
Location,
@@ -13,20 +13,17 @@ import {
Selection,
TextEditorRevealType,
ThemeColor,
} from 'vscode';
import {
tryGetResolvableLocation,
isLineColumnLoc
} from './pure/bqrs-utils';
import { DatabaseItem, DatabaseManager } from './databases';
import { ViewSourceFileMsg } from './pure/interface-types';
import { Logger } from './logging';
} from "vscode";
import { tryGetResolvableLocation, isLineColumnLoc } from "./pure/bqrs-utils";
import { DatabaseItem, DatabaseManager } from "./databases";
import { ViewSourceFileMsg } from "./pure/interface-types";
import { Logger } from "./logging";
import {
LineColumnLocation,
WholeFileLocation,
UrlValue,
ResolvableLocationValue
} from './pure/bqrs-cli-types';
ResolvableLocationValue,
} from "./pure/bqrs-cli-types";
/**
* This module contains functions and types that are sharedd between
@@ -35,7 +32,7 @@ import {
/** Gets a nonce string created with 128 bits of entropy. */
export function getNonce(): string {
return crypto.randomBytes(16).toString('base64');
return crypto.randomBytes(16).toString("base64");
}
/**
@@ -52,7 +49,7 @@ export enum WebviewReveal {
*/
export function fileUriToWebviewUri(
panel: WebviewPanel,
fileUriOnDisk: Uri
fileUriOnDisk: Uri,
): string {
return panel.webview.asWebviewUri(fileUriOnDisk).toString();
}
@@ -64,7 +61,7 @@ export function fileUriToWebviewUri(
*/
function resolveFivePartLocation(
loc: LineColumnLocation,
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly.
@@ -72,7 +69,7 @@ function resolveFivePartLocation(
Math.max(0, loc.startLine - 1),
Math.max(0, loc.startColumn - 1),
Math.max(0, loc.endLine - 1),
Math.max(1, loc.endColumn)
Math.max(1, loc.endColumn),
);
return new Location(databaseItem.resolveSourceFile(loc.uri), range);
@@ -85,7 +82,7 @@ function resolveFivePartLocation(
*/
function resolveWholeFileLocation(
loc: WholeFileLocation,
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Location {
// A location corresponding to the start of the file.
const range = new Range(0, 0, 0, 0);
@@ -100,10 +97,10 @@ function resolveWholeFileLocation(
*/
export function tryResolveLocation(
loc: UrlValue | undefined,
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Location | undefined {
const resolvableLoc = tryGetResolvableLocation(loc);
if (!resolvableLoc || typeof resolvableLoc === 'string') {
if (!resolvableLoc || typeof resolvableLoc === "string") {
return;
} else if (isLineColumnLoc(resolvableLoc)) {
return resolveFivePartLocation(resolvableLoc, databaseItem);
@@ -112,7 +109,11 @@ export function tryResolveLocation(
}
}
export type WebviewView = 'results' | 'compare' | 'remote-queries' | 'variant-analysis';
export type WebviewView =
| "results"
| "compare"
| "remote-queries"
| "variant-analysis";
export interface WebviewMessage {
t: string;
@@ -131,28 +132,27 @@ export function getHtmlForWebview(
}: {
allowInlineStyles?: boolean;
} = {
allowInlineStyles: false,
}
allowInlineStyles: false,
},
): string {
const scriptUriOnDisk = Uri.file(
ctx.asAbsolutePath('out/webview.js')
);
const scriptUriOnDisk = Uri.file(ctx.asAbsolutePath("out/webview.js"));
const stylesheetUrisOnDisk = [
Uri.file(ctx.asAbsolutePath('out/webview.css'))
Uri.file(ctx.asAbsolutePath("out/webview.css")),
];
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUris = stylesheetUrisOnDisk.map(stylesheetUriOnDisk =>
webview.asWebviewUri(stylesheetUriOnDisk));
const stylesheetWebviewUris = stylesheetUrisOnDisk.map(
(stylesheetUriOnDisk) => webview.asWebviewUri(stylesheetUriOnDisk),
);
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
const stylesheetsHtmlLines = allowInlineStyles
? stylesheetWebviewUris.map(uri => createStylesLinkWithoutNonce(uri))
: stylesheetWebviewUris.map(uri => createStylesLinkWithNonce(nonce, uri));
? stylesheetWebviewUris.map((uri) => createStylesLinkWithoutNonce(uri))
: stylesheetWebviewUris.map((uri) => createStylesLinkWithNonce(nonce, uri));
const styleSrc = allowInlineStyles
? `${webview.cspSource} vscode-file: 'unsafe-inline'`
@@ -172,7 +172,9 @@ export function getHtmlForWebview(
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${webview.cspSource};">
content="default-src 'none'; script-src 'nonce-${nonce}'; font-src ${fontSrc}; style-src ${styleSrc}; connect-src ${
webview.cspSource
};">
${stylesheetsHtmlLines.join(` ${os.EOL}`)}
</head>
<body>
@@ -186,7 +188,7 @@ export function getHtmlForWebview(
export async function showResolvableLocation(
loc: ResolvableLocationValue,
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Promise<void> {
await showLocation(tryResolveLocation(loc, databaseItem));
}
@@ -198,17 +200,16 @@ export async function showLocation(location?: Location) {
const doc = await workspace.openTextDocument(location.uri);
const editorsWithDoc = Window.visibleTextEditors.filter(
(e) => e.document === doc
(e) => e.document === doc,
);
const editor =
editorsWithDoc.length > 0
? editorsWithDoc[0]
: await Window.showTextDocument(
doc, {
// avoid preview mode so editor is sticky and will be added to navigation and search histories.
preview: false,
viewColumn: ViewColumn.One,
});
: await Window.showTextDocument(doc, {
// avoid preview mode so editor is sticky and will be added to navigation and search histories.
preview: false,
viewColumn: ViewColumn.One,
});
const range = location.range;
// When highlighting the range, vscode's occurrence-match and bracket-match highlighting will
@@ -229,30 +230,28 @@ export async function showLocation(location?: Location) {
editor.setDecorations(shownLocationLineDecoration, [range]);
}
const findMatchBackground = new ThemeColor('editor.findMatchBackground');
const findMatchBackground = new ThemeColor("editor.findMatchBackground");
const findRangeHighlightBackground = new ThemeColor(
'editor.findRangeHighlightBackground'
"editor.findRangeHighlightBackground",
);
export const shownLocationDecoration = Window.createTextEditorDecorationType({
backgroundColor: findMatchBackground,
});
export const shownLocationLineDecoration = Window.createTextEditorDecorationType(
{
export const shownLocationLineDecoration =
Window.createTextEditorDecorationType({
backgroundColor: findRangeHighlightBackground,
isWholeLine: true,
}
);
});
export async function jumpToLocation(
msg: ViewSourceFileMsg,
databaseManager: DatabaseManager,
logger: Logger
logger: Logger,
) {
const databaseItem = databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
Uri.parse(msg.databaseUri),
);
if (databaseItem !== undefined) {
try {
@@ -261,7 +260,7 @@ export async function jumpToLocation(
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
void Window.showErrorMessage(
'Original file of this result is not in the database\'s source archive.'
"Original file of this result is not in the database's source archive.",
);
} else {
void logger.log(`Unable to handleMsgFromView: ${e.message}`);

View File

@@ -1,5 +1,5 @@
import * as Sarif from 'sarif';
import * as vscode from 'vscode';
import * as Sarif from "sarif";
import * as vscode from "vscode";
import {
Diagnostic,
DiagnosticRelatedInformation,
@@ -7,13 +7,18 @@ import {
languages,
Uri,
window as Window,
env, WebviewPanel
} from 'vscode';
import * as cli from './cli';
import { CodeQLCliServer } from './cli';
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from './databases';
import { showAndLogErrorMessage } from './helpers';
import { assertNever, getErrorMessage, getErrorStack } from './pure/helpers-pure';
env,
WebviewPanel,
} from "vscode";
import * as cli from "./cli";
import { CodeQLCliServer } from "./cli";
import { DatabaseEventKind, DatabaseItem, DatabaseManager } from "./databases";
import { showAndLogErrorMessage } from "./helpers";
import {
assertNever,
getErrorMessage,
getErrorStack,
} from "./pure/helpers-pure";
import {
FromResultsViewMsg,
Interpretation,
@@ -28,12 +33,19 @@ import {
GRAPH_TABLE_NAME,
RawResultsSortState,
NavigationDirection,
} from './pure/interface-types';
import { Logger } from './logging';
import { commandRunner } from './commandRunner';
import { CompletedQueryInfo, interpretResultsSarif, interpretGraphResults } from './query-results';
import { QueryEvaluationInfo } from './run-queries-shared';
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
} from "./pure/interface-types";
import { Logger } from "./logging";
import { commandRunner } from "./commandRunner";
import {
CompletedQueryInfo,
interpretResultsSarif,
interpretGraphResults,
} from "./query-results";
import { QueryEvaluationInfo } from "./run-queries-shared";
import {
parseSarifLocation,
parseSarifPlainTextMessage,
} from "./pure/sarif-utils";
import {
WebviewReveal,
fileUriToWebviewUri,
@@ -41,13 +53,20 @@ import {
shownLocationDecoration,
shownLocationLineDecoration,
jumpToLocation,
} from './interface-utils';
import { getDefaultResultSetName, ParsedResultSets } from './pure/interface-types';
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './pure/bqrs-cli-types';
import { AbstractWebview, WebviewPanelConfig } from './abstract-webview';
import { PAGE_SIZE } from './config';
import { CompletedLocalQueryInfo } from './query-results';
import { HistoryItemLabelProvider } from './history-item-label-provider';
} from "./interface-utils";
import {
getDefaultResultSetName,
ParsedResultSets,
} from "./pure/interface-types";
import {
RawResultSet,
transformBqrsResultSet,
ResultSetSchema,
} from "./pure/bqrs-cli-types";
import { AbstractWebview, WebviewPanelConfig } from "./abstract-webview";
import { PAGE_SIZE } from "./config";
import { CompletedLocalQueryInfo } from "./query-results";
import { HistoryItemLabelProvider } from "./history-item-label-provider";
/**
* interface.ts
@@ -68,18 +87,19 @@ function sortMultiplier(sortDirection: SortDirection): number {
function sortInterpretedResults(
results: Sarif.Result[],
sortState: InterpretedResultsSortState | undefined
sortState: InterpretedResultsSortState | undefined,
): void {
if (sortState !== undefined) {
const multiplier = sortMultiplier(sortState.sortDirection);
switch (sortState.sortBy) {
case 'alert-message':
case "alert-message":
results.sort((a, b) =>
a.message.text === undefined
? 0
: b.message.text === undefined
? 0
: multiplier * a.message.text?.localeCompare(b.message.text, env.language)
? 0
: multiplier *
a.message.text?.localeCompare(b.message.text, env.language),
);
break;
default:
@@ -88,44 +108,56 @@ function sortInterpretedResults(
}
}
function interpretedPageSize(interpretation: Interpretation | undefined): number {
if (interpretation?.data.t == 'GraphInterpretationData') {
function interpretedPageSize(
interpretation: Interpretation | undefined,
): number {
if (interpretation?.data.t == "GraphInterpretationData") {
// Graph views always have one result per page.
return 1;
}
return PAGE_SIZE.getValue<number>();
}
function numPagesOfResultSet(resultSet: RawResultSet, interpretation?: Interpretation): number {
function numPagesOfResultSet(
resultSet: RawResultSet,
interpretation?: Interpretation,
): number {
const pageSize = interpretedPageSize(interpretation);
const n = interpretation?.data.t == 'GraphInterpretationData'
? interpretation.data.dot.length
: resultSet.schema.rows;
const n =
interpretation?.data.t == "GraphInterpretationData"
? interpretation.data.dot.length
: resultSet.schema.rows;
return Math.ceil(n / pageSize);
}
function numInterpretedPages(interpretation: Interpretation | undefined): number {
function numInterpretedPages(
interpretation: Interpretation | undefined,
): number {
if (!interpretation) {
return 0;
}
const pageSize = interpretedPageSize(interpretation);
const n = interpretation.data.t == 'GraphInterpretationData'
? interpretation.data.dot.length
: interpretation.data.runs[0].results?.length || 0;
const n =
interpretation.data.t == "GraphInterpretationData"
? interpretation.data.dot.length
: interpretation.data.runs[0].results?.length || 0;
return Math.ceil(n / pageSize);
}
export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResultsViewMsg> {
export class ResultsView extends AbstractWebview<
IntoResultsViewMsg,
FromResultsViewMsg
> {
private _displayedQuery?: CompletedLocalQueryInfo;
private _interpretation?: Interpretation;
private readonly _diagnosticCollection = languages.createDiagnosticCollection(
'codeql-query-results'
"codeql-query-results",
);
constructor(
@@ -133,31 +165,28 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer,
public logger: Logger,
private labelProvider: HistoryItemLabelProvider
private labelProvider: HistoryItemLabelProvider,
) {
super(ctx);
this.push(this._diagnosticCollection);
this.push(
vscode.window.onDidChangeTextEditorSelection(
this.handleSelectionChange.bind(this)
)
this.handleSelectionChange.bind(this),
),
);
const navigationCommands = {
'codeQLQueryResults.up': NavigationDirection.up,
'codeQLQueryResults.down': NavigationDirection.down,
'codeQLQueryResults.left': NavigationDirection.left,
'codeQLQueryResults.right': NavigationDirection.right,
"codeQLQueryResults.up": NavigationDirection.up,
"codeQLQueryResults.down": NavigationDirection.down,
"codeQLQueryResults.left": NavigationDirection.left,
"codeQLQueryResults.right": NavigationDirection.right,
// For backwards compatibility with keybindings set using an earlier version of the extension.
'codeQLQueryResults.nextPathStep': NavigationDirection.down,
'codeQLQueryResults.previousPathStep': NavigationDirection.up,
"codeQLQueryResults.nextPathStep": NavigationDirection.down,
"codeQLQueryResults.previousPathStep": NavigationDirection.up,
};
void logger.log('Registering result view navigation commands.');
void logger.log("Registering result view navigation commands.");
for (const [commandId, direction] of Object.entries(navigationCommands)) {
this.push(
commandRunner(
commandId,
this.navigateResultView.bind(this, direction)
)
commandRunner(commandId, this.navigateResultView.bind(this, direction)),
);
}
@@ -167,11 +196,11 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
this._diagnosticCollection.clear();
if (this.isShowingPanel) {
void this.postMessage({
t: 'untoggleShowProblems'
t: "untoggleShowProblems",
});
}
}
})
}),
);
}
@@ -181,16 +210,16 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
// Reveal the panel now as the subsequent call to 'Window.showTextEditor' in 'showLocation' may destroy the webview otherwise.
this.panel.reveal();
await this.postMessage({ t: 'navigate', direction });
await this.postMessage({ t: "navigate", direction });
}
protected getPanelConfig(): WebviewPanelConfig {
return {
viewId: 'resultsView',
title: 'CodeQL Query Results',
viewId: "resultsView",
title: "CodeQL Query Results",
viewColumn: this.chooseColumnForWebview(),
preserveFocus: true,
view: 'results',
view: "results",
};
}
@@ -201,23 +230,23 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
protected async onMessage(msg: FromResultsViewMsg): Promise<void> {
try {
switch (msg.t) {
case 'viewLoaded':
case "viewLoaded":
this.onWebViewLoaded();
break;
case 'viewSourceFile': {
case "viewSourceFile": {
await jumpToLocation(msg, this.databaseManager, this.logger);
break;
}
case 'toggleDiagnostics': {
case "toggleDiagnostics": {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(
Uri.parse(msg.databaseUri)
Uri.parse(msg.databaseUri),
);
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(
msg.origResultsPaths,
msg.metadata,
databaseItem
databaseItem,
);
}
} else {
@@ -226,17 +255,19 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
break;
}
case 'changeSort':
case "changeSort":
await this.changeRawSortState(msg.resultSetName, msg.sortState);
break;
case 'changeInterpretedSort':
case "changeInterpretedSort":
await this.changeInterpretedSortState(msg.sortState);
break;
case 'changePage':
if (msg.selectedTable === ALERTS_TABLE_NAME || msg.selectedTable === GRAPH_TABLE_NAME) {
case "changePage":
if (
msg.selectedTable === ALERTS_TABLE_NAME ||
msg.selectedTable === GRAPH_TABLE_NAME
) {
await this.showPageOfInterpretedResults(msg.pageNumber);
}
else {
} else {
await this.showPageOfRawResults(
msg.selectedTable,
msg.pageNumber,
@@ -244,11 +275,13 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
// sortedResultsInfo doesn't have an entry for the current
// result set. Use this to determine whether or not we use
// the sorted bqrs file.
!!this._displayedQuery?.completedQuery.sortedResultsInfo[msg.selectedTable]
!!this._displayedQuery?.completedQuery.sortedResultsInfo[
msg.selectedTable
],
);
}
break;
case 'openFile':
case "openFile":
await this.openFile(msg.filePath);
break;
default:
@@ -256,7 +289,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
} catch (e) {
void showAndLogErrorMessage(getErrorMessage(e), {
fullMessage: getErrorStack(e)
fullMessage: getErrorStack(e),
});
}
}
@@ -275,46 +308,59 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
// can't find a vscode API that does it any better.
// Here, iterate through all the visible editors and determine the max view column.
// This won't work if the largest view column is empty.
const colCount = Window.visibleTextEditors.reduce((maxVal, editor) =>
Math.max(maxVal, Number.parseInt(editor.viewColumn?.toFixed() || '0', 10)), 0);
const colCount = Window.visibleTextEditors.reduce(
(maxVal, editor) =>
Math.max(
maxVal,
Number.parseInt(editor.viewColumn?.toFixed() || "0", 10),
),
0,
);
if (colCount <= 1) {
return vscode.ViewColumn.Beside;
}
const activeViewColumnNum = Number.parseInt(Window.activeTextEditor?.viewColumn?.toFixed() || '0', 10);
return activeViewColumnNum === colCount ? vscode.ViewColumn.One : vscode.ViewColumn.Beside;
const activeViewColumnNum = Number.parseInt(
Window.activeTextEditor?.viewColumn?.toFixed() || "0",
10,
);
return activeViewColumnNum === colCount
? vscode.ViewColumn.One
: vscode.ViewColumn.Beside;
}
private async changeInterpretedSortState(
sortState: InterpretedResultsSortState | undefined
sortState: InterpretedResultsSortState | undefined,
): Promise<void> {
if (this._displayedQuery === undefined) {
void showAndLogErrorMessage(
'Failed to sort results since evaluation info was unknown.'
"Failed to sort results since evaluation info was unknown.",
);
return;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedQuery.completedQuery.updateInterpretedSortState(sortState);
await this.postMessage({ t: "resultsUpdating" });
await this._displayedQuery.completedQuery.updateInterpretedSortState(
sortState,
);
await this.showResults(this._displayedQuery, WebviewReveal.NotForced, true);
}
private async changeRawSortState(
resultSetName: string,
sortState: RawResultsSortState | undefined
sortState: RawResultsSortState | undefined,
): Promise<void> {
if (this._displayedQuery === undefined) {
void showAndLogErrorMessage(
'Failed to sort results since evaluation info was unknown.'
"Failed to sort results since evaluation info was unknown.",
);
return;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this.postMessage({ t: "resultsUpdating" });
await this._displayedQuery.completedQuery.updateSortState(
this.cliServer,
resultSetName,
sortState
sortState,
);
// Sorting resets to first page, as there is arguably no particular
// correlation between the results on the nth page that the user
@@ -335,7 +381,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
public async showResults(
fullQuery: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
shouldKeepOldResultsWhileRendering = false
shouldKeepOldResultsWhileRendering = false,
): Promise<void> {
if (!fullQuery.completedQuery.successful) {
return;
@@ -346,13 +392,16 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
this._interpretation = undefined;
const interpretationPage = await this.interpretResultsInfo(
fullQuery.completedQuery.query,
fullQuery.completedQuery.interpretedResultsSortState
fullQuery.completedQuery.interpretedResultsSortState,
);
const sortedResultsMap: SortedResultsMap = {};
Object.entries(fullQuery.completedQuery.sortedResultsInfo).forEach(
([k, v]) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(panel, v))
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
panel,
v,
)),
);
this._displayedQuery = fullQuery;
@@ -366,12 +415,13 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
// is not visible; it's in a not-currently-viewed tab. Show a
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const showButton = "View Results";
const queryName = this.labelProvider.getShortLabel(fullQuery);
const resultPromise = vscode.window.showInformationMessage(
`Finished running query ${queryName.length > 0 ? ` "${queryName}"` : ''
`Finished running query ${
queryName.length > 0 ? ` "${queryName}"` : ""
}.`,
showButton
showButton,
);
// Address this click asynchronously so we still update the
// query history immediately.
@@ -386,49 +436,49 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
// Note that the resultSetSchemas will return offsets for the default (unsorted) page,
// which may not be correct. However, in this case, it doesn't matter since we only
// need the first offset, which will be the same no matter which sorting we use.
const resultSetSchemas = await this.getResultSetSchemas(fullQuery.completedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
const resultSetSchemas = await this.getResultSetSchemas(
fullQuery.completedQuery,
);
const resultSetNames = resultSetSchemas.map((schema) => schema.name);
const selectedTable = getDefaultResultSetName(resultSetNames);
const schema = resultSetSchemas.find(
(resultSet) => resultSet.name == selectedTable
(resultSet) => resultSet.name == selectedTable,
)!;
// Use sorted results path if it exists. This may happen if we are
// reloading the results view after it has been sorted in the past.
const resultsPath = fullQuery.completedQuery.getResultsPath(selectedTable);
const pageSize = PAGE_SIZE.getValue<number>();
const chunk = await this.cliServer.bqrsDecode(
resultsPath,
schema.name,
{
// Always send the first page.
// It may not wind up being the page we actually show,
// if there are interpreted results, but speculatively
// send anyway.
offset: schema.pagination?.offsets[0],
pageSize
}
);
const chunk = await this.cliServer.bqrsDecode(resultsPath, schema.name, {
// Always send the first page.
// It may not wind up being the page we actually show,
// if there are interpreted results, but speculatively
// send anyway.
offset: schema.pagination?.offsets[0],
pageSize,
});
const resultSet = transformBqrsResultSet(schema, chunk);
fullQuery.completedQuery.setResultCount(interpretationPage?.numTotalResults || resultSet.schema.rows);
fullQuery.completedQuery.setResultCount(
interpretationPage?.numTotalResults || resultSet.schema.rows,
);
const parsedResultSets: ParsedResultSets = {
pageNumber: 0,
pageSize,
numPages: numPagesOfResultSet(resultSet, this._interpretation),
numInterpretedPages: numInterpretedPages(this._interpretation),
resultSet: { ...resultSet, t: 'RawResultSet' },
resultSet: { ...resultSet, t: "RawResultSet" },
selectedTable: undefined,
resultSetNames,
};
await this.postMessage({
t: 'setState',
t: "setState",
interpretation: interpretationPage,
origResultsPaths: fullQuery.completedQuery.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
panel,
fullQuery.completedQuery.query.resultsPaths.resultsPath
fullQuery.completedQuery.query.resultsPaths.resultsPath,
),
parsedResultSets,
sortedResultsMap,
@@ -436,31 +486,40 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
shouldKeepOldResultsWhileRendering,
metadata: fullQuery.completedQuery.query.metadata,
queryName: this.labelProvider.getLabel(fullQuery),
queryPath: fullQuery.initialInfo.queryPath
queryPath: fullQuery.initialInfo.queryPath,
});
}
/**
* Show a page of interpreted results
*/
public async showPageOfInterpretedResults(
pageNumber: number
): Promise<void> {
public async showPageOfInterpretedResults(pageNumber: number): Promise<void> {
if (this._displayedQuery === undefined) {
throw new Error('Trying to show interpreted results but displayed query was undefined');
throw new Error(
"Trying to show interpreted results but displayed query was undefined",
);
}
if (this._interpretation === undefined) {
throw new Error('Trying to show interpreted results but interpretation was undefined');
throw new Error(
"Trying to show interpreted results but interpretation was undefined",
);
}
if (this._interpretation.data.t === 'SarifInterpretationData' && this._interpretation.data.runs[0].results === undefined) {
throw new Error('Trying to show interpreted results but results were undefined');
if (
this._interpretation.data.t === "SarifInterpretationData" &&
this._interpretation.data.runs[0].results === undefined
) {
throw new Error(
"Trying to show interpreted results but results were undefined",
);
}
const resultSetSchemas = await this.getResultSetSchemas(this._displayedQuery.completedQuery);
const resultSetNames = resultSetSchemas.map(schema => schema.name);
const resultSetSchemas = await this.getResultSetSchemas(
this._displayedQuery.completedQuery,
);
const resultSetNames = resultSetSchemas.map((schema) => schema.name);
await this.postMessage({
t: 'showInterpretedPage',
t: "showInterpretedPage",
interpretation: this.getPageOfInterpretedResults(pageNumber),
database: this._displayedQuery.initialInfo.databaseInfo,
metadata: this._displayedQuery.completedQuery.query.metadata,
@@ -469,17 +528,20 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
pageSize: interpretedPageSize(this._interpretation),
numPages: numInterpretedPages(this._interpretation),
queryName: this.labelProvider.getLabel(this._displayedQuery),
queryPath: this._displayedQuery.initialInfo.queryPath
queryPath: this._displayedQuery.initialInfo.queryPath,
});
}
private async getResultSetSchemas(completedQuery: CompletedQueryInfo, selectedTable = ''): Promise<ResultSetSchema[]> {
private async getResultSetSchemas(
completedQuery: CompletedQueryInfo,
selectedTable = "",
): Promise<ResultSetSchema[]> {
const resultsPath = completedQuery.getResultsPath(selectedTable);
const schemas = await this.cliServer.bqrsInfo(
resultsPath,
PAGE_SIZE.getValue()
PAGE_SIZE.getValue(),
);
return schemas['result-sets'];
return schemas["result-sets"];
}
public async openFile(filePath: string) {
@@ -493,11 +555,11 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
public async showPageOfRawResults(
selectedTable: string,
pageNumber: number,
sorted = false
sorted = false,
): Promise<void> {
const results = this._displayedQuery;
if (results === undefined) {
throw new Error('trying to view a page of a query that is not loaded');
throw new Error("trying to view a page of a query that is not loaded");
}
const panel = await this.getPanel();
@@ -505,19 +567,27 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
const sortedResultsMap: SortedResultsMap = {};
Object.entries(results.completedQuery.sortedResultsInfo).forEach(
([k, v]) =>
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(panel, v))
(sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(
panel,
v,
)),
);
const resultSetSchemas = await this.getResultSetSchemas(results.completedQuery, sorted ? selectedTable : '');
const resultSetSchemas = await this.getResultSetSchemas(
results.completedQuery,
sorted ? selectedTable : "",
);
// If there is a specific sorted table selected, a different bqrs file is loaded that doesn't have all the result set names.
// Make sure that we load all result set names here.
// See https://github.com/github/vscode-codeql/issues/1005
const allResultSetSchemas = sorted ? await this.getResultSetSchemas(results.completedQuery, '') : resultSetSchemas;
const resultSetNames = allResultSetSchemas.map(schema => schema.name);
const allResultSetSchemas = sorted
? await this.getResultSetSchemas(results.completedQuery, "")
: resultSetSchemas;
const resultSetNames = allResultSetSchemas.map((schema) => schema.name);
const schema = resultSetSchemas.find(
(resultSet) => resultSet.name == selectedTable
(resultSet) => resultSet.name == selectedTable,
)!;
if (schema === undefined)
throw new Error(`Query result set '${selectedTable}' not found.`);
@@ -528,15 +598,15 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
schema.name,
{
offset: schema.pagination?.offsets[pageNumber],
pageSize
}
pageSize,
},
);
const resultSet = transformBqrsResultSet(schema, chunk);
const parsedResultSets: ParsedResultSets = {
pageNumber,
pageSize,
resultSet: { t: 'RawResultSet', ...resultSet },
resultSet: { t: "RawResultSet", ...resultSet },
numPages: numPagesOfResultSet(resultSet),
numInterpretedPages: numInterpretedPages(this._interpretation),
selectedTable: selectedTable,
@@ -544,12 +614,12 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
};
await this.postMessage({
t: 'setState',
t: "setState",
interpretation: this._interpretation,
origResultsPaths: results.completedQuery.query.resultsPaths,
resultsPath: this.convertPathToWebviewUri(
panel,
results.completedQuery.query.resultsPaths.resultsPath
results.completedQuery.query.resultsPaths.resultsPath,
),
parsedResultSets,
sortedResultsMap,
@@ -557,7 +627,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
shouldKeepOldResultsWhileRendering: false,
metadata: results.completedQuery.query.metadata,
queryName: this.labelProvider.getLabel(results),
queryPath: results.initialInfo.queryPath
queryPath: results.initialInfo.queryPath,
});
}
@@ -566,10 +636,12 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
resultsPaths: ResultsPaths,
sourceInfo: cli.SourceInfo | undefined,
sourceLocationPrefix: string,
sortState: InterpretedResultsSortState | undefined
sortState: InterpretedResultsSortState | undefined,
): Promise<Interpretation | undefined> {
if (!resultsPaths) {
void this.logger.log('No results path. Cannot display interpreted results.');
void this.logger.log(
"No results path. Cannot display interpreted results.",
);
return undefined;
}
let data;
@@ -579,7 +651,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
this.cliServer,
metadata,
resultsPaths,
sourceInfo
sourceInfo,
);
numTotalResults = data.dot.length;
} else {
@@ -587,10 +659,10 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
this.cliServer,
metadata,
resultsPaths,
sourceInfo
sourceInfo,
);
sarif.runs.forEach(run => {
sarif.runs.forEach((run) => {
if (run.results) {
sortInterpretedResults(run.results, sortState);
}
@@ -600,9 +672,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
data = sarif;
numTotalResults = (() => {
return sarif.runs?.[0]?.results
? sarif.runs[0].results.length
: 0;
return sarif.runs?.[0]?.results ? sarif.runs[0].results.length : 0;
})();
}
@@ -610,81 +680,89 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
data,
sourceLocationPrefix,
numTruncatedResults: 0,
numTotalResults
numTotalResults,
};
this._interpretation = interpretation;
return interpretation;
}
private getPageOfInterpretedResults(
pageNumber: number
): Interpretation {
private getPageOfInterpretedResults(pageNumber: number): Interpretation {
function getPageOfRun(run: Sarif.Run): Sarif.Run {
return {
...run, results: run.results?.slice(
...run,
results: run.results?.slice(
PAGE_SIZE.getValue<number>() * pageNumber,
PAGE_SIZE.getValue<number>() * (pageNumber + 1)
)
PAGE_SIZE.getValue<number>() * (pageNumber + 1),
),
};
}
const interp = this._interpretation;
if (interp === undefined) {
throw new Error('Tried to get interpreted results before interpretation finished');
throw new Error(
"Tried to get interpreted results before interpretation finished",
);
}
if (interp.data.t !== 'SarifInterpretationData')
return interp;
if (interp.data.t !== "SarifInterpretationData") return interp;
if (interp.data.runs.length !== 1) {
void this.logger.log(`Warning: SARIF file had ${interp.data.runs.length} runs, expected 1`);
void this.logger.log(
`Warning: SARIF file had ${interp.data.runs.length} runs, expected 1`,
);
}
return {
...interp,
data: {
...interp.data,
runs: [getPageOfRun(interp.data.runs[0])]
}
runs: [getPageOfRun(interp.data.runs[0])],
},
};
}
private async interpretResultsInfo(
query: QueryEvaluationInfo,
sortState: InterpretedResultsSortState | undefined
sortState: InterpretedResultsSortState | undefined,
): Promise<Interpretation | undefined> {
if (
query.canHaveInterpretedResults() &&
query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
const dbItem = this.databaseManager.findDatabaseItem(Uri.file(query.dbItemPath));
const dbItem = this.databaseManager.findDatabaseItem(
Uri.file(query.dbItemPath),
);
if (!dbItem) {
throw new Error(`Could not find database item for ${query.dbItemPath}`);
throw new Error(
`Could not find database item for ${query.dbItemPath}`,
);
}
const sourceLocationPrefix = await dbItem.getSourceLocationPrefix(
this.cliServer
this.cliServer,
);
const sourceArchiveUri = dbItem.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
await this._getInterpretedResults(
query.metadata,
query.resultsPaths,
sourceInfo,
sourceLocationPrefix,
sortState
sortState,
);
} catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
void showAndLogErrorMessage(
`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(e)}`
`Showing raw results instead of interpreted ones due to an error. ${getErrorMessage(
e,
)}`,
);
}
}
@@ -694,26 +772,26 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
private async showResultsAsDiagnostics(
resultsInfo: ResultsPaths,
metadata: QueryMetadata | undefined,
database: DatabaseItem
database: DatabaseItem,
): Promise<void> {
const sourceLocationPrefix = await database.getSourceLocationPrefix(
this.cliServer
this.cliServer,
);
const sourceArchiveUri = database.sourceArchive;
const sourceInfo =
sourceArchiveUri === undefined
? undefined
: {
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
sourceArchive: sourceArchiveUri.fsPath,
sourceLocationPrefix,
};
// TODO: Performance-testing to determine whether this truncation is necessary.
const interpretation = await this._getInterpretedResults(
metadata,
resultsInfo,
sourceInfo,
sourceLocationPrefix,
undefined
undefined,
);
if (!interpretation) {
@@ -724,7 +802,9 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
await this.showProblemResultsAsDiagnostics(interpretation, database);
} catch (e) {
void this.logger.log(
`Exception while computing problem results as diagnostics: ${getErrorMessage(e)}`
`Exception while computing problem results as diagnostics: ${getErrorMessage(
e,
)}`,
);
this._diagnosticCollection.clear();
}
@@ -732,16 +812,15 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
private async showProblemResultsAsDiagnostics(
interpretation: Interpretation,
databaseItem: DatabaseItem
databaseItem: DatabaseItem,
): Promise<void> {
const { data, sourceLocationPrefix } = interpretation;
if (data.t !== 'SarifInterpretationData')
return;
if (data.t !== "SarifInterpretationData") return;
if (!data.runs || !data.runs[0].results) {
void this.logger.log(
'Didn\'t find a run in the sarif results. Error processing sarif?'
"Didn't find a run in the sarif results. Error processing sarif?",
);
return;
}
@@ -751,24 +830,24 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
for (const result of data.runs[0].results) {
const message = result.message.text;
if (message === undefined) {
void this.logger.log('Sarif had result without plaintext message');
void this.logger.log("Sarif had result without plaintext message");
continue;
}
if (!result.locations) {
void this.logger.log('Sarif had result without location');
void this.logger.log("Sarif had result without location");
continue;
}
const sarifLoc = parseSarifLocation(
result.locations[0],
sourceLocationPrefix
sourceLocationPrefix,
);
if ('hint' in sarifLoc) {
if ("hint" in sarifLoc) {
continue;
}
const resultLocation = tryResolveLocation(sarifLoc, databaseItem);
if (!resultLocation) {
void this.logger.log('Sarif location was not resolvable ' + sarifLoc);
void this.logger.log("Sarif location was not resolvable " + sarifLoc);
continue;
}
const parsedMessage = parseSarifPlainTextMessage(message);
@@ -780,26 +859,26 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
const resultMessageChunks: string[] = [];
for (const section of parsedMessage) {
if (typeof section === 'string') {
if (typeof section === "string") {
resultMessageChunks.push(section);
} else {
resultMessageChunks.push(section.text);
const sarifChunkLoc = parseSarifLocation(
relatedLocationsById[section.dest],
sourceLocationPrefix
sourceLocationPrefix,
);
if ('hint' in sarifChunkLoc) {
if ("hint" in sarifChunkLoc) {
continue;
}
const referenceLocation = tryResolveLocation(
sarifChunkLoc,
databaseItem
databaseItem,
);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(
referenceLocation,
section.text
section.text,
);
relatedInformation.push(related);
}
@@ -807,8 +886,8 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
const diagnostic = new Diagnostic(
resultLocation.range,
resultMessageChunks.join(''),
DiagnosticSeverity.Warning
resultMessageChunks.join(""),
DiagnosticSeverity.Warning,
);
diagnostic.relatedInformation = relatedInformation;
@@ -823,7 +902,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
private convertPathPropertiesToWebviewUris(
panel: WebviewPanel,
info: SortedResultSetInfo
info: SortedResultSetInfo,
): SortedResultSetInfo {
return {
resultsPath: this.convertPathToWebviewUri(panel, info.resultsPath),
@@ -832,7 +911,7 @@ export class ResultsView extends AbstractWebview<IntoResultsViewMsg, FromResults
}
private handleSelectionChange(
event: vscode.TextEditorSelectionChangeEvent
event: vscode.TextEditorSelectionChangeEvent,
): void {
if (event.kind === vscode.TextEditorSelectionChangeKind.Command) {
return; // Ignore selection events we caused ourselves.

View File

@@ -1,8 +1,7 @@
import { Logger } from './logging';
import * as cp from 'child_process';
import { Disposable } from 'vscode';
import { MessageConnection } from 'vscode-jsonrpc';
import { Logger } from "./logging";
import * as cp from "child_process";
import { Disposable } from "vscode";
import { MessageConnection } from "vscode-jsonrpc";
/** A running query server process and its associated message connection. */
export class ServerProcess implements Disposable {
@@ -10,7 +9,12 @@ export class ServerProcess implements Disposable {
connection: MessageConnection;
logger: Logger;
constructor(child: cp.ChildProcess, connection: MessageConnection, private name: string, logger: Logger) {
constructor(
child: cp.ChildProcess,
connection: MessageConnection,
private name: string,
logger: Logger,
) {
this.child = child;
this.connection = connection;
this.logger = logger;

View File

@@ -1,4 +1,4 @@
import { languages, IndentAction, OnEnterRule } from 'vscode';
import { languages, IndentAction, OnEnterRule } from "vscode";
/**
* OnEnterRules are available in language-configurations, but you cannot specify them in the language-configuration.json.
@@ -12,18 +12,18 @@ import { languages, IndentAction, OnEnterRule } from 'vscode';
*/
export function install() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const langConfig = require('../language-configuration.json');
const langConfig = require("../language-configuration.json");
// setLanguageConfiguration requires a regexp for the wordpattern, not a string
langConfig.wordPattern = new RegExp(langConfig.wordPattern);
langConfig.onEnterRules = onEnterRules;
langConfig.indentationRules = {
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/,
};
languages.setLanguageConfiguration('ql', langConfig);
languages.setLanguageConfiguration('qll', langConfig);
languages.setLanguageConfiguration('dbscheme', langConfig);
languages.setLanguageConfiguration("ql", langConfig);
languages.setLanguageConfiguration("qll", langConfig);
languages.setLanguageConfiguration("dbscheme", langConfig);
}
const onEnterRules: OnEnterRule[] = [
@@ -31,18 +31,18 @@ const onEnterRules: OnEnterRule[] = [
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' },
action: { indentAction: IndentAction.IndentOutdent, appendText: " * " },
},
{
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction: IndentAction.None, appendText: ' * ' },
action: { indentAction: IndentAction.None, appendText: " * " },
},
{
// e.g. * ...|
beforeText: /^(\t|[ ])*[ ]\*([ ]([^\*]|\*(?!\/))*)?$/,
// oneLineAboveText: /^(\s*(\/\*\*|\*)).*/,
action: { indentAction: IndentAction.None, appendText: '* ' },
action: { indentAction: IndentAction.None, appendText: "* " },
},
{
// e.g. */|

View File

@@ -1,17 +1,22 @@
import { CancellationToken } from 'vscode';
import { ProgressCallback } from '../commandRunner';
import { DatabaseItem } from '../databases';
import { Dataset, deregisterDatabases, registerDatabases } from '../pure/legacy-messages';
import { InitialQueryInfo, LocalQueryInfo } from '../query-results';
import { QueryRunner } from '../queryRunner';
import { QueryWithResults } from '../run-queries-shared';
import { QueryServerClient } from './queryserver-client';
import { clearCacheInDatabase, compileAndRunQueryAgainstDatabase } from './run-queries';
import { upgradeDatabaseExplicit } from './upgrades';
import { CancellationToken } from "vscode";
import { ProgressCallback } from "../commandRunner";
import { DatabaseItem } from "../databases";
import {
Dataset,
deregisterDatabases,
registerDatabases,
} from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import { QueryRunner } from "../queryRunner";
import { QueryWithResults } from "../run-queries-shared";
import { QueryServerClient } from "./queryserver-client";
import {
clearCacheInDatabase,
compileAndRunQueryAgainstDatabase,
} from "./run-queries";
import { upgradeDatabaseExplicit } from "./upgrades";
export class LegacyQueryRunner extends QueryRunner {
constructor(public readonly qs: QueryServerClient) {
super();
}
@@ -20,40 +25,102 @@ export class LegacyQueryRunner extends QueryRunner {
return this.qs.cliServer;
}
async restartQueryServer(progress: ProgressCallback, token: CancellationToken): Promise<void> {
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
await this.qs.restartQueryServer(progress, token);
}
onStart(callBack: (progress: ProgressCallback, token: CancellationToken) => Promise<void>) {
onStart(
callBack: (
progress: ProgressCallback,
token: CancellationToken,
) => Promise<void>,
) {
this.qs.onDidStartQueryServer(callBack);
}
async clearCacheInDatabase(dbItem: DatabaseItem, progress: ProgressCallback, token: CancellationToken): Promise<void> {
async clearCacheInDatabase(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
await clearCacheInDatabase(this.qs, dbItem, progress, token);
}
async compileAndRunQueryAgainstDatabase(dbItem: DatabaseItem, initialInfo: InitialQueryInfo, queryStorageDir: string, progress: ProgressCallback, token: CancellationToken, templates?: Record<string, string>, queryInfo?: LocalQueryInfo): Promise<QueryWithResults> {
return await compileAndRunQueryAgainstDatabase(this.qs.cliServer, this.qs, dbItem, initialInfo, queryStorageDir, progress, token, templates, queryInfo);
async compileAndRunQueryAgainstDatabase(
dbItem: DatabaseItem,
initialInfo: InitialQueryInfo,
queryStorageDir: string,
progress: ProgressCallback,
token: CancellationToken,
templates?: Record<string, string>,
queryInfo?: LocalQueryInfo,
): Promise<QueryWithResults> {
return await compileAndRunQueryAgainstDatabase(
this.qs.cliServer,
this.qs,
dbItem,
initialInfo,
queryStorageDir,
progress,
token,
templates,
queryInfo,
);
}
async deregisterDatabase(progress: ProgressCallback, token: CancellationToken, dbItem: DatabaseItem): Promise<void> {
if (dbItem.contents && (await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())) {
const databases: Dataset[] = [{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
}];
await this.qs.sendRequest(deregisterDatabases, { databases }, token, progress);
async deregisterDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (
dbItem.contents &&
(await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())
) {
const databases: Dataset[] = [
{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: "default",
},
];
await this.qs.sendRequest(
deregisterDatabases,
{ databases },
token,
progress,
);
}
}
async registerDatabase(progress: ProgressCallback, token: CancellationToken, dbItem: DatabaseItem): Promise<void> {
if (dbItem.contents && (await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())) {
const databases: Dataset[] = [{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
}];
await this.qs.sendRequest(registerDatabases, { databases }, token, progress);
async registerDatabase(
progress: ProgressCallback,
token: CancellationToken,
dbItem: DatabaseItem,
): Promise<void> {
if (
dbItem.contents &&
(await this.qs.cliServer.cliConstraints.supportsDatabaseRegistration())
) {
const databases: Dataset[] = [
{
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: "default",
},
];
await this.qs.sendRequest(
registerDatabases,
{ databases },
token,
progress,
);
}
}
async upgradeDatabaseExplicit(dbItem: DatabaseItem, progress: ProgressCallback, token: CancellationToken): Promise<void> {
async upgradeDatabaseExplicit(
dbItem: DatabaseItem,
progress: ProgressCallback,
token: CancellationToken,
): Promise<void> {
await upgradeDatabaseExplicit(this.qs, dbItem, progress, token);
}

View File

@@ -1,24 +1,35 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import * as path from "path";
import * as fs from "fs-extra";
import { DisposableObject } from '../pure/disposable-object';
import { CancellationToken, commands } from 'vscode';
import { createMessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from '../cli';
import { QueryServerConfig } from '../config';
import { Logger, ProgressReporter } from '../logging';
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from '../pure/legacy-messages';
import * as messages from '../pure/legacy-messages';
import { ProgressCallback, ProgressTask } from '../commandRunner';
import { findQueryLogFile } from '../run-queries-shared';
import { ServerProcess } from '../json-rpc-server';
import { DisposableObject } from "../pure/disposable-object";
import { CancellationToken, commands } from "vscode";
import { createMessageConnection, RequestType } from "vscode-jsonrpc";
import * as cli from "../cli";
import { QueryServerConfig } from "../config";
import { Logger, ProgressReporter } from "../logging";
import {
completeQuery,
EvaluationResult,
progress,
ProgressMessage,
WithProgressId,
} from "../pure/legacy-messages";
import * as messages from "../pure/legacy-messages";
import { ProgressCallback, ProgressTask } from "../commandRunner";
import { findQueryLogFile } from "../run-queries-shared";
import { ServerProcess } from "../json-rpc-server";
type WithProgressReporting = (task: (progress: ProgressReporter, token: CancellationToken) => Thenable<void>) => Thenable<void>;
type WithProgressReporting = (
task: (
progress: ProgressReporter,
token: CancellationToken,
) => Thenable<void>,
) => Thenable<void>;
type ServerOpts = {
logger: Logger;
contextStoragePath: string;
}
};
/**
* Client that manages a query server process.
@@ -27,10 +38,11 @@ type ServerOpts = {
* to restart it (which disposes the existing process and starts a new one).
*/
export class QueryServerClient extends DisposableObject {
serverProcess?: ServerProcess;
evaluationResultCallbacks: { [key: number]: (res: EvaluationResult) => void };
progressCallbacks: { [key: number]: ((res: ProgressMessage) => void) | undefined };
progressCallbacks: {
[key: number]: ((res: ProgressMessage) => void) | undefined;
};
nextCallback: number;
nextProgress: number;
withProgressReporting: WithProgressReporting;
@@ -42,7 +54,7 @@ export class QueryServerClient extends DisposableObject {
// we need here.
readonly onDidStartQueryServer = (e: ProgressTask<void>) => {
this.queryServerStartListeners.push(e);
}
};
public activeQueryLogFile: string | undefined;
@@ -50,13 +62,16 @@ export class QueryServerClient extends DisposableObject {
readonly config: QueryServerConfig,
readonly cliServer: cli.CodeQLCliServer,
readonly opts: ServerOpts,
withProgressReporting: WithProgressReporting
withProgressReporting: WithProgressReporting,
) {
super();
// When the query server configuration changes, restart the query server.
if (config.onDidChangeConfiguration !== undefined) {
this.push(config.onDidChangeConfiguration(() =>
commands.executeCommand('codeQL.restartQueryServer')));
this.push(
config.onDidChangeConfiguration(() =>
commands.executeCommand("codeQL.restartQueryServer"),
),
);
}
this.withProgressReporting = withProgressReporting;
this.nextCallback = 0;
@@ -74,24 +89,23 @@ export class QueryServerClient extends DisposableObject {
if (this.serverProcess !== undefined) {
this.disposeAndStopTracking(this.serverProcess);
} else {
void this.logger.log('No server process to be stopped.');
void this.logger.log("No server process to be stopped.");
}
}
/** Restarts the query server by disposing of the current server process and then starting a new one. */
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken
token: CancellationToken,
): Promise<void> {
this.stopQueryServer();
await this.startQueryServer();
// Ensure we await all responses from event handlers so that
// errors can be properly reported to the user.
await Promise.all(this.queryServerStartListeners.map(handler => handler(
progress,
token
)));
await Promise.all(
this.queryServerStartListeners.map((handler) => handler(progress, token)),
);
}
showLog(): void {
@@ -101,86 +115,108 @@ export class QueryServerClient extends DisposableObject {
/** Starts a new query server process, sending progress messages to the status bar. */
async startQueryServer(): Promise<void> {
// Use an arrow function to preserve the value of `this`.
return this.withProgressReporting((progress, _) => this.startQueryServerImpl(progress));
return this.withProgressReporting((progress, _) =>
this.startQueryServerImpl(progress),
);
}
/** Starts a new query server process, sending progress messages to the given reporter. */
private async startQueryServerImpl(progressReporter: ProgressReporter): Promise<void> {
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
private async startQueryServerImpl(
progressReporter: ProgressReporter,
): Promise<void> {
const ramArgs = await this.cliServer.resolveRam(
this.config.queryMemoryMb,
progressReporter,
);
const args = ["--threads", this.config.numThreads.toString()].concat(
ramArgs,
);
if (this.config.saveCache) {
args.push('--save-cache');
args.push("--save-cache");
}
if (this.config.cacheSize > 0) {
args.push('--max-disk-cache');
args.push("--max-disk-cache");
args.push(this.config.cacheSize.toString());
}
if (await this.cliServer.cliConstraints.supportsDatabaseRegistration()) {
args.push('--require-db-registration');
args.push("--require-db-registration");
}
if (await this.cliServer.cliConstraints.supportsOldEvalStats() && !(await this.cliServer.cliConstraints.supportsPerQueryEvalLog())) {
args.push('--old-eval-stats');
if (
(await this.cliServer.cliConstraints.supportsOldEvalStats()) &&
!(await this.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
args.push("--old-eval-stats");
}
if (await this.cliServer.cliConstraints.supportsStructuredEvalLog()) {
const structuredLogFile = `${this.opts.contextStoragePath}/structured-evaluator-log.json`;
await fs.ensureFile(structuredLogFile);
args.push('--evaluator-log');
args.push("--evaluator-log");
args.push(structuredLogFile);
// We hard-code the verbosity level to 5 and minify to false.
// This will be the behavior of the per-query structured logging in the CLI after 2.8.3.
args.push('--evaluator-log-level');
args.push('5');
args.push("--evaluator-log-level");
args.push("5");
}
if (this.config.debug) {
args.push('--debug', '--tuple-counting');
args.push("--debug", "--tuple-counting");
}
if (cli.shouldDebugQueryServer()) {
args.push('-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9010,server=n,suspend=y,quiet=y');
args.push(
"-J=-agentlib:jdwp=transport=dt_socket,address=localhost:9010,server=n,suspend=y,quiet=y",
);
}
const child = cli.spawnServer(
this.config.codeQlPath,
'CodeQL query server',
['execute', 'query-server'],
"CodeQL query server",
["execute", "query-server"],
args,
this.logger,
data => this.logger.log(data.toString(), {
trailingNewline: false,
additionalLogLocation: this.activeQueryLogFile
}),
(data) =>
this.logger.log(data.toString(), {
trailingNewline: false,
additionalLogLocation: this.activeQueryLogFile,
}),
undefined, // no listener for stdout
progressReporter
progressReporter,
);
progressReporter.report({ message: 'Connecting to CodeQL query server' });
progressReporter.report({ message: "Connecting to CodeQL query server" });
const connection = createMessageConnection(child.stdout, child.stdin);
connection.onRequest(completeQuery, res => {
connection.onRequest(completeQuery, (res) => {
if (!(res.runId in this.evaluationResultCallbacks)) {
void this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
void this.logger.log(
`No callback associated with run id ${res.runId}, continuing without executing any callback`,
);
} else {
this.evaluationResultCallbacks[res.runId](res);
}
return {};
});
connection.onNotification(progress, res => {
connection.onNotification(progress, (res) => {
const callback = this.progressCallbacks[res.id];
if (callback) {
callback(res);
}
});
this.serverProcess = new ServerProcess(child, connection, 'Query server', this.logger);
this.serverProcess = new ServerProcess(
child,
connection,
"Query server",
this.logger,
);
// Ensure the server process is disposed together with this client.
this.track(this.serverProcess);
connection.listen();
progressReporter.report({ message: 'Connected to CodeQL query server' });
progressReporter.report({ message: "Connected to CodeQL query server" });
this.nextCallback = 0;
this.nextProgress = 0;
this.progressCallbacks = {};
@@ -201,16 +237,25 @@ export class QueryServerClient extends DisposableObject {
return this.serverProcess!.child.pid || 0;
}
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
async sendRequest<P, R, E, RO>(
type: RequestType<WithProgressId<P>, R, E, RO>,
parameter: P,
token?: CancellationToken,
progress?: (res: ProgressMessage) => void,
): Promise<R> {
const id = this.nextProgress++;
this.progressCallbacks[id] = progress;
this.updateActiveQuery(type.method, parameter);
try {
if (this.serverProcess === undefined) {
throw new Error('No query server process found.');
throw new Error("No query server process found.");
}
return await this.serverProcess.connection.sendRequest(type, { body: parameter, progressId: id }, token);
return await this.serverProcess.connection.sendRequest(
type,
{ body: parameter, progressId: id },
token,
);
} finally {
delete this.progressCallbacks[id];
}
@@ -226,7 +271,9 @@ export class QueryServerClient extends DisposableObject {
*/
private updateActiveQuery(method: string, parameter: any): void {
if (method === messages.compileQuery.method) {
this.activeQueryLogFile = findQueryLogFile(path.dirname(parameter.resultPath));
this.activeQueryLogFile = findQueryLogFile(
path.dirname(parameter.resultPath),
);
}
}
}

View File

@@ -1,31 +1,31 @@
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as tmp from 'tmp-promise';
import * as path from 'path';
import {
CancellationToken,
Uri,
} from 'vscode';
import { ErrorCodes, ResponseError } from 'vscode-languageclient';
import * as crypto from "crypto";
import * as fs from "fs-extra";
import * as tmp from "tmp-promise";
import * as path from "path";
import { CancellationToken, Uri } from "vscode";
import { ErrorCodes, ResponseError } from "vscode-languageclient";
import * as cli from '../cli';
import { DatabaseItem, } from '../databases';
import * as cli from "../cli";
import { DatabaseItem } from "../databases";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogWarningMessage,
tryGetQueryMetadata,
upgradesTmpDir
} from '../helpers';
import { ProgressCallback } from '../commandRunner';
import { QueryMetadata } from '../pure/interface-types';
import { logger } from '../logging';
import * as messages from '../pure/legacy-messages';
import { InitialQueryInfo, LocalQueryInfo } from '../query-results';
import * as qsClient from './queryserver-client';
import { getErrorMessage } from '../pure/helpers-pure';
import { compileDatabaseUpgradeSequence, upgradeDatabaseExplicit } from './upgrades';
import { QueryEvaluationInfo, QueryWithResults } from '../run-queries-shared';
upgradesTmpDir,
} from "../helpers";
import { ProgressCallback } from "../commandRunner";
import { QueryMetadata } from "../pure/interface-types";
import { logger } from "../logging";
import * as messages from "../pure/legacy-messages";
import { InitialQueryInfo, LocalQueryInfo } from "../query-results";
import * as qsClient from "./queryserver-client";
import { getErrorMessage } from "../pure/helpers-pure";
import {
compileDatabaseUpgradeSequence,
upgradeDatabaseExplicit,
} from "./upgrades";
import { QueryEvaluationInfo, QueryWithResults } from "../run-queries-shared";
/**
* A collection of evaluation-time information about a query,
@@ -34,7 +34,6 @@ import { QueryEvaluationInfo, QueryWithResults } from '../run-queries-shared';
* output and results.
*/
export class QueryInProgress {
public queryEvalInfo: QueryEvaluationInfo;
/**
* Note that in the {@link slurpQueryHistory} method, we create a QueryEvaluationInfo instance
@@ -49,7 +48,13 @@ export class QueryInProgress {
readonly metadata?: QueryMetadata,
readonly templates?: Record<string, string>,
) {
this.queryEvalInfo = new QueryEvaluationInfo(querySaveDir, dbItemPath, databaseHasMetadataFile, quickEvalPosition, metadata);
this.queryEvalInfo = new QueryEvaluationInfo(
querySaveDir,
dbItemPath,
databaseHasMetadataFile,
quickEvalPosition,
metadata,
);
/**/
}
@@ -57,7 +62,6 @@ export class QueryInProgress {
return this.queryEvalInfo.compileQueryPath;
}
async run(
qs: qsClient.QueryServerClient,
upgradeQlo: string | undefined,
@@ -68,19 +72,21 @@ export class QueryInProgress {
queryInfo?: LocalQueryInfo,
): Promise<messages.EvaluationResult> {
if (!dbItem.contents || dbItem.error) {
throw new Error('Can\'t run query on invalid database.');
throw new Error("Can't run query on invalid database.");
}
let result: messages.EvaluationResult | null = null;
const callbackId = qs.registerCallback(res => {
const callbackId = qs.registerCallback((res) => {
result = {
...res,
logFileLocation: this.queryEvalInfo.logPath
logFileLocation: this.queryEvalInfo.logPath,
};
});
const availableMlModelUris: messages.MlModel[] = availableMlModels.map(model => ({ uri: Uri.file(model.path).toString(true) }));
const availableMlModelUris: messages.MlModel[] = availableMlModels.map(
(model) => ({ uri: Uri.file(model.path).toString(true) }),
);
const queryToRun: messages.QueryToRun = {
resultsPath: this.queryEvalInfo.resultsPaths.resultsPath,
@@ -95,50 +101,63 @@ export class QueryInProgress {
const dataset: messages.Dataset = {
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default'
workingSet: "default",
};
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
if (
queryInfo &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.startLog, {
db: dataset,
logPath: this.queryEvalInfo.evalLogPath,
});
}
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false
useSequenceHint: false,
};
try {
await qs.sendRequest(messages.runQueries, params, token, progress);
if (qs.config.customLogDirectory) {
void showAndLogWarningMessage(
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${this.queryEvalInfo.logPath}.`
`Custom log directories are no longer supported. The "codeQL.runningQueries.customLogDirectory" setting is deprecated. Unset the setting to stop seeing this message. Query logs saved to ${this.queryEvalInfo.logPath}.`,
);
}
} finally {
qs.unRegisterCallback(callbackId);
if (queryInfo && await qs.cliServer.cliConstraints.supportsPerQueryEvalLog()) {
if (
queryInfo &&
(await qs.cliServer.cliConstraints.supportsPerQueryEvalLog())
) {
await qs.sendRequest(messages.endLog, {
db: dataset,
logPath: this.queryEvalInfo.evalLogPath,
});
if (await this.queryEvalInfo.hasEvalLog()) {
await this.queryEvalInfo.addQueryLogs(queryInfo, qs.cliServer, qs.logger);
await this.queryEvalInfo.addQueryLogs(
queryInfo,
qs.cliServer,
qs.logger,
);
} else {
void showAndLogWarningMessage(`Failed to write structured evaluator log to ${this.queryEvalInfo.evalLogPath}.`);
void showAndLogWarningMessage(
`Failed to write structured evaluator log to ${this.queryEvalInfo.evalLogPath}.`,
);
}
}
}
return result || {
evaluationTime: 0,
message: 'No result from server',
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR
};
return (
result || {
evaluationTime: 0,
message: "No result from server",
queryId: -1,
runId: callbackId,
resultType: messages.QueryResultType.OTHER_ERROR,
}
);
}
async compile(
@@ -149,9 +168,11 @@ export class QueryInProgress {
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult | undefined;
try {
const target = this.quickEvalPosition ? {
quickEval: { quickEvalPos: this.quickEvalPosition }
} : { query: {} };
const target = this.quickEvalPosition
? {
quickEval: { quickEvalPos: this.quickEvalPosition },
}
: { query: {} };
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
@@ -162,21 +183,30 @@ export class QueryInProgress {
noComputeGetUrl: false,
noComputeToString: false,
computeDefaultStrings: true,
emitDebugInfo: true
emitDebugInfo: true,
},
extraOptions: {
timeoutSecs: qs.config.timeoutSecs
timeoutSecs: qs.config.timeoutSecs,
},
queryToCheck: program,
resultPath: this.compiledQueryPath,
target,
};
compiled = await qs.sendRequest(messages.compileQuery, params, token, progress);
compiled = await qs.sendRequest(
messages.compileQuery,
params,
token,
progress,
);
} finally {
void qs.logger.log(' - - - COMPILATION DONE - - - ', { additionalLogLocation: this.queryEvalInfo.logPath });
void qs.logger.log(" - - - COMPILATION DONE - - - ", {
additionalLogLocation: this.queryEvalInfo.logPath,
});
}
return (compiled?.messages || []).filter(msg => msg.severity === messages.Severity.ERROR);
return (compiled?.messages || []).filter(
(msg) => msg.severity === messages.Severity.ERROR,
);
}
}
@@ -187,12 +217,12 @@ export async function clearCacheInDatabase(
token: CancellationToken,
): Promise<messages.ClearCacheResult> {
if (dbItem.contents === undefined) {
throw new Error('Can\'t clear the cache in an invalid database.');
throw new Error("Can't clear the cache in an invalid database.");
}
const db: messages.Dataset = {
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default',
workingSet: "default",
};
const params: messages.ClearCacheParams = {
@@ -203,7 +233,6 @@ export async function clearCacheInDatabase(
return qs.sendRequest(messages.clearCache, params, token, progress);
}
/**
* Compare the dbscheme implied by the query `query` and that of the current database.
* - If they are compatible, do nothing.
@@ -222,9 +251,16 @@ async function checkDbschemeCompatibility(
const searchPath = getOnDiskWorkspaceFolders();
if (dbItem.contents?.dbSchemeUri !== undefined) {
const { finalDbscheme } = await cliServer.resolveUpgrades(dbItem.contents.dbSchemeUri.fsPath, searchPath, false);
const hash = async function(filename: string): Promise<string> {
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
const { finalDbscheme } = await cliServer.resolveUpgrades(
dbItem.contents.dbSchemeUri.fsPath,
searchPath,
false,
);
const hash = async function (filename: string): Promise<string> {
return crypto
.createHash("sha256")
.update(await fs.readFile(filename))
.digest("hex");
};
// At this point, we have learned about three dbschemes:
@@ -242,22 +278,19 @@ async function checkDbschemeCompatibility(
reportNoUpgradePath(qlProgram, query);
}
if (upgradableTo == dbschemeOfLib &&
dbschemeOfDb != dbschemeOfLib) {
if (upgradableTo == dbschemeOfLib && dbschemeOfDb != dbschemeOfLib) {
// Try to upgrade the database
await upgradeDatabaseExplicit(
qs,
dbItem,
progress,
token
);
await upgradeDatabaseExplicit(qs, dbItem, progress, token);
}
}
}
function reportNoUpgradePath(qlProgram: messages.QlProgram, query: QueryInProgress): void {
function reportNoUpgradePath(
qlProgram: messages.QlProgram,
query: QueryInProgress,
): void {
throw new Error(
`Query ${qlProgram.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`
`Query ${qlProgram.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`,
);
}
@@ -273,9 +306,8 @@ async function compileNonDestructiveUpgrade(
progress: ProgressCallback,
token: CancellationToken,
): Promise<string> {
if (!dbItem?.contents?.dbSchemeUri) {
throw new Error('Database is invalid, and cannot be upgraded.');
throw new Error("Database is invalid, and cannot be upgraded.");
}
// When packaging is used, dependencies may exist outside of the workspace and they are always on the resolved search path.
@@ -288,15 +320,22 @@ async function compileNonDestructiveUpgrade(
dbItem.contents.dbSchemeUri.fsPath,
upgradesPath,
true,
query.queryDbscheme
query.queryDbscheme,
);
if (!matchesTarget) {
reportNoUpgradePath(qlProgram, query);
}
const result = await compileDatabaseUpgradeSequence(qs, dbItem, scripts, upgradeTemp, progress, token);
const result = await compileDatabaseUpgradeSequence(
qs,
dbItem,
scripts,
upgradeTemp,
progress,
token,
);
if (result.compiledUpgrade === undefined) {
const error = result.error || '[no error message available]';
const error = result.error || "[no error message available]";
throw new Error(error);
}
// We can upgrade to the actual target
@@ -305,8 +344,6 @@ async function compileNonDestructiveUpgrade(
return result.compiledUpgrade;
}
export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
@@ -319,16 +356,23 @@ export async function compileAndRunQueryAgainstDatabase(
queryInfo?: LocalQueryInfo, // May be omitted for queries not initiated by the user. If omitted we won't create a structured log for the query.
): Promise<QueryWithResults> {
if (!dbItem.contents || !dbItem.contents.dbSchemeUri) {
throw new Error(`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`);
throw new Error(
`Database ${dbItem.databaseUri} does not have a CodeQL database scheme.`,
);
}
// Get the workspace folder paths.
const diskWorkspaceFolders = getOnDiskWorkspaceFolders();
// Figure out the library path for the query.
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, initialInfo.queryPath);
const packConfig = await cliServer.resolveLibraryPath(
diskWorkspaceFolders,
initialInfo.queryPath,
);
if (!packConfig.dbscheme) {
throw new Error('Could not find a database scheme for this query. Please check that you have a valid qlpack.yml file for this query, which refers to a database scheme either in the `dbscheme` field or through one of its dependencies.');
throw new Error(
"Could not find a database scheme for this query. Please check that you have a valid qlpack.yml file for this query, which refers to a database scheme either in the `dbscheme` field or through one of its dependencies.",
);
}
// Check whether the query has an entirely different schema from the
@@ -338,8 +382,16 @@ export async function compileAndRunQueryAgainstDatabase(
const querySchemaName = path.basename(packConfig.dbscheme);
const dbSchemaName = path.basename(dbItem.contents.dbSchemeUri.fsPath);
if (querySchemaName != dbSchemaName) {
void logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
throw new Error(`The query ${path.basename(initialInfo.queryPath)} cannot be run against the selected database (${dbItem.name}): their target languages are different. Please select a different database and try again.`);
void logger.log(
`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`,
);
throw new Error(
`The query ${path.basename(
initialInfo.queryPath,
)} cannot be run against the selected database (${
dbItem.name
}): their target languages are different. Please select a different database and try again.`,
);
}
const qlProgram: messages.QlProgram = {
@@ -351,31 +403,43 @@ export async function compileAndRunQueryAgainstDatabase(
// we use the database's DB scheme here instead of the DB scheme
// from the current document's project.
dbschemePath: dbItem.contents.dbSchemeUri.fsPath,
queryPath: initialInfo.queryPath
queryPath: initialInfo.queryPath,
};
// Read the query metadata if possible, to use in the UI.
const metadata = await tryGetQueryMetadata(cliServer, qlProgram.queryPath);
let availableMlModels: cli.MlModelInfo[] = [];
if (!await cliServer.cliConstraints.supportsResolveMlModels()) {
void logger.log('Resolving ML models is unsupported by this version of the CLI. Running the query without any ML models.');
if (!(await cliServer.cliConstraints.supportsResolveMlModels())) {
void logger.log(
"Resolving ML models is unsupported by this version of the CLI. Running the query without any ML models.",
);
} else {
try {
availableMlModels = (await cliServer.resolveMlModels(diskWorkspaceFolders, initialInfo.queryPath)).models;
availableMlModels = (
await cliServer.resolveMlModels(
diskWorkspaceFolders,
initialInfo.queryPath,
)
).models;
if (availableMlModels.length) {
void logger.log(`Found available ML models at the following paths: ${availableMlModels.map(x => `'${x.path}'`).join(', ')}.`);
void logger.log(
`Found available ML models at the following paths: ${availableMlModels
.map((x) => `'${x.path}'`)
.join(", ")}.`,
);
} else {
void logger.log('Did not find any available ML models.');
void logger.log("Did not find any available ML models.");
}
} catch (e) {
const message = `Couldn't resolve available ML models for ${qlProgram.queryPath}. Running the ` +
const message =
`Couldn't resolve available ML models for ${qlProgram.queryPath}. Running the ` +
`query without any ML models: ${e}.`;
void showAndLogErrorMessage(message);
}
}
const hasMetadataFile = (await dbItem.hasMetadataFile());
const hasMetadataFile = await dbItem.hasMetadataFile();
const query = new QueryInProgress(
path.join(queryStorageDir, initialInfo.id),
dbItem.databaseUri.fsPath,
@@ -383,7 +447,7 @@ export async function compileAndRunQueryAgainstDatabase(
packConfig.dbscheme,
initialInfo.quickEvalPosition,
metadata,
templates
templates,
);
await query.queryEvalInfo.createTimestampFile();
@@ -392,25 +456,49 @@ export async function compileAndRunQueryAgainstDatabase(
let upgradeQlo;
if (await cliServer.cliConstraints.supportsNonDestructiveUpgrades()) {
upgradeDir = await tmp.dir({ dir: upgradesTmpDir, unsafeCleanup: true });
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, qlProgram, dbItem, progress, token);
upgradeQlo = await compileNonDestructiveUpgrade(
qs,
upgradeDir,
query,
qlProgram,
dbItem,
progress,
token,
);
} else {
await checkDbschemeCompatibility(cliServer, qs, query, qlProgram, dbItem, progress, token);
await checkDbschemeCompatibility(
cliServer,
qs,
query,
qlProgram,
dbItem,
progress,
token,
);
}
let errors;
try {
errors = await query.compile(qs, qlProgram, progress, token);
} catch (e) {
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
return createSyntheticResult(query, 'Query cancelled');
return createSyntheticResult(query, "Query cancelled");
} else {
throw e;
}
}
if (errors.length === 0) {
const result = await query.run(qs, upgradeQlo, availableMlModels, dbItem, progress, token, queryInfo);
const result = await query.run(
qs,
upgradeQlo,
availableMlModels,
dbItem,
progress,
token,
queryInfo,
);
if (result.resultType !== messages.QueryResultType.SUCCESS) {
const message = result.message || 'Failed to run query';
const message = result.message || "Failed to run query";
void logger.log(message);
void showAndLogErrorMessage(message);
}
@@ -424,7 +512,7 @@ export async function compileAndRunQueryAgainstDatabase(
logFileLocation: result.logFileLocation,
dispose: () => {
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
}
},
};
} else {
// Error dialogs are limited in size and scrollability,
@@ -433,26 +521,34 @@ export async function compileAndRunQueryAgainstDatabase(
// However we don't show quick eval errors there so we need to display them anyway.
void qs.logger.log(
`Failed to compile query ${initialInfo.queryPath} against database scheme ${qlProgram.dbschemePath}:`,
{ additionalLogLocation: query.queryEvalInfo.logPath }
{ additionalLogLocation: query.queryEvalInfo.logPath },
);
const formattedMessages: string[] = [];
for (const error of errors) {
const message = error.message || '[no error message available]';
const message = error.message || "[no error message available]";
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
formattedMessages.push(formatted);
void qs.logger.log(formatted, { additionalLogLocation: query.queryEvalInfo.logPath });
void qs.logger.log(formatted, {
additionalLogLocation: query.queryEvalInfo.logPath,
});
}
if (initialInfo.isQuickEval && formattedMessages.length <= 2) {
// If there are more than 2 error messages, they will not be displayed well in a popup
// and will be trimmed by the function displaying the error popup. Accordingly, we only
// try to show the errors if there are 2 or less, otherwise we direct the user to the log.
void showAndLogErrorMessage('Quick evaluation compilation failed: ' + formattedMessages.join('\n'));
void showAndLogErrorMessage(
"Quick evaluation compilation failed: " +
formattedMessages.join("\n"),
);
} else {
void showAndLogErrorMessage((initialInfo.isQuickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
void showAndLogErrorMessage(
(initialInfo.isQuickEval ? "Quick evaluation" : "Query") +
compilationFailedErrorTail,
);
}
return createSyntheticResult(query, 'Query had compilation errors');
return createSyntheticResult(query, "Query had compilation errors");
}
} finally {
try {
@@ -460,30 +556,34 @@ export async function compileAndRunQueryAgainstDatabase(
} catch (e) {
void qs.logger.log(
`Could not clean up the upgrades dir. Reason: ${getErrorMessage(e)}`,
{ additionalLogLocation: query.queryEvalInfo.logPath }
{ additionalLogLocation: query.queryEvalInfo.logPath },
);
}
}
}
const compilationFailedErrorTail = ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
' and choose CodeQL Query Server from the dropdown.';
const compilationFailedErrorTail =
" compilation failed. Please make sure there are no errors in the query, the database is up to date," +
" and the query and database use the same target language. For more details on the error, go to View > Output," +
" and choose CodeQL Query Server from the dropdown.";
export function formatLegacyMessage(result: messages.EvaluationResult) {
switch (result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${Math.round(result.evaluationTime / 1000)} seconds`;
return `cancelled after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
case messages.QueryResultType.OOM:
return 'out of memory';
return "out of memory";
case messages.QueryResultType.SUCCESS:
return `finished in ${Math.round(result.evaluationTime / 1000)} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${Math.round(result.evaluationTime / 1000)} seconds`;
return `timed out after ${Math.round(
result.evaluationTime / 1000,
)} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return result.message ? `failed: ${result.message}` : 'failed';
return result.message ? `failed: ${result.message}` : "failed";
}
}
@@ -505,12 +605,15 @@ function createSyntheticResult(
runId: 0,
},
successful: false,
dispose: () => { /**/ },
dispose: () => {
/**/
},
};
}
function createSimpleTemplates(templates: Record<string, string> | undefined): messages.TemplateDefinitions | undefined {
function createSimpleTemplates(
templates: Record<string, string> | undefined,
): messages.TemplateDefinitions | undefined {
if (!templates) {
return undefined;
}
@@ -518,8 +621,8 @@ function createSimpleTemplates(templates: Record<string, string> | undefined): m
for (const key of Object.keys(templates)) {
result[key] = {
values: {
tuples: [[{ stringValue: templates[key] }]]
}
tuples: [[{ stringValue: templates[key] }]],
},
};
}
return result;

View File

@@ -1,12 +1,16 @@
import * as vscode from 'vscode';
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage, tmpDir } from '../helpers';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { logger } from '../logging';
import * as messages from '../pure/legacy-messages';
import * as qsClient from './queryserver-client';
import * as tmp from 'tmp-promise';
import * as path from 'path';
import { DatabaseItem } from '../databases';
import * as vscode from "vscode";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
tmpDir,
} from "../helpers";
import { ProgressCallback, UserCancellationException } from "../commandRunner";
import { logger } from "../logging";
import * as messages from "../pure/legacy-messages";
import * as qsClient from "./queryserver-client";
import * as tmp from "tmp-promise";
import * as path from "path";
import { DatabaseItem } from "../databases";
/**
* Maximum number of lines to include from database upgrade message,
@@ -15,7 +19,6 @@ import { DatabaseItem } from '../databases';
*/
const MAX_UPGRADE_MESSAGE_LINES = 10;
/**
* Compile a database upgrade sequence.
* Callers must check that this is valid with the current queryserver first.
@@ -26,19 +29,29 @@ export async function compileDatabaseUpgradeSequence(
resolvedSequence: string[],
currentUpgradeTmp: tmp.DirectoryResult,
progress: ProgressCallback,
token: vscode.CancellationToken
token: vscode.CancellationToken,
): Promise<messages.CompileUpgradeSequenceResult> {
if (dbItem.contents === undefined || dbItem.contents.dbSchemeUri === undefined) {
throw new Error('Database is invalid, and cannot be upgraded.');
if (
dbItem.contents === undefined ||
dbItem.contents.dbSchemeUri === undefined
) {
throw new Error("Database is invalid, and cannot be upgraded.");
}
if (!await qs.cliServer.cliConstraints.supportsNonDestructiveUpgrades()) {
throw new Error('The version of codeql is too old to run non-destructive upgrades.');
if (!(await qs.cliServer.cliConstraints.supportsNonDestructiveUpgrades())) {
throw new Error(
"The version of codeql is too old to run non-destructive upgrades.",
);
}
// If possible just compile the upgrade sequence
return await qs.sendRequest(messages.compileUpgradeSequence, {
upgradeTempDir: currentUpgradeTmp.path,
upgradePaths: resolvedSequence
}, token, progress);
return await qs.sendRequest(
messages.compileUpgradeSequence,
{
upgradeTempDir: currentUpgradeTmp.path,
upgradePaths: resolvedSequence,
},
token,
progress,
);
}
async function compileDatabaseUpgrade(
@@ -48,30 +61,35 @@ async function compileDatabaseUpgrade(
resolvedSequence: string[],
currentUpgradeTmp: tmp.DirectoryResult,
progress: ProgressCallback,
token: vscode.CancellationToken
token: vscode.CancellationToken,
): Promise<messages.CompileUpgradeResult> {
if (!dbItem.contents?.dbSchemeUri) {
throw new Error('Database is invalid, and cannot be upgraded.');
throw new Error("Database is invalid, and cannot be upgraded.");
}
// We have the upgrades we want but compileUpgrade
// requires searching for them. So we use the parent directories of the upgrades
// as the upgrade path.
const parentDirs = resolvedSequence.map(dir => path.dirname(dir));
const parentDirs = resolvedSequence.map((dir) => path.dirname(dir));
const uniqueParentDirs = new Set(parentDirs);
progress({
step: 1,
maxStep: 3,
message: 'Checking for database upgrades'
message: "Checking for database upgrades",
});
return qs.sendRequest(messages.compileUpgrade, {
upgrade: {
fromDbscheme: dbItem.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme,
additionalUpgrades: Array.from(uniqueParentDirs)
return qs.sendRequest(
messages.compileUpgrade,
{
upgrade: {
fromDbscheme: dbItem.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme,
additionalUpgrades: Array.from(uniqueParentDirs),
},
upgradeTempDir: currentUpgradeTmp.path,
singleFileUpgrades: true,
},
upgradeTempDir: currentUpgradeTmp.path,
singleFileUpgrades: true,
}, token, progress);
token,
progress,
);
}
/**
@@ -81,10 +99,9 @@ async function compileDatabaseUpgrade(
async function checkAndConfirmDatabaseUpgrade(
compiled: messages.CompiledUpgrades,
db: DatabaseItem,
quiet: boolean
quiet: boolean,
): Promise<void> {
let descriptionMessage = '';
let descriptionMessage = "";
const descriptions = getUpgradeDescriptions(compiled);
for (const script of descriptions) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
@@ -92,7 +109,6 @@ async function checkAndConfirmDatabaseUpgrade(
}
void logger.log(descriptionMessage);
// If the quiet flag is set, do the upgrade without a popup.
if (quiet) {
return;
@@ -100,39 +116,52 @@ async function checkAndConfirmDatabaseUpgrade(
// Ask the user to confirm the upgrade.
const showLogItem: vscode.MessageItem = { title: 'No, Show Changes', isCloseAffordance: true };
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true };
const showLogItem: vscode.MessageItem = {
title: "No, Show Changes",
isCloseAffordance: true,
};
const yesItem = { title: "Yes", isCloseAffordance: false };
const noItem = { title: "No", isCloseAffordance: true };
const dialogOptions: vscode.MessageItem[] = [yesItem, noItem];
let messageLines = descriptionMessage.split('\n');
let messageLines = descriptionMessage.split("\n");
if (messageLines.length > MAX_UPGRADE_MESSAGE_LINES) {
messageLines = messageLines.slice(0, MAX_UPGRADE_MESSAGE_LINES);
messageLines.push('The list of upgrades was truncated, click "No, Show Changes" to see the full list.');
messageLines.push(
'The list of upgrades was truncated, click "No, Show Changes" to see the full list.',
);
dialogOptions.push(showLogItem);
}
const message = `Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${messageLines.join('\n')}`;
const chosenItem = await vscode.window.showInformationMessage(message, { modal: true }, ...dialogOptions);
const message = `Should the database ${
db.databaseUri.fsPath
} be upgraded?\n\n${messageLines.join("\n")}`;
const chosenItem = await vscode.window.showInformationMessage(
message,
{ modal: true },
...dialogOptions,
);
if (chosenItem === showLogItem) {
logger.outputChannel.show();
}
if (chosenItem !== yesItem) {
throw new UserCancellationException('User cancelled the database upgrade.');
throw new UserCancellationException("User cancelled the database upgrade.");
}
}
/**
* Get the descriptions from a compiled upgrade
*/
function getUpgradeDescriptions(compiled: messages.CompiledUpgrades): messages.UpgradeDescription[] {
function getUpgradeDescriptions(
compiled: messages.CompiledUpgrades,
): messages.UpgradeDescription[] {
// We use the presence of compiledUpgradeFile to check
// if it is multifile or not. We need to explicitly check undefined
// as the types claim the empty string is a valid value
if (compiled.compiledUpgradeFile === undefined) {
return compiled.scripts.map(script => script.description);
return compiled.scripts.map((script) => script.description);
} else {
return compiled.descriptions;
}
@@ -150,50 +179,77 @@ export async function upgradeDatabaseExplicit(
progress: ProgressCallback,
token: vscode.CancellationToken,
): Promise<messages.RunUpgradeResult | undefined> {
const searchPath: string[] = getOnDiskWorkspaceFolders();
if (!dbItem?.contents?.dbSchemeUri) {
throw new Error('Database is invalid, and cannot be upgraded.');
throw new Error("Database is invalid, and cannot be upgraded.");
}
const upgradeInfo = await qs.cliServer.resolveUpgrades(
dbItem.contents.dbSchemeUri.fsPath,
searchPath,
false
false,
);
const { scripts, finalDbscheme } = upgradeInfo;
if (finalDbscheme === undefined) {
throw new Error('Could not determine target dbscheme to upgrade to.');
throw new Error("Could not determine target dbscheme to upgrade to.");
}
const currentUpgradeTmp = await tmp.dir({ dir: tmpDir.name, prefix: 'upgrade_', keep: false, unsafeCleanup: true });
const currentUpgradeTmp = await tmp.dir({
dir: tmpDir.name,
prefix: "upgrade_",
keep: false,
unsafeCleanup: true,
});
try {
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, dbItem, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
}
catch (e) {
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
compileUpgradeResult = await compileDatabaseUpgrade(
qs,
dbItem,
finalDbscheme,
scripts,
currentUpgradeTmp,
progress,
token,
);
} catch (e) {
void showAndLogErrorMessage(
`Compilation of database upgrades failed: ${e}`,
);
return;
}
finally {
void qs.logger.log('Done compiling database upgrade.');
} finally {
void qs.logger.log("Done compiling database upgrade.");
}
if (!compileUpgradeResult.compiledUpgrades) {
const error = compileUpgradeResult.error || '[no error message available]';
void showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
const error =
compileUpgradeResult.error || "[no error message available]";
void showAndLogErrorMessage(
`Compilation of database upgrades failed: ${error}`,
);
return;
}
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, dbItem, qs.cliServer.quiet);
await checkAndConfirmDatabaseUpgrade(
compileUpgradeResult.compiledUpgrades,
dbItem,
qs.cliServer.quiet,
);
try {
void qs.logger.log('Running the following database upgrade:');
void qs.logger.log("Running the following database upgrade:");
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
const result = await runDatabaseUpgrade(qs, dbItem, compileUpgradeResult.compiledUpgrades, progress, token);
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades)
.map((s) => s.description)
.join("\n");
const result = await runDatabaseUpgrade(
qs,
dbItem,
compileUpgradeResult.compiledUpgrades,
progress,
token,
);
// TODO Can remove the next lines when https://github.com/github/codeql-team/issues/1241 is fixed
// restart the query server to avoid a bug in the CLI where the upgrade is applied, but the old dbscheme
@@ -201,12 +257,11 @@ export async function upgradeDatabaseExplicit(
await qs.restartQueryServer(progress, token);
return result;
}
catch (e) {
} catch (e) {
void showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
} finally {
void qs.logger.log('Done running database upgrade.');
void qs.logger.log("Done running database upgrade.");
}
} finally {
await currentUpgradeTmp.cleanup();
@@ -220,19 +275,18 @@ async function runDatabaseUpgrade(
progress: ProgressCallback,
token: vscode.CancellationToken,
): Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
throw new Error("Can't upgrade an invalid database.");
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
workingSet: "default",
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
toRun: upgrades,
};
return qs.sendRequest(messages.runUpgrade, params, token, progress);

View File

@@ -1,6 +1,16 @@
import * as I from 'immutable';
import { EvaluationLogProblemReporter, EvaluationLogScanner, EvaluationLogScannerProvider } from './log-scanner';
import { InLayer, ComputeRecursive, SummaryEvent, PipelineRun, ComputeSimple } from './log-summary';
import * as I from "immutable";
import {
EvaluationLogProblemReporter,
EvaluationLogScanner,
EvaluationLogScannerProvider,
} from "./log-scanner";
import {
InLayer,
ComputeRecursive,
SummaryEvent,
PipelineRun,
ComputeSimple,
} from "./log-summary";
/**
* Like `max`, but returns 0 if no meaningful maximum can be computed.
@@ -17,14 +27,14 @@ function safeMax(it?: Iterable<number>) {
function makeKey(
queryCausingWork: string | undefined,
predicate: string,
suffix = ''
suffix = "",
): string {
if (queryCausingWork === undefined) {
throw new Error(
'queryCausingWork was not defined on an event we expected it to be defined for!'
"queryCausingWork was not defined on an event we expected it to be defined for!",
);
}
return `${queryCausingWork}:${predicate}${suffix ? ' ' + suffix : ''}`;
return `${queryCausingWork}:${predicate}${suffix ? " " + suffix : ""}`;
}
const DEPENDENT_PREDICATES_REGEXP = (() => {
@@ -40,22 +50,22 @@ const DEPENDENT_PREDICATES_REGEXP = (() => {
// INVOKE HIGHER-ORDER RELATION rel ON <id, ..., id>
String.raw`INVOKE\s+HIGHER-ORDER\s+RELATION\s[^\s]+\sON\s+<([0-9a-zA-Z:#_<>]+)((?:,[0-9a-zA-Z:#_<>]+)*)>`,
// SELECT id
String.raw`SELECT\s+([0-9a-zA-Z:#_]+)`
String.raw`SELECT\s+([0-9a-zA-Z:#_]+)`,
];
return new RegExp(
`${String.raw`\{[0-9]+\}\s+[0-9a-zA-Z]+\s=\s(?:` + regexps.join('|')})`
`${String.raw`\{[0-9]+\}\s+[0-9a-zA-Z]+\s=\s(?:` + regexps.join("|")})`,
);
})();
function getDependentPredicates(operations: string[]): I.List<string> {
return I.List(operations).flatMap(operation => {
return I.List(operations).flatMap((operation) => {
const matches = DEPENDENT_PREDICATES_REGEXP.exec(operation.trim());
if (matches !== null) {
return I.List(matches)
.rest() // Skip the first group as it's just the entire string
.filter(x => !!x && !x.match('r[0-9]+|PRIMITIVE')) // Only keep the references to predicates.
.flatMap(x => x.split(',')) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.filter(x => !!x); // Remove empty strings
.filter((x) => !!x && !x.match("r[0-9]+|PRIMITIVE")) // Only keep the references to predicates.
.flatMap((x) => x.split(",")) // Group 2 in the INVOKE HIGHER_ORDER RELATION case is a comma-separated list of identifiers.
.filter((x) => !!x); // Remove empty strings
} else {
return I.List();
}
@@ -64,9 +74,9 @@ function getDependentPredicates(operations: string[]): I.List<string> {
function getMainHash(event: InLayer | ComputeRecursive): string {
switch (event.evaluationStrategy) {
case 'IN_LAYER':
case "IN_LAYER":
return event.mainHash;
case 'COMPUTE_RECURSIVE':
case "COMPUTE_RECURSIVE":
return event.raHash;
}
}
@@ -74,16 +84,20 @@ function getMainHash(event: InLayer | ComputeRecursive): string {
/**
* Sum arrays a and b element-wise. The shorter array is padded with 0s if the arrays are not the same length.
*/
function pointwiseSum(a: Int32Array, b: Int32Array, problemReporter: EvaluationLogProblemReporter): Int32Array {
function pointwiseSum(
a: Int32Array,
b: Int32Array,
problemReporter: EvaluationLogProblemReporter,
): Int32Array {
function reportIfInconsistent(ai: number, bi: number) {
if (ai === -1 && bi !== -1) {
problemReporter.log(
`Operation was not evaluated in the first pipeline, but it was evaluated in the accumulated pipeline (with tuple count ${bi}).`
`Operation was not evaluated in the first pipeline, but it was evaluated in the accumulated pipeline (with tuple count ${bi}).`,
);
}
if (ai !== -1 && bi === -1) {
problemReporter.log(
`Operation was evaluated in the first pipeline (with tuple count ${ai}), but it was not evaluated in the accumulated pipeline.`
`Operation was evaluated in the first pipeline (with tuple count ${ai}), but it was not evaluated in the accumulated pipeline.`,
);
}
}
@@ -115,7 +129,7 @@ function pushValue<K, V>(m: Map<K, V[]>, k: K, v: V) {
function computeJoinOrderBadness(
maxTupleCount: number,
maxDependentPredicateSize: number,
resultSize: number
resultSize: number,
): number {
return maxTupleCount / Math.max(maxDependentPredicateSize, resultSize);
}
@@ -133,7 +147,10 @@ interface Bucket {
class JoinOrderScanner implements EvaluationLogScanner {
// Map a predicate hash to its result size
private readonly predicateSizes = new Map<string, number>();
private readonly layerEvents = new Map<string, (ComputeRecursive | InLayer)[]>();
private readonly layerEvents = new Map<
string,
(ComputeRecursive | InLayer)[]
>();
// Map a key of the form 'query-with-demand : predicate name' to its badness input.
private readonly maxTupleCountMap = new Map<string, number[]>();
private readonly resultSizeMap = new Map<string, number[]>();
@@ -142,13 +159,13 @@ class JoinOrderScanner implements EvaluationLogScanner {
constructor(
private readonly problemReporter: EvaluationLogProblemReporter,
private readonly warningThreshold: number) {
}
private readonly warningThreshold: number,
) {}
public onEvent(event: SummaryEvent): void {
if (
event.completionType !== undefined &&
event.completionType !== 'SUCCESS'
event.completionType !== "SUCCESS"
) {
return; // Skip any evaluation that wasn't successful
}
@@ -163,20 +180,20 @@ class JoinOrderScanner implements EvaluationLogScanner {
private recordPredicateSizes(event: SummaryEvent): void {
switch (event.evaluationStrategy) {
case 'EXTENSIONAL':
case 'COMPUTED_EXTENSIONAL':
case 'COMPUTE_SIMPLE':
case 'CACHACA':
case 'CACHE_HIT': {
case "EXTENSIONAL":
case "COMPUTED_EXTENSIONAL":
case "COMPUTE_SIMPLE":
case "CACHACA":
case "CACHE_HIT": {
this.predicateSizes.set(event.raHash, event.resultSize);
break;
}
case 'SENTINEL_EMPTY': {
case "SENTINEL_EMPTY": {
this.predicateSizes.set(event.raHash, 0);
break;
}
case 'COMPUTE_RECURSIVE':
case 'IN_LAYER': {
case "COMPUTE_RECURSIVE":
case "IN_LAYER": {
this.predicateSizes.set(event.raHash, event.resultSize);
// layerEvents are indexed by the mainHash.
const hash = getMainHash(event);
@@ -189,22 +206,36 @@ class JoinOrderScanner implements EvaluationLogScanner {
}
}
private reportProblemIfNecessary(event: SummaryEvent, iteration: number, metric: number): void {
private reportProblemIfNecessary(
event: SummaryEvent,
iteration: number,
metric: number,
): void {
if (metric >= this.warningThreshold) {
this.problemReporter.reportProblem(event.predicateName, event.raHash, iteration,
`Relation '${event.predicateName}' has an inefficient join order. Its join order metric is ${metric.toFixed(2)}, which is larger than the threshold of ${this.warningThreshold.toFixed(2)}.`);
this.problemReporter.reportProblem(
event.predicateName,
event.raHash,
iteration,
`Relation '${
event.predicateName
}' has an inefficient join order. Its join order metric is ${metric.toFixed(
2,
)}, which is larger than the threshold of ${this.warningThreshold.toFixed(
2,
)}.`,
);
}
}
private computeBadnessMetric(event: SummaryEvent): void {
if (
event.completionType !== undefined &&
event.completionType !== 'SUCCESS'
event.completionType !== "SUCCESS"
) {
return; // Skip any evaluation that wasn't successful
}
switch (event.evaluationStrategy) {
case 'COMPUTE_SIMPLE': {
case "COMPUTE_SIMPLE": {
if (!event.pipelineRuns) {
// skip if the optional pipelineRuns field is not present.
break;
@@ -224,16 +255,20 @@ class JoinOrderScanner implements EvaluationLogScanner {
pushValue(
this.maxDependentPredicateSizeMap,
key,
maxDependentPredicateSize
maxDependentPredicateSize,
);
const metric = computeJoinOrderBadness(
maxTupleCount,
maxDependentPredicateSize,
resultSize!,
);
const metric = computeJoinOrderBadness(maxTupleCount, maxDependentPredicateSize, resultSize!);
this.joinOrderMetricMap.set(key, metric);
this.reportProblemIfNecessary(event, 0, metric);
}
break;
}
case 'COMPUTE_RECURSIVE': {
case "COMPUTE_RECURSIVE": {
// Compute the badness metric for a recursive predicate for each ordering.
const sccMetricInput = this.badnessInputsForRecursiveDelta(event);
// Loop through each predicate in the SCC
@@ -244,12 +279,12 @@ class JoinOrderScanner implements EvaluationLogScanner {
const key = makeKey(
event.queryCausingWork,
predicate,
`(${raReference})`
`(${raReference})`,
);
const maxTupleCount = Math.max(...bucket.tupleCounts);
const resultSize = bucket.resultSize;
const maxDependentPredicateSize = Math.max(
...bucket.dependentPredicateSizes.values()
...bucket.dependentPredicateSizes.values(),
);
if (maxDependentPredicateSize > 0) {
@@ -258,11 +293,15 @@ class JoinOrderScanner implements EvaluationLogScanner {
pushValue(
this.maxDependentPredicateSizeMap,
key,
maxDependentPredicateSize
maxDependentPredicateSize,
);
const metric = computeJoinOrderBadness(
maxTupleCount,
maxDependentPredicateSize,
resultSize,
);
const metric = computeJoinOrderBadness(maxTupleCount, maxDependentPredicateSize, resultSize);
const oldMetric = this.joinOrderMetricMap.get(key);
if ((oldMetric === undefined) || (metric > oldMetric)) {
if (oldMetric === undefined || metric > oldMetric) {
this.joinOrderMetricMap.set(key, metric);
}
}
@@ -281,14 +320,14 @@ class JoinOrderScanner implements EvaluationLogScanner {
func: (
inLayerEvent: ComputeRecursive | InLayer,
run: PipelineRun,
iteration: number
) => void
iteration: number,
) => void,
): void {
const sccEvents = this.layerEvents.get(event.raHash)!;
const nextPipeline: number[] = new Array(sccEvents.length).fill(0);
const maxIteration = Math.max(
...sccEvents.map(e => e.predicateIterationMillis.length)
...sccEvents.map((e) => e.predicateIterationMillis.length),
);
for (let iteration = 0; iteration < maxIteration; ++iteration) {
@@ -313,19 +352,23 @@ class JoinOrderScanner implements EvaluationLogScanner {
*/
private badnessInputsForNonRecursiveDelta(
pipelineRun: PipelineRun,
event: ComputeSimple
event: ComputeSimple,
): { maxTupleCount: number; maxDependentPredicateSize: number } {
const dependentPredicateSizes = Object.values(event.dependencies).map(hash =>
this.predicateSizes.get(hash) ?? 0 // Should always be present, but zero is a safe default.
const dependentPredicateSizes = Object.values(event.dependencies).map(
(hash) => this.predicateSizes.get(hash) ?? 0, // Should always be present, but zero is a safe default.
);
const maxDependentPredicateSize = safeMax(dependentPredicateSizes);
return {
maxTupleCount: safeMax(pipelineRun.counts),
maxDependentPredicateSize: maxDependentPredicateSize
maxDependentPredicateSize: maxDependentPredicateSize,
};
}
private prevDeltaSizes(event: ComputeRecursive, predicate: string, i: number) {
private prevDeltaSizes(
event: ComputeRecursive,
predicate: string,
i: number,
) {
// If an iteration isn't present in the map it means it was skipped because the optimizer
// inferred that it was empty. So its size is 0.
return this.curDeltaSizes(event, predicate, i - 1);
@@ -335,7 +378,9 @@ class JoinOrderScanner implements EvaluationLogScanner {
// If an iteration isn't present in the map it means it was skipped because the optimizer
// inferred that it was empty. So its size is 0.
return (
this.layerEvents.get(event.raHash)?.find(x => x.predicateName === predicate)?.deltaSizes[i] ?? 0
this.layerEvents
.get(event.raHash)
?.find((x) => x.predicateName === predicate)?.deltaSizes[i] ?? 0
);
}
@@ -346,42 +391,42 @@ class JoinOrderScanner implements EvaluationLogScanner {
event: ComputeRecursive,
inLayerEvent: InLayer | ComputeRecursive,
raReference: string,
iteration: number
iteration: number,
) {
const dependentPredicates = getDependentPredicates(
inLayerEvent.ra[raReference]
inLayerEvent.ra[raReference],
);
let dependentPredicateSizes: I.Map<string, number>;
// We treat the base case as a non-recursive pipeline. In that case, the dependent predicates are
// the dependencies of the base case and the cur_deltas.
if (raReference === 'base') {
if (raReference === "base") {
dependentPredicateSizes = I.Map(
dependentPredicates.map((pred): [string, number] => {
// A base case cannot contain a `prev_delta`, but it can contain a `cur_delta`.
let size = 0;
if (pred.endsWith('#cur_delta')) {
if (pred.endsWith("#cur_delta")) {
size = this.curDeltaSizes(
event,
pred.slice(0, -'#cur_delta'.length),
iteration
pred.slice(0, -"#cur_delta".length),
iteration,
);
} else {
const hash = event.dependencies[pred];
size = this.predicateSizes.get(hash)!;
}
return [pred, size];
})
}),
);
} else {
// It's a non-base case in a recursive pipeline. In that case, the dependent predicates are
// only the prev_deltas.
dependentPredicateSizes = I.Map(
dependentPredicates
.flatMap(pred => {
.flatMap((pred) => {
// If it's actually a prev_delta
if (pred.endsWith('#prev_delta')) {
if (pred.endsWith("#prev_delta")) {
// Return the predicate without the #prev_delta suffix.
return [pred.slice(0, -'#prev_delta'.length)];
return [pred.slice(0, -"#prev_delta".length)];
} else {
// Not a recursive delta. Skip it.
return [];
@@ -390,7 +435,7 @@ class JoinOrderScanner implements EvaluationLogScanner {
.map((prev): [string, number] => {
const size = this.prevDeltaSizes(event, prev, iteration);
return [prev, size];
})
}),
);
}
@@ -401,7 +446,9 @@ class JoinOrderScanner implements EvaluationLogScanner {
/**
* Compute the metric input for all the events in a SCC that starts with main node `event`
*/
private badnessInputsForRecursiveDelta(event: ComputeRecursive): Map<string, Map<string, Bucket>> {
private badnessInputsForRecursiveDelta(
event: ComputeRecursive,
): Map<string, Map<string, Bucket>> {
// nameToOrderToBucket : predicate name -> ordering (i.e., standard, order_500000, etc.) -> bucket
const nameToOrderToBucket = new Map<string, Map<string, Bucket>>();
@@ -417,7 +464,7 @@ class JoinOrderScanner implements EvaluationLogScanner {
orderTobucket.set(raReference, {
tupleCounts: new Int32Array(0),
resultSize: 0,
dependentPredicateSizes: I.Map()
dependentPredicateSizes: I.Map(),
});
}
@@ -425,7 +472,7 @@ class JoinOrderScanner implements EvaluationLogScanner {
event,
inLayerEvent,
raReference,
iteration
iteration,
);
const bucket = orderTobucket.get(raReference)!;
@@ -433,18 +480,19 @@ class JoinOrderScanner implements EvaluationLogScanner {
const newTupleCounts = pointwiseSum(
bucket.tupleCounts,
new Int32Array(run.counts),
this.problemReporter
this.problemReporter,
);
const resultSize = bucket.resultSize + deltaSize;
// Pointwise sum the deltas.
const newDependentPredicateSizes = bucket.dependentPredicateSizes.mergeWith(
(oldSize, newSize) => oldSize + newSize,
dependentPredicateSizes
);
const newDependentPredicateSizes =
bucket.dependentPredicateSizes.mergeWith(
(oldSize, newSize) => oldSize + newSize,
dependentPredicateSizes,
);
orderTobucket.set(raReference, {
tupleCounts: newTupleCounts,
resultSize: resultSize,
dependentPredicateSizes: newDependentPredicateSizes
dependentPredicateSizes: newDependentPredicateSizes,
});
});
return nameToOrderToBucket;
@@ -452,10 +500,11 @@ class JoinOrderScanner implements EvaluationLogScanner {
}
export class JoinOrderScannerProvider implements EvaluationLogScannerProvider {
constructor(private readonly getThreshdold: () => number) {
}
constructor(private readonly getThreshdold: () => number) {}
public createScanner(problemReporter: EvaluationLogProblemReporter): EvaluationLogScanner {
public createScanner(
problemReporter: EvaluationLogProblemReporter,
): EvaluationLogScanner {
const threshold = this.getThreshdold();
return new JoinOrderScanner(problemReporter, threshold);
}

View File

@@ -1,4 +1,4 @@
import * as fs from 'fs-extra';
import * as fs from "fs-extra";
/**
* Read a file consisting of multiple JSON objects. Each object is separated from the previous one
@@ -10,8 +10,11 @@ import * as fs from 'fs-extra';
* @param path The path to the file.
* @param handler Callback to be invoked for each top-level JSON object in order.
*/
export async function readJsonlFile(path: string, handler: (value: any) => Promise<void>): Promise<void> {
const logSummary = await fs.readFile(path, 'utf-8');
export async function readJsonlFile(
path: string,
handler: (value: any) => Promise<void>,
): Promise<void> {
const logSummary = await fs.readFile(path, "utf-8");
// Remove newline delimiters because summary is in .jsonl format.
const jsonSummaryObjects: string[] = logSummary.split(/\r?\n\r?\n/g);

View File

@@ -1,11 +1,14 @@
import { Diagnostic, DiagnosticSeverity, languages, Range, Uri } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { QueryHistoryManager } from '../query-history';
import { QueryHistoryInfo } from '../query-history-info';
import { EvaluationLogProblemReporter, EvaluationLogScannerSet } from './log-scanner';
import { PipelineInfo, SummarySymbols } from './summary-parser';
import * as fs from 'fs-extra';
import { logger } from '../logging';
import { Diagnostic, DiagnosticSeverity, languages, Range, Uri } from "vscode";
import { DisposableObject } from "../pure/disposable-object";
import { QueryHistoryManager } from "../query-history";
import { QueryHistoryInfo } from "../query-history-info";
import {
EvaluationLogProblemReporter,
EvaluationLogScannerSet,
} from "./log-scanner";
import { PipelineInfo, SummarySymbols } from "./summary-parser";
import * as fs from "fs-extra";
import { logger } from "../logging";
/**
* Compute the key used to find a predicate in the summary symbols.
@@ -25,10 +28,14 @@ function predicateSymbolKey(name: string, raHash: string): string {
class ProblemReporter implements EvaluationLogProblemReporter {
public readonly diagnostics: Diagnostic[] = [];
constructor(private readonly symbols: SummarySymbols | undefined) {
}
constructor(private readonly symbols: SummarySymbols | undefined) {}
public reportProblem(predicateName: string, raHash: string, iteration: number, message: string): void {
public reportProblem(
predicateName: string,
raHash: string,
iteration: number,
message: string,
): void {
const nameWithHash = predicateSymbolKey(predicateName, raHash);
const predicateSymbol = this.symbols?.predicates[nameWithHash];
let predicateInfo: PipelineInfo | undefined = undefined;
@@ -36,8 +43,15 @@ class ProblemReporter implements EvaluationLogProblemReporter {
predicateInfo = predicateSymbol.iterations[iteration];
}
if (predicateInfo !== undefined) {
const range = new Range(predicateInfo.raStartLine, 0, predicateInfo.raEndLine + 1, 0);
this.diagnostics.push(new Diagnostic(range, message, DiagnosticSeverity.Error));
const range = new Range(
predicateInfo.raStartLine,
0,
predicateInfo.raEndLine + 1,
0,
);
this.diagnostics.push(
new Diagnostic(range, message, DiagnosticSeverity.Error),
);
}
}
@@ -48,24 +62,30 @@ class ProblemReporter implements EvaluationLogProblemReporter {
export class LogScannerService extends DisposableObject {
public readonly scanners = new EvaluationLogScannerSet();
private readonly diagnosticCollection = this.push(languages.createDiagnosticCollection('ql-eval-log'));
private readonly diagnosticCollection = this.push(
languages.createDiagnosticCollection("ql-eval-log"),
);
private currentItem: QueryHistoryInfo | undefined = undefined;
constructor(qhm: QueryHistoryManager) {
super();
this.push(qhm.onDidChangeCurrentQueryItem(async (item) => {
if (item !== this.currentItem) {
this.currentItem = item;
await this.scanEvalLog(item);
}
}));
this.push(
qhm.onDidChangeCurrentQueryItem(async (item) => {
if (item !== this.currentItem) {
this.currentItem = item;
await this.scanEvalLog(item);
}
}),
);
this.push(qhm.onDidCompleteQuery(async (item) => {
if (item === this.currentItem) {
await this.scanEvalLog(item);
}
}));
this.push(
qhm.onDidCompleteQuery(async (item) => {
if (item === this.currentItem) {
await this.scanEvalLog(item);
}
}),
);
}
/**
@@ -73,18 +93,21 @@ export class LogScannerService extends DisposableObject {
*
* @param query The query whose log is to be scanned.
*/
public async scanEvalLog(
query: QueryHistoryInfo | undefined
): Promise<void> {
public async scanEvalLog(query: QueryHistoryInfo | undefined): Promise<void> {
this.diagnosticCollection.clear();
if ((query?.t !== 'local')
|| (query.evalLogSummaryLocation === undefined)
|| (query.jsonEvalLogSummaryLocation === undefined)) {
if (
query?.t !== "local" ||
query.evalLogSummaryLocation === undefined ||
query.jsonEvalLogSummaryLocation === undefined
) {
return;
}
const diagnostics = await this.scanLog(query.jsonEvalLogSummaryLocation, query.evalLogSummarySymbolsLocation);
const diagnostics = await this.scanLog(
query.jsonEvalLogSummaryLocation,
query.evalLogSummarySymbolsLocation,
);
const uri = Uri.file(query.evalLogSummaryLocation);
this.diagnosticCollection.set(uri, diagnostics);
}
@@ -95,10 +118,15 @@ export class LogScannerService extends DisposableObject {
* @param symbolsLocation The file path of the symbols file for the human-readable log summary.
* @returns An array of `Diagnostic`s representing the problems found by scanners.
*/
private async scanLog(jsonSummaryLocation: string, symbolsLocation: string | undefined): Promise<Diagnostic[]> {
private async scanLog(
jsonSummaryLocation: string,
symbolsLocation: string | undefined,
): Promise<Diagnostic[]> {
let symbols: SummarySymbols | undefined = undefined;
if (symbolsLocation !== undefined) {
symbols = JSON.parse(await fs.readFile(symbolsLocation, { encoding: 'utf-8' }));
symbols = JSON.parse(
await fs.readFile(symbolsLocation, { encoding: "utf-8" }),
);
}
const problemReporter = new ProblemReporter(symbols);

View File

@@ -1,5 +1,5 @@
import { SummaryEvent } from './log-summary';
import { readJsonlFile } from './jsonl-reader';
import { SummaryEvent } from "./log-summary";
import { readJsonlFile } from "./jsonl-reader";
/**
* Callback interface used to report diagnostics from a log scanner.
@@ -14,7 +14,12 @@ export interface EvaluationLogProblemReporter {
* must be zero.
* @param message The problem message.
*/
reportProblem(predicateName: string, raHash: string, iteration: number, message: string): void;
reportProblem(
predicateName: string,
raHash: string,
iteration: number,
message: string,
): void;
/**
* Log a message about a problem in the implementation of the scanner. These will typically be
@@ -52,7 +57,9 @@ export interface EvaluationLogScannerProvider {
* Create a new instance of `EvaluationLogScanner` to scan a single summary log.
* @param problemReporter Callback interface for reporting any problems discovered.
*/
createScanner(problemReporter: EvaluationLogProblemReporter): EvaluationLogScanner;
createScanner(
problemReporter: EvaluationLogProblemReporter,
): EvaluationLogScanner;
}
/**
@@ -63,7 +70,10 @@ export interface Disposable {
}
export class EvaluationLogScannerSet {
private readonly scannerProviders = new Map<number, EvaluationLogScannerProvider>();
private readonly scannerProviders = new Map<
number,
EvaluationLogScannerProvider
>();
private nextScannerProviderId = 0;
/**
@@ -72,7 +82,9 @@ export class EvaluationLogScannerSet {
* @param provider The provider.
* @returns A `Disposable` that, when disposed, will unregister the provider.
*/
public registerLogScannerProvider(provider: EvaluationLogScannerProvider): Disposable {
public registerLogScannerProvider(
provider: EvaluationLogScannerProvider,
): Disposable {
const id = this.nextScannerProviderId;
this.nextScannerProviderId++;
@@ -80,7 +92,7 @@ export class EvaluationLogScannerSet {
return {
dispose: () => {
this.scannerProviders.delete(id);
}
},
};
}
@@ -89,15 +101,20 @@ export class EvaluationLogScannerSet {
* @param jsonSummaryLocation The file path of the JSON summary log.
* @param problemReporter Callback interface for reporting any problems discovered.
*/
public async scanLog(jsonSummaryLocation: string, problemReporter: EvaluationLogProblemReporter): Promise<void> {
const scanners = [...this.scannerProviders.values()].map(p => p.createScanner(problemReporter));
public async scanLog(
jsonSummaryLocation: string,
problemReporter: EvaluationLogProblemReporter,
): Promise<void> {
const scanners = [...this.scannerProviders.values()].map((p) =>
p.createScanner(problemReporter),
);
await readJsonlFile(jsonSummaryLocation, async obj => {
scanners.forEach(scanner => {
await readJsonlFile(jsonSummaryLocation, async (obj) => {
scanners.forEach((scanner) => {
scanner.onEvent(obj);
});
});
scanners.forEach(scanner => scanner.onDone());
scanners.forEach((scanner) => scanner.onDone());
}
}

View File

@@ -9,14 +9,14 @@ export interface Ra {
}
export type EvaluationStrategy =
'COMPUTE_SIMPLE' |
'COMPUTE_RECURSIVE' |
'IN_LAYER' |
'COMPUTED_EXTENSIONAL' |
'EXTENSIONAL' |
'SENTINEL_EMPTY' |
'CACHACA' |
'CACHE_HIT';
| "COMPUTE_SIMPLE"
| "COMPUTE_RECURSIVE"
| "IN_LAYER"
| "COMPUTED_EXTENSIONAL"
| "EXTENSIONAL"
| "SENTINEL_EMPTY"
| "CACHACA"
| "CACHE_HIT";
interface SummaryEventBase {
evaluationStrategy: EvaluationStrategy;
@@ -31,7 +31,7 @@ interface ResultEventBase extends SummaryEventBase {
}
export interface ComputeSimple extends ResultEventBase {
evaluationStrategy: 'COMPUTE_SIMPLE';
evaluationStrategy: "COMPUTE_SIMPLE";
ra: Ra;
pipelineRuns?: [PipelineRun];
queryCausingWork?: string;
@@ -39,7 +39,7 @@ export interface ComputeSimple extends ResultEventBase {
}
export interface ComputeRecursive extends ResultEventBase {
evaluationStrategy: 'COMPUTE_RECURSIVE';
evaluationStrategy: "COMPUTE_RECURSIVE";
deltaSizes: number[];
ra: Ra;
pipelineRuns: PipelineRun[];
@@ -49,7 +49,7 @@ export interface ComputeRecursive extends ResultEventBase {
}
export interface InLayer extends ResultEventBase {
evaluationStrategy: 'IN_LAYER';
evaluationStrategy: "IN_LAYER";
deltaSizes: number[];
ra: Ra;
pipelineRuns: PipelineRun[];
@@ -59,26 +59,26 @@ export interface InLayer extends ResultEventBase {
}
export interface ComputedExtensional extends ResultEventBase {
evaluationStrategy: 'COMPUTED_EXTENSIONAL';
evaluationStrategy: "COMPUTED_EXTENSIONAL";
queryCausingWork?: string;
}
export interface NonComputedExtensional extends ResultEventBase {
evaluationStrategy: 'EXTENSIONAL';
evaluationStrategy: "EXTENSIONAL";
queryCausingWork?: string;
}
export interface SentinelEmpty extends SummaryEventBase {
evaluationStrategy: 'SENTINEL_EMPTY';
evaluationStrategy: "SENTINEL_EMPTY";
sentinelRaHash: string;
}
export interface Cachaca extends ResultEventBase {
evaluationStrategy: 'CACHACA';
evaluationStrategy: "CACHACA";
}
export interface CacheHit extends ResultEventBase {
evaluationStrategy: 'CACHE_HIT';
evaluationStrategy: "CACHE_HIT";
}
export type Extensional = ComputedExtensional | NonComputedExtensional;

View File

@@ -1,10 +1,21 @@
import * as fs from 'fs-extra';
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';
import { logger } from '../logging';
import { getErrorMessage } from '../pure/helpers-pure';
import * as fs from "fs-extra";
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";
import { logger } from "../logging";
import { getErrorMessage } from "../pure/helpers-pure";
/** A `Position` within a specified file on disk. */
interface PositionInFile {
@@ -20,7 +31,10 @@ 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);
editor.revealRange(
editor.selection,
TextEditorRevealType.InCenterIfOutsideViewport,
);
}
/**
@@ -44,11 +58,19 @@ export class SummaryLanguageSupport extends DisposableObject {
constructor() {
super();
this.push(window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor));
this.push(window.onDidChangeTextEditorSelection(this.handleDidChangeTextEditorSelection));
this.push(workspace.onDidCloseTextDocument(this.handleDidCloseTextDocument));
this.push(
window.onDidChangeActiveTextEditor(this.handleDidChangeActiveTextEditor),
);
this.push(
window.onDidChangeTextEditorSelection(
this.handleDidChangeTextEditorSelection,
),
);
this.push(
workspace.onDidCloseTextDocument(this.handleDidCloseTextDocument),
);
this.push(commandRunner('codeQL.gotoQL', this.handleGotoQL));
this.push(commandRunner("codeQL.gotoQL", this.handleGotoQL));
}
/**
@@ -62,26 +84,28 @@ export class SummaryLanguageSupport extends DisposableObject {
}
const document = editor.document;
if (document.languageId !== 'ql-summary') {
if (document.languageId !== "ql-summary") {
return undefined;
}
if (document.uri.scheme !== 'file') {
if (document.uri.scheme !== "file") {
return undefined;
}
if (this.lastDocument !== document) {
this.clearCache();
const mapPath = document.uri.fsPath + '.map';
const mapPath = document.uri.fsPath + ".map";
try {
const sourceMapText = await fs.readFile(mapPath, 'utf-8');
const sourceMapText = await fs.readFile(mapPath, "utf-8");
const rawMap: RawSourceMap = JSON.parse(sourceMapText);
this.sourceMap = await new SourceMapConsumer(rawMap);
} catch (e: unknown) {
// Error reading sourcemap. Pretend there was no sourcemap.
void logger.log(`Error reading sourcemap file '${mapPath}': ${getErrorMessage(e)}`);
void logger.log(
`Error reading sourcemap file '${mapPath}': ${getErrorMessage(e)}`,
);
this.sourceMap = undefined;
}
this.lastDocument = document;
@@ -94,19 +118,19 @@ export class SummaryLanguageSupport extends DisposableObject {
const qlPosition = this.sourceMap.originalPositionFor({
line: editor.selection.start.line + 1,
column: editor.selection.start.character,
bias: SourceMapConsumer.GREATEST_LOWER_BOUND
bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
});
if ((qlPosition.source === null) || (qlPosition.line === null)) {
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 :(
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)
position: new Position(line, column),
};
}
@@ -128,22 +152,30 @@ export class SummaryLanguageSupport extends DisposableObject {
private async updateContext(): Promise<void> {
const position = await this.getQLSourceLocation();
await commands.executeCommand('setContext', 'codeql.hasQLSource', position !== undefined);
await commands.executeCommand(
"setContext",
"codeql.hasQLSource",
position !== undefined,
);
}
handleDidChangeActiveTextEditor = async (_editor: TextEditor | undefined): Promise<void> => {
handleDidChangeActiveTextEditor = async (
_editor: TextEditor | undefined,
): Promise<void> => {
await this.updateContext();
}
};
handleDidChangeTextEditorSelection = async (_e: TextEditorSelectionChangeEvent): Promise<void> => {
handleDidChangeTextEditorSelection = async (
_e: TextEditorSelectionChangeEvent,
): Promise<void> => {
await this.updateContext();
}
};
handleDidCloseTextDocument = (document: TextDocument): void => {
if (this.lastDocument === document) {
this.clearCache();
}
}
};
handleGotoQL = async (): Promise<void> => {
const position = await this.getQLSourceLocation();

View File

@@ -1,4 +1,4 @@
import * as fs from 'fs-extra';
import * as fs from "fs-extra";
/**
* Location information for a single pipeline invocation in the RA.
@@ -28,9 +28,11 @@ export interface SummarySymbols {
}
// Tuple counts for Expr::Expr::getParent#dispred#f0820431#ff@76d6745o:
const NON_RECURSIVE_TUPLE_COUNT_REGEXP = /^Evaluated relational algebra for predicate (?<predicateName>\S+) with tuple counts:$/;
const NON_RECURSIVE_TUPLE_COUNT_REGEXP =
/^Evaluated relational algebra for predicate (?<predicateName>\S+) with tuple counts:$/;
// Tuple counts for Expr::Expr::getEnclosingStmt#f0820431#bf@923ddwj9 on iteration 0 running pipeline base:
const RECURSIVE_TUPLE_COUNT_REGEXP = /^Evaluated relational algebra for predicate (?<predicateName>\S+) on iteration (?<iteration>\d+) running pipeline (?<pipeline>\S+) with tuple counts:$/;
const RECURSIVE_TUPLE_COUNT_REGEXP =
/^Evaluated relational algebra for predicate (?<predicateName>\S+) on iteration (?<iteration>\d+) running pipeline (?<pipeline>\S+) with tuple counts:$/;
const RETURN_REGEXP = /^\s*return /;
/**
@@ -44,7 +46,10 @@ const RETURN_REGEXP = /^\s*return /;
* @param summaryPath The path to the summary file.
* @param symbolsPath The path to the symbols file to generate.
*/
export async function generateSummarySymbolsFile(summaryPath: string, symbolsPath: string): Promise<void> {
export async function generateSummarySymbolsFile(
summaryPath: string,
symbolsPath: string,
): Promise<void> {
const symbols = await generateSummarySymbols(summaryPath);
await fs.writeFile(symbolsPath, JSON.stringify(symbols));
}
@@ -56,10 +61,14 @@ export async function generateSummarySymbolsFile(summaryPath: string, symbolsPat
* @param fileLocation The path to the summary file.
* @returns Symbol information for the summary file.
*/
async function generateSummarySymbols(summaryPath: string): Promise<SummarySymbols> {
const summary = await fs.promises.readFile(summaryPath, { encoding: 'utf-8' });
async function generateSummarySymbols(
summaryPath: string,
): Promise<SummarySymbols> {
const summary = await fs.promises.readFile(summaryPath, {
encoding: "utf-8",
});
const symbols: SummarySymbols = {
predicates: {}
predicates: {},
};
const lines = summary.split(/\r?\n/);
@@ -84,7 +93,7 @@ async function generateSummarySymbols(summaryPath: string): Promise<SummarySymbo
if (predicateName !== undefined) {
const raStartLine = lineNumber;
let raEndLine: number | undefined = undefined;
while ((lineNumber < lines.length) && (raEndLine === undefined)) {
while (lineNumber < lines.length && raEndLine === undefined) {
const raLine = lines[lineNumber];
const returnMatch = raLine.match(RETURN_REGEXP);
if (returnMatch) {
@@ -96,14 +105,14 @@ async function generateSummarySymbols(summaryPath: string): Promise<SummarySymbo
let symbol = symbols.predicates[predicateName];
if (symbol === undefined) {
symbol = {
iterations: {}
iterations: {},
};
symbols.predicates[predicateName] = symbol;
}
symbol.iterations[iteration] = {
startLine: lineNumber,
raStartLine: raStartLine,
raEndLine: raEndLine
raEndLine: raEndLine,
};
}
}

View File

@@ -1,7 +1,7 @@
import { window as Window, OutputChannel, Progress } from 'vscode';
import { DisposableObject } from './pure/disposable-object';
import * as fs from 'fs-extra';
import * as path from 'path';
import { window as Window, OutputChannel, Progress } from "vscode";
import { DisposableObject } from "./pure/disposable-object";
import * as fs from "fs-extra";
import * as path from "path";
interface LogOptions {
/** If false, don't output a trailing newline for the log entry. Default true. */
@@ -33,7 +33,10 @@ export type ProgressReporter = Progress<{ message: string }>;
/** A logger that writes messages to an output channel in the Output tab. */
export class OutputChannelLogger extends DisposableObject implements Logger {
public readonly outputChannel: OutputChannel;
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
private readonly additionalLocations = new Map<
string,
AdditionalLogLocation
>();
isCustomLogDirectory: boolean;
constructor(title: string) {
@@ -62,13 +65,15 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
if (options.additionalLogLocation) {
if (!path.isAbsolute(options.additionalLogLocation)) {
throw new Error(`Additional Log Location must be an absolute path: ${options.additionalLogLocation}`);
throw new Error(
`Additional Log Location must be an absolute path: ${options.additionalLogLocation}`,
);
}
const logPath = options.additionalLogLocation;
let additional = this.additionalLocations.get(logPath);
if (!additional) {
const msg = `| Log being saved to ${logPath} |`;
const separator = new Array(msg.length).fill('-').join('');
const separator = new Array(msg.length).fill("-").join("");
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
@@ -79,9 +84,12 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
await additional.log(message, options);
}
} catch (e) {
if (e instanceof Error && e.message === 'Channel has been closed') {
if (e instanceof Error && e.message === "Channel has been closed") {
// Output channel is closed logging to console instead
console.log('Output channel is closed logging to console instead:', message);
console.log(
"Output channel is closed logging to console instead:",
message,
);
} else {
throw e;
}
@@ -110,22 +118,26 @@ class AdditionalLogLocation {
}
await fs.ensureFile(this.location);
await fs.appendFile(this.location, message + (options.trailingNewline ? '\n' : ''), {
encoding: 'utf8'
});
await fs.appendFile(
this.location,
message + (options.trailingNewline ? "\n" : ""),
{
encoding: "utf8",
},
);
}
}
/** The global logger for the extension. */
export const logger = new OutputChannelLogger('CodeQL Extension Log');
export const logger = new OutputChannelLogger("CodeQL Extension Log");
/** The logger for messages from the query server. */
export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
export const queryServerLogger = new OutputChannelLogger("CodeQL Query Server");
/** The logger for messages from the language server. */
export const ideServerLogger = new OutputChannelLogger(
'CodeQL Language Server'
"CodeQL Language Server",
);
/** The logger for messages from tests. */
export const testLogger = new OutputChannelLogger('CodeQL Tests');
export const testLogger = new OutputChannelLogger("CodeQL Tests");

View File

@@ -1,15 +1,18 @@
import { Repository } from '../remote-queries/gh-api/repository';
import { VariantAnalysis, VariantAnalysisRepoTask } from '../remote-queries/gh-api/variant-analysis';
import { Repository } from "../remote-queries/gh-api/repository";
import {
VariantAnalysis,
VariantAnalysisRepoTask,
} from "../remote-queries/gh-api/variant-analysis";
// Types that represent requests/responses from the GitHub API
// that we need to mock.
export enum RequestKind {
GetRepo = 'getRepo',
SubmitVariantAnalysis = 'submitVariantAnalysis',
GetVariantAnalysis = 'getVariantAnalysis',
GetVariantAnalysisRepo = 'getVariantAnalysisRepo',
GetVariantAnalysisRepoResult = 'getVariantAnalysisRepoResult',
GetRepo = "getRepo",
SubmitVariantAnalysis = "submitVariantAnalysis",
GetVariantAnalysis = "getVariantAnalysis",
GetVariantAnalysisRepo = "getVariantAnalysisRepo",
GetVariantAnalysisRepoResult = "getVariantAnalysisRepoResult",
}
export interface BasicErorResponse {
@@ -18,55 +21,55 @@ export interface BasicErorResponse {
export interface GetRepoRequest {
request: {
kind: RequestKind.GetRepo
},
kind: RequestKind.GetRepo;
};
response: {
status: number,
body: Repository | BasicErorResponse | undefined
}
status: number;
body: Repository | BasicErorResponse | undefined;
};
}
export interface SubmitVariantAnalysisRequest {
request: {
kind: RequestKind.SubmitVariantAnalysis
},
kind: RequestKind.SubmitVariantAnalysis;
};
response: {
status: number,
body?: VariantAnalysis | BasicErorResponse
}
status: number;
body?: VariantAnalysis | BasicErorResponse;
};
}
export interface GetVariantAnalysisRequest {
request: {
kind: RequestKind.GetVariantAnalysis
},
kind: RequestKind.GetVariantAnalysis;
};
response: {
status: number,
body?: VariantAnalysis | BasicErorResponse
}
status: number;
body?: VariantAnalysis | BasicErorResponse;
};
}
export interface GetVariantAnalysisRepoRequest {
request: {
kind: RequestKind.GetVariantAnalysisRepo,
repositoryId: number
},
kind: RequestKind.GetVariantAnalysisRepo;
repositoryId: number;
};
response: {
status: number,
body?: VariantAnalysisRepoTask | BasicErorResponse
}
status: number;
body?: VariantAnalysisRepoTask | BasicErorResponse;
};
}
export interface GetVariantAnalysisRepoResultRequest {
request: {
kind: RequestKind.GetVariantAnalysisRepoResult,
repositoryId: number
},
kind: RequestKind.GetVariantAnalysisRepoResult;
repositoryId: number;
};
response: {
status: number,
body?: Buffer | string,
contentType: string,
}
status: number;
body?: Buffer | string;
contentType: string;
};
}
export type GitHubApiRequest =
@@ -77,26 +80,25 @@ export type GitHubApiRequest =
| GetVariantAnalysisRepoResultRequest;
export const isGetRepoRequest = (
request: GitHubApiRequest
): request is GetRepoRequest =>
request.request.kind === RequestKind.GetRepo;
request: GitHubApiRequest,
): request is GetRepoRequest => request.request.kind === RequestKind.GetRepo;
export const isSubmitVariantAnalysisRequest = (
request: GitHubApiRequest
request: GitHubApiRequest,
): request is SubmitVariantAnalysisRequest =>
request.request.kind === RequestKind.SubmitVariantAnalysis;
export const isGetVariantAnalysisRequest = (
request: GitHubApiRequest
request: GitHubApiRequest,
): request is GetVariantAnalysisRequest =>
request.request.kind === RequestKind.GetVariantAnalysis;
export const isGetVariantAnalysisRepoRequest = (
request: GitHubApiRequest
request: GitHubApiRequest,
): request is GetVariantAnalysisRepoRequest =>
request.request.kind === RequestKind.GetVariantAnalysisRepo;
export const isGetVariantAnalysisRepoResultRequest = (
request: GitHubApiRequest
request: GitHubApiRequest,
): request is GetVariantAnalysisRepoResultRequest =>
request.request.kind === RequestKind.GetVariantAnalysisRepoResult;

View File

@@ -1,12 +1,12 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { setupServer, SetupServerApi } from 'msw/node';
import * as path from "path";
import * as fs from "fs-extra";
import { setupServer, SetupServerApi } from "msw/node";
import { DisposableObject } from '../pure/disposable-object';
import { DisposableObject } from "../pure/disposable-object";
import { Recorder } from './recorder';
import { createRequestHandlers } from './request-handlers';
import { getDirectoryNamesInsidePath } from '../pure/files';
import { Recorder } from "./recorder";
import { createRequestHandlers } from "./request-handlers";
import { getDirectoryNamesInsidePath } from "../pure/files";
/**
* Enables mocking of the GitHub API server via HTTP interception, using msw.
@@ -30,7 +30,7 @@ export class MockGitHubApiServer extends DisposableObject {
return;
}
this.server.listen({ onUnhandledRequest: 'bypass' });
this.server.listen({ onUnhandledRequest: "bypass" });
this._isListening = true;
}
@@ -39,7 +39,10 @@ export class MockGitHubApiServer extends DisposableObject {
this._isListening = false;
}
public async loadScenario(scenarioName: string, scenariosPath?: string): Promise<void> {
public async loadScenario(
scenarioName: string,
scenariosPath?: string,
): Promise<void> {
if (!scenariosPath) {
scenariosPath = await this.getDefaultScenariosPath();
if (!scenariosPath) {
@@ -54,11 +57,14 @@ export class MockGitHubApiServer extends DisposableObject {
this.server.use(...handlers);
}
public async saveScenario(scenarioName: string, scenariosPath?: string): Promise<string> {
public async saveScenario(
scenarioName: string,
scenariosPath?: string,
): Promise<string> {
if (!scenariosPath) {
scenariosPath = await this.getDefaultScenariosPath();
if (!scenariosPath) {
throw new Error('Could not find scenarios path');
throw new Error("Could not find scenarios path");
}
}
@@ -123,9 +129,9 @@ export class MockGitHubApiServer extends DisposableObject {
public async getDefaultScenariosPath(): Promise<string | undefined> {
// This should be the directory where package.json is located
const rootDirectory = path.resolve(__dirname, '../..');
const rootDirectory = path.resolve(__dirname, "../..");
const scenariosPath = path.resolve(rootDirectory, 'src/mocks/scenarios');
const scenariosPath = path.resolve(rootDirectory, "src/mocks/scenarios");
if (await fs.pathExists(scenariosPath)) {
return scenariosPath;
}

View File

@@ -1,16 +1,20 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
import { MockedRequest } from 'msw';
import { SetupServerApi } from 'msw/node';
import { IsomorphicResponse } from '@mswjs/interceptors';
import { MockedRequest } from "msw";
import { SetupServerApi } from "msw/node";
import { IsomorphicResponse } from "@mswjs/interceptors";
import { Headers } from 'headers-polyfill';
import fetch from 'node-fetch';
import { Headers } from "headers-polyfill";
import fetch from "node-fetch";
import { DisposableObject } from '../pure/disposable-object';
import { DisposableObject } from "../pure/disposable-object";
import { GetVariantAnalysisRepoResultRequest, GitHubApiRequest, RequestKind } from './gh-api-request';
import {
GetVariantAnalysisRepoResultRequest,
GitHubApiRequest,
RequestKind,
} from "./gh-api-request";
export class Recorder extends DisposableObject {
private readonly allRequests = new Map<string, MockedRequest>();
@@ -18,9 +22,7 @@ export class Recorder extends DisposableObject {
private _isRecording = false;
constructor(
private readonly server: SetupServerApi,
) {
constructor(private readonly server: SetupServerApi) {
super();
this.onRequestStart = this.onRequestStart.bind(this);
this.onResponseBypass = this.onResponseBypass.bind(this);
@@ -43,8 +45,8 @@ export class Recorder extends DisposableObject {
this.clear();
this.server.events.on('request:start', this.onRequestStart);
this.server.events.on('response:bypass', this.onResponseBypass);
this.server.events.on("request:start", this.onRequestStart);
this.server.events.on("response:bypass", this.onResponseBypass);
}
public stop(): void {
@@ -54,8 +56,8 @@ export class Recorder extends DisposableObject {
this._isRecording = false;
this.server.events.removeListener('request:start', this.onRequestStart);
this.server.events.removeListener('response:bypass', this.onResponseBypass);
this.server.events.removeListener("request:start", this.onRequestStart);
this.server.events.removeListener("response:bypass", this.onResponseBypass);
}
public clear() {
@@ -75,11 +77,14 @@ export class Recorder extends DisposableObject {
const filePath = path.join(scenarioDirectory, fileName);
let writtenRequest = {
...request
...request,
};
if (shouldWriteBodyToFile(writtenRequest)) {
const extension = writtenRequest.response.contentType === 'application/zip' ? 'zip' : 'bin';
const extension =
writtenRequest.response.contentType === "application/zip"
? "zip"
: "bin";
const bodyFileName = `${i}-${writtenRequest.request.kind}.body.${extension}`;
const bodyFilePath = path.join(scenarioDirectory, bodyFileName);
@@ -103,14 +108,17 @@ export class Recorder extends DisposableObject {
}
private onRequestStart(request: MockedRequest): void {
if (request.headers.has('x-vscode-codeql-msw-bypass')) {
if (request.headers.has("x-vscode-codeql-msw-bypass")) {
return;
}
this.allRequests.set(request.id, request);
}
private async onResponseBypass(response: IsomorphicResponse, requestId: string): Promise<void> {
private async onResponseBypass(
response: IsomorphicResponse,
requestId: string,
): Promise<void> {
const request = this.allRequests.get(requestId);
this.allRequests.delete(requestId);
if (!request) {
@@ -121,7 +129,12 @@ export class Recorder extends DisposableObject {
return;
}
const gitHubApiRequest = await createGitHubApiRequest(request.url.toString(), response.status, response.body, response.headers);
const gitHubApiRequest = await createGitHubApiRequest(
request.url.toString(),
response.status,
response.body,
response.headers,
);
if (!gitHubApiRequest) {
return;
}
@@ -130,7 +143,12 @@ export class Recorder extends DisposableObject {
}
}
async function createGitHubApiRequest(url: string, status: number, body: string, headers: Headers): Promise<GitHubApiRequest | undefined> {
async function createGitHubApiRequest(
url: string,
status: number,
body: string,
headers: Headers,
): Promise<GitHubApiRequest | undefined> {
if (!url) {
return undefined;
}
@@ -147,7 +165,9 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
};
}
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses$/)) {
if (
url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses$/)
) {
return {
request: {
kind: RequestKind.SubmitVariantAnalysis,
@@ -159,7 +179,11 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
};
}
if (url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+$/)) {
if (
url.match(
/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+$/,
)
) {
return {
request: {
kind: RequestKind.GetVariantAnalysis,
@@ -171,7 +195,9 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
};
}
const repoTaskMatch = url.match(/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+\/repositories\/(?<repositoryId>\d+)$/);
const repoTaskMatch = url.match(
/\/repositories\/\d+\/code-scanning\/codeql\/variant-analyses\/\d+\/repositories\/(?<repositoryId>\d+)$/,
);
if (repoTaskMatch?.groups?.repositoryId) {
return {
request: {
@@ -186,7 +212,9 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
}
// if url is a download URL for a variant analysis result, then it's a get-variant-analysis-repoResult.
const repoDownloadMatch = url.match(/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/);
const repoDownloadMatch = url.match(
/objects-origin\.githubusercontent\.com\/codeql-query-console\/codeql-variant-analysis-repo-tasks\/\d+\/(?<repositoryId>\d+)/,
);
if (repoDownloadMatch?.groups?.repositoryId) {
// msw currently doesn't support binary response bodies, so we need to download this separately
// see https://github.com/mswjs/interceptors/blob/15eafa6215a328219999403e3ff110e71699b016/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts#L24-L33
@@ -194,7 +222,7 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
const response = await fetch(url, {
headers: {
// We need to ensure we don't end up in an infinite loop, since this request will also be intercepted
'x-vscode-codeql-msw-bypass': 'true',
"x-vscode-codeql-msw-bypass": "true",
},
});
const responseBuffer = await response.buffer();
@@ -207,14 +235,16 @@ async function createGitHubApiRequest(url: string, status: number, body: string,
response: {
status,
body: responseBuffer,
contentType: headers.get('content-type') ?? 'application/octet-stream',
}
contentType: headers.get("content-type") ?? "application/octet-stream",
},
};
}
return undefined;
}
function shouldWriteBodyToFile(request: GitHubApiRequest): request is GetVariantAnalysisRepoResultRequest {
function shouldWriteBodyToFile(
request: GitHubApiRequest,
): request is GetVariantAnalysisRepoResultRequest {
return request.response.body instanceof Buffer;
}

View File

@@ -1,20 +1,22 @@
import * as path from 'path';
import * as fs from 'fs-extra';
import { DefaultBodyType, MockedRequest, rest, RestHandler } from 'msw';
import * as path from "path";
import * as fs from "fs-extra";
import { DefaultBodyType, MockedRequest, rest, RestHandler } from "msw";
import {
GitHubApiRequest,
isGetRepoRequest,
isGetVariantAnalysisRepoRequest,
isGetVariantAnalysisRepoResultRequest,
isGetVariantAnalysisRequest,
isSubmitVariantAnalysisRequest
} from './gh-api-request';
isSubmitVariantAnalysisRequest,
} from "./gh-api-request";
const baseUrl = 'https://api.github.com';
const baseUrl = "https://api.github.com";
export type RequestHandler = RestHandler<MockedRequest<DefaultBodyType>>;
export async function createRequestHandlers(scenarioDirPath: string): Promise<RequestHandler[]> {
export async function createRequestHandlers(
scenarioDirPath: string,
): Promise<RequestHandler[]> {
const requests = await readRequestFiles(scenarioDirPath);
const handlers = [
@@ -28,26 +30,35 @@ export async function createRequestHandlers(scenarioDirPath: string): Promise<Re
return handlers;
}
async function readRequestFiles(scenarioDirPath: string): Promise<GitHubApiRequest[]> {
async function readRequestFiles(
scenarioDirPath: string,
): Promise<GitHubApiRequest[]> {
const files = await fs.readdir(scenarioDirPath);
const orderedFiles = files.sort((a, b) => {
const aNum = parseInt(a.split('-')[0]);
const bNum = parseInt(b.split('-')[0]);
const aNum = parseInt(a.split("-")[0]);
const bNum = parseInt(b.split("-")[0]);
return aNum - bNum;
});
const requests: GitHubApiRequest[] = [];
for (const file of orderedFiles) {
if (!file.endsWith('.json')) {
if (!file.endsWith(".json")) {
continue;
}
const filePath = path.join(scenarioDirPath, file);
const request: GitHubApiRequest = await fs.readJson(filePath, { encoding: 'utf8' });
const request: GitHubApiRequest = await fs.readJson(filePath, {
encoding: "utf8",
});
if (typeof request.response.body === 'string' && request.response.body.startsWith('file:')) {
request.response.body = await fs.readFile(path.join(scenarioDirPath, request.response.body.substring(5)));
if (
typeof request.response.body === "string" &&
request.response.body.startsWith("file:")
) {
request.response.body = await fs.readFile(
path.join(scenarioDirPath, request.response.body.substring(5)),
);
}
requests.push(request);
@@ -56,11 +67,13 @@ async function readRequestFiles(scenarioDirPath: string): Promise<GitHubApiReque
return requests;
}
function createGetRepoRequestHandler(requests: GitHubApiRequest[]): RequestHandler {
function createGetRepoRequestHandler(
requests: GitHubApiRequest[],
): RequestHandler {
const getRepoRequests = requests.filter(isGetRepoRequest);
if (getRepoRequests.length > 1) {
throw Error('More than one get repo request found');
throw Error("More than one get repo request found");
}
const getRepoRequest = getRepoRequests[0];
@@ -73,52 +86,72 @@ function createGetRepoRequestHandler(requests: GitHubApiRequest[]): RequestHandl
});
}
function createSubmitVariantAnalysisRequestHandler(requests: GitHubApiRequest[]): RequestHandler {
const submitVariantAnalysisRequests = requests.filter(isSubmitVariantAnalysisRequest);
function createSubmitVariantAnalysisRequestHandler(
requests: GitHubApiRequest[],
): RequestHandler {
const submitVariantAnalysisRequests = requests.filter(
isSubmitVariantAnalysisRequest,
);
if (submitVariantAnalysisRequests.length > 1) {
throw Error('More than one submit variant analysis request found');
throw Error("More than one submit variant analysis request found");
}
const getRepoRequest = submitVariantAnalysisRequests[0];
return rest.post(`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses`, (_req, res, ctx) => {
return res(
ctx.status(getRepoRequest.response.status),
ctx.json(getRepoRequest.response.body),
);
});
return rest.post(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses`,
(_req, res, ctx) => {
return res(
ctx.status(getRepoRequest.response.status),
ctx.json(getRepoRequest.response.body),
);
},
);
}
function createGetVariantAnalysisRequestHandler(requests: GitHubApiRequest[]): RequestHandler {
const getVariantAnalysisRequests = requests.filter(isGetVariantAnalysisRequest);
function createGetVariantAnalysisRequestHandler(
requests: GitHubApiRequest[],
): RequestHandler {
const getVariantAnalysisRequests = requests.filter(
isGetVariantAnalysisRequest,
);
let requestIndex = 0;
// During the lifetime of a variant analysis run, there are multiple requests
// to get the variant analysis. We need to return different responses for each
// request, so keep an index of the request and return the appropriate response.
return rest.get(`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId`, (_req, res, ctx) => {
const request = getVariantAnalysisRequests[requestIndex];
return rest.get(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId`,
(_req, res, ctx) => {
const request = getVariantAnalysisRequests[requestIndex];
if (requestIndex < getVariantAnalysisRequests.length - 1) {
// If there are more requests to come, increment the index.
requestIndex++;
}
if (requestIndex < getVariantAnalysisRequests.length - 1) {
// If there are more requests to come, increment the index.
requestIndex++;
}
return res(
ctx.status(request.response.status),
ctx.json(request.response.body),
);
});
return res(
ctx.status(request.response.status),
ctx.json(request.response.body),
);
},
);
}
function createGetVariantAnalysisRepoRequestHandler(requests: GitHubApiRequest[]): RequestHandler {
const getVariantAnalysisRepoRequests = requests.filter(isGetVariantAnalysisRepoRequest);
function createGetVariantAnalysisRepoRequestHandler(
requests: GitHubApiRequest[],
): RequestHandler {
const getVariantAnalysisRepoRequests = requests.filter(
isGetVariantAnalysisRepoRequest,
);
return rest.get(
`${baseUrl}/repositories/:controllerRepoId/code-scanning/codeql/variant-analyses/:variantAnalysisId/repositories/:repoId`,
(req, res, ctx) => {
const scenarioRequest = getVariantAnalysisRepoRequests.find(r => r.request.repositoryId.toString() === req.params.repoId);
const scenarioRequest = getVariantAnalysisRepoRequests.find(
(r) => r.request.repositoryId.toString() === req.params.repoId,
);
if (!scenarioRequest) {
throw Error(`No scenario request found for ${req.url}`);
}
@@ -127,16 +160,23 @@ function createGetVariantAnalysisRepoRequestHandler(requests: GitHubApiRequest[]
ctx.status(scenarioRequest.response.status),
ctx.json(scenarioRequest.response.body),
);
});
},
);
}
function createGetVariantAnalysisRepoResultRequestHandler(requests: GitHubApiRequest[]): RequestHandler {
const getVariantAnalysisRepoResultRequests = requests.filter(isGetVariantAnalysisRepoResultRequest);
function createGetVariantAnalysisRepoResultRequestHandler(
requests: GitHubApiRequest[],
): RequestHandler {
const getVariantAnalysisRepoResultRequests = requests.filter(
isGetVariantAnalysisRepoResultRequest,
);
return rest.get(
'https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/:variantAnalysisId/:repoId/*',
"https://objects-origin.githubusercontent.com/codeql-query-console/codeql-variant-analysis-repo-tasks/:variantAnalysisId/:repoId/*",
(req, res, ctx) => {
const scenarioRequest = getVariantAnalysisRepoResultRequests.find(r => r.request.repositoryId.toString() === req.params.repoId);
const scenarioRequest = getVariantAnalysisRepoResultRequests.find(
(r) => r.request.repositoryId.toString() === req.params.repoId,
);
if (!scenarioRequest) {
throw Error(`No scenario request found for ${req.url}`);
}
@@ -144,13 +184,12 @@ function createGetVariantAnalysisRepoResultRequestHandler(requests: GitHubApiReq
if (scenarioRequest.response.body) {
return res(
ctx.status(scenarioRequest.response.status),
ctx.set('Content-Type', scenarioRequest.response.contentType),
ctx.set("Content-Type", scenarioRequest.response.contentType),
ctx.body(scenarioRequest.response.body),
);
} else {
return res(
ctx.status(scenarioRequest.response.status),
);
return res(ctx.status(scenarioRequest.response.status));
}
});
},
);
}

View File

@@ -1,9 +1,20 @@
import * as fs from 'fs-extra';
import { commands, env, ExtensionContext, ExtensionMode, QuickPickItem, Uri, window } from 'vscode';
import * as fs from "fs-extra";
import {
commands,
env,
ExtensionContext,
ExtensionMode,
QuickPickItem,
Uri,
window,
} from "vscode";
import { getMockGitHubApiServerScenariosPath, MockGitHubApiConfigListener } from '../config';
import { DisposableObject } from '../pure/disposable-object';
import { MockGitHubApiServer } from './mock-gh-api-server';
import {
getMockGitHubApiServerScenariosPath,
MockGitHubApiConfigListener,
} from "../config";
import { DisposableObject } from "../pure/disposable-object";
import { MockGitHubApiServer } from "./mock-gh-api-server";
/**
* "Interface" to the mock GitHub API server which implements VSCode interactions, such as
@@ -15,9 +26,7 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
private readonly server: MockGitHubApiServer;
private readonly config: MockGitHubApiConfigListener;
constructor(
private readonly ctx: ExtensionContext,
) {
constructor(private readonly ctx: ExtensionContext) {
super();
this.server = new MockGitHubApiServer();
this.config = new MockGitHubApiConfigListener();
@@ -32,8 +41,16 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
public async stopServer(): Promise<void> {
await this.server.stopServer();
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false);
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.scenarioLoaded",
false,
);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.recording",
false,
);
}
public async loadScenario(): Promise<void> {
@@ -43,13 +60,14 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}
const scenarioNames = await this.server.getScenarioNames(scenariosPath);
const scenarioQuickPickItems = scenarioNames.map(s => ({ label: s }));
const scenarioQuickPickItems = scenarioNames.map((s) => ({ label: s }));
const quickPickOptions = {
placeHolder: 'Select a scenario to load',
placeHolder: "Select a scenario to load",
};
const selectedScenario = await window.showQuickPick<QuickPickItem>(
scenarioQuickPickItems,
quickPickOptions);
quickPickOptions,
);
if (!selectedScenario) {
return;
}
@@ -60,38 +78,60 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
// Set a value in the context to track whether we have a scenario loaded.
// This allows us to use this to show/hide commands (see package.json)
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', true);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.scenarioLoaded",
true,
);
await window.showInformationMessage(`Loaded scenario '${scenarioName}'`);
}
public async unloadScenario(): Promise<void> {
if (!this.server.isScenarioLoaded) {
await window.showInformationMessage('No scenario currently loaded');
await window.showInformationMessage("No scenario currently loaded");
} else {
await this.server.unloadScenario();
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false);
await window.showInformationMessage('Unloaded scenario');
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.scenarioLoaded",
false,
);
await window.showInformationMessage("Unloaded scenario");
}
}
public async startRecording(): Promise<void> {
if (this.server.isRecording) {
void window.showErrorMessage('A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.');
void window.showErrorMessage(
'A scenario is already being recorded. Use the "Save Scenario" or "Cancel Scenario" commands to finish recording.',
);
return;
}
if (this.server.isScenarioLoaded) {
await this.server.unloadScenario();
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.scenarioLoaded', false);
void window.showInformationMessage('A scenario was loaded so it has been unloaded');
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.scenarioLoaded",
false,
);
void window.showInformationMessage(
"A scenario was loaded so it has been unloaded",
);
}
await this.server.startRecording();
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', true);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.recording",
true,
);
await window.showInformationMessage('Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.');
await window.showInformationMessage(
'Recording scenario. To save the scenario, use the "CodeQL Mock GitHub API Server: Save Scenario" command.',
);
}
public async saveScenario(): Promise<void> {
@@ -101,14 +141,20 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.recording",
false,
);
if (!this.server.isRecording) {
void window.showErrorMessage('No scenario is currently being recorded.');
void window.showErrorMessage("No scenario is currently being recorded.");
return;
}
if (!this.server.anyRequestsRecorded) {
void window.showWarningMessage('No requests were recorded. Cancelling scenario.');
void window.showWarningMessage(
"No requests were recorded. Cancelling scenario.",
);
await this.stopRecording();
@@ -116,9 +162,9 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}
const name = await window.showInputBox({
title: 'Save scenario',
prompt: 'Enter a name for the scenario.',
placeHolder: 'successful-run',
title: "Save scenario",
prompt: "Enter a name for the scenario.",
placeHolder: "successful-run",
});
if (!name) {
return;
@@ -128,26 +174,33 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
await this.stopRecording();
const action = await window.showInformationMessage(`Scenario saved to ${filePath}`, 'Open directory');
if (action === 'Open directory') {
const action = await window.showInformationMessage(
`Scenario saved to ${filePath}`,
"Open directory",
);
if (action === "Open directory") {
await env.openExternal(Uri.file(filePath));
}
}
public async cancelRecording(): Promise<void> {
if (!this.server.isRecording) {
void window.showErrorMessage('No scenario is currently being recorded.');
void window.showErrorMessage("No scenario is currently being recorded.");
return;
}
await this.stopRecording();
void window.showInformationMessage('Recording cancelled.');
void window.showInformationMessage("Recording cancelled.");
}
private async stopRecording(): Promise<void> {
// Set a value in the context to track whether we are recording. This allows us to use this to show/hide commands (see package.json)
await commands.executeCommand('setContext', 'codeQL.mockGitHubApiServer.recording', false);
await commands.executeCommand(
"setContext",
"codeQL.mockGitHubApiServer.recording",
false,
);
await this.server.stopRecording();
}
@@ -159,7 +212,10 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
}
if (this.ctx.extensionMode === ExtensionMode.Development) {
const developmentScenariosPath = Uri.joinPath(this.ctx.extensionUri, 'src/mocks/scenarios').fsPath.toString();
const developmentScenariosPath = Uri.joinPath(
this.ctx.extensionUri,
"src/mocks/scenarios",
).fsPath.toString();
if (await fs.pathExists(developmentScenariosPath)) {
return developmentScenariosPath;
}
@@ -169,11 +225,11 @@ export class VSCodeMockGitHubApiServer extends DisposableObject {
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select scenarios directory',
title: 'Select scenarios directory',
openLabel: "Select scenarios directory",
title: "Select scenarios directory",
});
if (directories === undefined || directories.length === 0) {
void window.showErrorMessage('No scenarios directory selected.');
void window.showErrorMessage("No scenarios directory selected.");
return undefined;
}

View File

@@ -1,23 +1,23 @@
import { CliVersionConstraint, CodeQLCliServer } from './cli';
import { CliVersionConstraint, CodeQLCliServer } from "./cli";
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
showAndLogInformationMessage,
} from './helpers';
import { QuickPickItem, window } from 'vscode';
import { ProgressCallback, UserCancellationException } from './commandRunner';
import { logger } from './logging';
} from "./helpers";
import { QuickPickItem, window } from "vscode";
import { ProgressCallback, UserCancellationException } from "./commandRunner";
import { logger } from "./logging";
const QUERY_PACKS = [
'codeql/cpp-queries',
'codeql/csharp-queries',
'codeql/go-queries',
'codeql/java-queries',
'codeql/javascript-queries',
'codeql/python-queries',
'codeql/ruby-queries',
'codeql/csharp-solorigate-queries',
'codeql/javascript-experimental-atm-queries',
"codeql/cpp-queries",
"codeql/csharp-queries",
"codeql/go-queries",
"codeql/java-queries",
"codeql/javascript-queries",
"codeql/python-queries",
"codeql/ruby-queries",
"codeql/csharp-solorigate-queries",
"codeql/javascript-experimental-atm-queries",
];
/**
@@ -31,47 +31,48 @@ export async function handleDownloadPacks(
progress: ProgressCallback,
): Promise<void> {
if (!(await cliServer.cliConstraints.supportsPackaging())) {
throw new Error(`Packaging commands are not supported by this version of CodeQL. Please upgrade to v${CliVersionConstraint.CLI_VERSION_WITH_PACKAGING
} or later.`);
throw new Error(
`Packaging commands are not supported by this version of CodeQL. Please upgrade to v${CliVersionConstraint.CLI_VERSION_WITH_PACKAGING} or later.`,
);
}
progress({
message: 'Choose packs to download',
message: "Choose packs to download",
step: 1,
maxStep: 2,
});
let packsToDownload: string[] = [];
const queryPackOption = 'Download all core query packs';
const customPackOption = 'Download custom specified pack';
const queryPackOption = "Download all core query packs";
const customPackOption = "Download custom specified pack";
const quickpick = await window.showQuickPick(
[queryPackOption, customPackOption],
{ ignoreFocusOut: true }
{ ignoreFocusOut: true },
);
if (quickpick === queryPackOption) {
packsToDownload = QUERY_PACKS;
} else if (quickpick === customPackOption) {
const customPack = await window.showInputBox({
prompt:
'Enter the <package-scope/name[@version]> of the pack to download',
"Enter the <package-scope/name[@version]> of the pack to download",
ignoreFocusOut: true,
});
if (customPack) {
packsToDownload.push(customPack);
} else {
throw new UserCancellationException('No pack specified.');
throw new UserCancellationException("No pack specified.");
}
}
if (packsToDownload?.length > 0) {
progress({
message: 'Downloading packs. This may take a few minutes.',
message: "Downloading packs. This may take a few minutes.",
step: 2,
maxStep: 2,
});
try {
await cliServer.packDownload(packsToDownload);
void showAndLogInformationMessage('Finished downloading packs.');
void showAndLogInformationMessage("Finished downloading packs.");
} catch (error) {
void showAndLogErrorMessage(
'Unable to download all packs. See log for more details.'
"Unable to download all packs. See log for more details.",
);
}
}
@@ -92,21 +93,26 @@ export async function handleInstallPackDependencies(
progress: ProgressCallback,
): Promise<void> {
if (!(await cliServer.cliConstraints.supportsPackaging())) {
throw new Error(`Packaging commands are not supported by this version of CodeQL. Please upgrade to v${CliVersionConstraint.CLI_VERSION_WITH_PACKAGING
} or later.`);
throw new Error(
`Packaging commands are not supported by this version of CodeQL. Please upgrade to v${CliVersionConstraint.CLI_VERSION_WITH_PACKAGING} or later.`,
);
}
progress({
message: 'Choose packs to install dependencies for',
message: "Choose packs to install dependencies for",
step: 1,
maxStep: 2,
});
const workspacePacks = await cliServer.resolveQlpacks(getOnDiskWorkspaceFolders());
const quickPickItems = Object.entries(workspacePacks).map<QLPackQuickPickItem>(([key, value]) => ({
const workspacePacks = await cliServer.resolveQlpacks(
getOnDiskWorkspaceFolders(),
);
const quickPickItems = Object.entries(
workspacePacks,
).map<QLPackQuickPickItem>(([key, value]) => ({
label: key,
packRootDir: value,
}));
const packsToInstall = await window.showQuickPick(quickPickItems, {
placeHolder: 'Select packs to install dependencies for',
placeHolder: "Select packs to install dependencies for",
canPickMany: true,
ignoreFocusOut: true,
});
@@ -133,14 +139,18 @@ export async function handleInstallPackDependencies(
}
}
if (failedPacks.length > 0) {
void logger.log(`Errors:\n${errors.join('\n')}`);
void logger.log(`Errors:\n${errors.join("\n")}`);
throw new Error(
`Unable to install pack dependencies for: ${failedPacks.join(', ')}. See log for more details.`
`Unable to install pack dependencies for: ${failedPacks.join(
", ",
)}. See log for more details.`,
);
} else {
void showAndLogInformationMessage('Finished installing pack dependencies.');
void showAndLogInformationMessage(
"Finished installing pack dependencies.",
);
}
} else {
throw new UserCancellationException('No packs selected.');
throw new UserCancellationException("No packs selected.");
}
}

View File

@@ -1,4 +1,3 @@
export const PAGE_SIZE = 1000;
/**
@@ -8,12 +7,12 @@ export const PAGE_SIZE = 1000;
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ColumnKindCode {
export const FLOAT = 'f';
export const INTEGER = 'i';
export const STRING = 's';
export const BOOLEAN = 'b';
export const DATE = 'd';
export const ENTITY = 'e';
export const FLOAT = "f";
export const INTEGER = "i";
export const STRING = "s";
export const BOOLEAN = "b";
export const DATE = "d";
export const ENTITY = "e";
}
export type ColumnKind =
@@ -36,8 +35,11 @@ export interface ResultSetSchema {
pagination?: PaginationInfo;
}
export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo): ResultSetSchema | undefined {
for (const schema of resultSets['result-sets']) {
export function getResultSetSchema(
resultSetName: string,
resultSets: BQRSInfo,
): ResultSetSchema | undefined {
for (const schema of resultSets["result-sets"]) {
if (schema.name === resultSetName) {
return schema;
}
@@ -45,12 +47,12 @@ export function getResultSetSchema(resultSetName: string, resultSets: BQRSInfo):
return undefined;
}
export interface PaginationInfo {
'step-size': number;
"step-size": number;
offsets: number[];
}
export interface BQRSInfo {
'result-sets': ResultSetSchema[];
"result-sets": ResultSetSchema[];
}
export type BqrsId = number;
@@ -95,7 +97,7 @@ export interface RawResultSet {
// boilerplate.
export function transformBqrsResultSet(
schema: ResultSetSchema,
page: DecodedBqrsChunk
page: DecodedBqrsChunk,
): RawResultSet {
return {
schema,
@@ -103,7 +105,14 @@ export function transformBqrsResultSet(
};
}
type BqrsKind = 'String' | 'Float' | 'Integer' | 'String' | 'Boolean' | 'Date' | 'Entity';
type BqrsKind =
| "String"
| "Float"
| "Integer"
| "String"
| "Boolean"
| "Date"
| "Entity";
interface BqrsColumn {
name: string;

View File

@@ -2,9 +2,9 @@ import {
UrlValue,
ResolvableLocationValue,
LineColumnLocation,
WholeFileLocation
} from './bqrs-cli-types';
import { createRemoteFileRef } from './location-link-utils';
WholeFileLocation,
} from "./bqrs-cli-types";
import { createRemoteFileRef } from "./location-link-utils";
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
@@ -20,7 +20,7 @@ const FILE_LOCATION_REGEX = /file:\/\/(.+):([0-9]+):([0-9]+):([0-9]+):([0-9]+)/;
* @param loc The location to test.
*/
export function tryGetResolvableLocation(
loc: UrlValue | undefined
loc: UrlValue | undefined,
): ResolvableLocationValue | undefined {
let resolvedLoc;
if (loc === undefined) {
@@ -37,7 +37,7 @@ export function tryGetResolvableLocation(
}
export function tryGetLocationFromString(
loc: string
loc: string,
): ResolvableLocationValue | undefined {
const matches = FILE_LOCATION_REGEX.exec(loc);
if (matches && matches.length > 1 && matches[1]) {
@@ -61,10 +61,10 @@ export function tryGetLocationFromString(
function isWholeFileMatch(matches: RegExpExecArray): boolean {
return (
matches[2] === '0' &&
matches[3] === '0' &&
matches[4] === '0' &&
matches[5] === '0'
matches[2] === "0" &&
matches[3] === "0" &&
matches[4] === "0" &&
matches[5] === "0"
);
}
@@ -75,24 +75,28 @@ function isWholeFileMatch(matches: RegExpExecArray): boolean {
* @param uri A file uri
*/
export function isEmptyPath(uriStr: string) {
return !uriStr || uriStr === 'file:/';
return !uriStr || uriStr === "file:/";
}
export function isLineColumnLoc(loc: UrlValue): loc is LineColumnLocation {
return typeof loc !== 'string'
&& !isEmptyPath(loc.uri)
&& 'startLine' in loc
&& 'startColumn' in loc
&& 'endLine' in loc
&& 'endColumn' in loc;
return (
typeof loc !== "string" &&
!isEmptyPath(loc.uri) &&
"startLine" in loc &&
"startColumn" in loc &&
"endLine" in loc &&
"endColumn" in loc
);
}
export function isWholeFileLoc(loc: UrlValue): loc is WholeFileLocation {
return typeof loc !== 'string' && !isEmptyPath(loc.uri) && !isLineColumnLoc(loc);
return (
typeof loc !== "string" && !isEmptyPath(loc.uri) && !isLineColumnLoc(loc)
);
}
export function isStringLoc(loc: UrlValue): loc is string {
return typeof loc === 'string';
return typeof loc === "string";
}
export function tryGetRemoteLocation(
@@ -114,17 +118,20 @@ export function tryGetRemoteLocation(
if (!resolvableLocation.uri.startsWith(`file:${sourceLocationPrefix}/`)) {
return undefined;
}
trimmedLocation = resolvableLocation.uri.replace(`file:${sourceLocationPrefix}/`, '');
trimmedLocation = resolvableLocation.uri.replace(
`file:${sourceLocationPrefix}/`,
"",
);
} else {
// If the source location prefix is empty (e.g. for older remote queries), we assume that the database
// was created on a Linux actions runner and has the format:
// "file:/home/runner/work/<repo>/<repo>/relative/path/to/file"
// So we need to drop the first 6 parts of the path.
if (!resolvableLocation.uri.startsWith('file:/home/runner/work/')) {
if (!resolvableLocation.uri.startsWith("file:/home/runner/work/")) {
return undefined;
}
const locationParts = resolvableLocation.uri.split('/');
trimmedLocation = locationParts.slice(6, locationParts.length).join('/');
const locationParts = resolvableLocation.uri.split("/");
trimmedLocation = locationParts.slice(6, locationParts.length).join("/");
}
const fileLink = {
@@ -134,5 +141,6 @@ export function tryGetRemoteLocation(
return createRemoteFileRef(
fileLink,
resolvableLocation.startLine,
resolvableLocation.endLine);
resolvableLocation.endLine,
);
}

View File

@@ -2,19 +2,19 @@
* Contains an assortment of helper constants and functions for working with dates.
*/
const dateWithoutYearFormatter = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
const dateWithoutYearFormatter = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
const dateFormatter = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
export function formatDate(value: Date): string {

View File

@@ -1,4 +1,3 @@
// Avoid explicitly referencing Disposable type in vscode.
// This file cannot have dependencies on the vscode API.
export interface Disposable {

View File

@@ -1,6 +1,5 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as fs from "fs-extra";
import * as path from "path";
/**
* Recursively finds all .ql files in this set of Uris.
@@ -9,7 +8,9 @@ import * as path from 'path';
*
* @returns list of ql files and a boolean describing whether or not a directory was found/
*/
export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean]> {
export async function gatherQlFiles(
paths: string[],
): Promise<[string[], boolean]> {
const gatheredUris: Set<string> = new Set();
let dirFound = false;
for (const nextPath of paths) {
@@ -19,10 +20,10 @@ export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean
) {
dirFound = true;
const subPaths = await fs.readdir(nextPath);
const fullPaths = subPaths.map(p => path.join(nextPath, p));
const fullPaths = subPaths.map((p) => path.join(nextPath, p));
const nestedFiles = (await gatherQlFiles(fullPaths))[0];
nestedFiles.forEach(nested => gatheredUris.add(nested));
} else if (nextPath.endsWith('.ql')) {
nestedFiles.forEach((nested) => gatheredUris.add(nested));
} else if (nextPath.endsWith(".ql")) {
gatheredUris.add(nextPath);
}
}
@@ -34,7 +35,9 @@ export async function gatherQlFiles(paths: string[]): Promise<[string[], boolean
* @param path The path to the directory to read.
* @returns the names of the directories inside the given path.
*/
export async function getDirectoryNamesInsidePath(path: string): Promise<string[]> {
export async function getDirectoryNamesInsidePath(
path: string,
): Promise<string[]> {
if (!(await fs.pathExists(path))) {
throw Error(`Path does not exist: ${path}`);
}
@@ -45,8 +48,8 @@ export async function getDirectoryNamesInsidePath(path: string): Promise<string[
const dirItems = await fs.readdir(path, { withFileTypes: true });
const dirNames = dirItems
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
return dirNames;
}

View File

@@ -1,4 +1,3 @@
/**
* helpers-pure.ts
* ------------
@@ -11,7 +10,7 @@
*/
class ExhaustivityCheckingError extends Error {
constructor(public expectedExhaustiveValue: never) {
super('Internal error: exhaustivity checking failure');
super("Internal error: exhaustivity checking failure");
}
}
@@ -26,7 +25,10 @@ export function assertNever(value: never): never {
/**
* Use to perform array filters where the predicate is asynchronous.
*/
export const asyncFilter = async function <T>(arr: T[], predicate: (arg0: T) => Promise<boolean>) {
export const asyncFilter = async function <T>(
arr: T[],
predicate: (arg0: T) => Promise<boolean>,
) {
const results = await Promise.all(arr.map(predicate));
return arr.filter((_, index) => results[index]);
};
@@ -39,7 +41,7 @@ export const asyncFilter = async function <T>(arr: T[], predicate: (arg0: T) =>
export const REPO_REGEX = /^[a-zA-Z0-9-_\.]+\/[a-zA-Z0-9-_\.]+$/;
/**
* This regex matches GiHub organization and user strings. These are made up for alphanumeric
* This regex matches GiHub organization and user strings. These are made up for alphanumeric
* characters, hyphens, underscores or periods.
*/
export const OWNER_REGEX = /^[a-zA-Z0-9-_\.]+$/;
@@ -49,7 +51,7 @@ export function getErrorMessage(e: any) {
}
export function getErrorStack(e: any) {
return e instanceof Error ? e.stack ?? '' : '';
return e instanceof Error ? e.stack ?? "" : "";
}
export function asError(e: any): Error {

View File

@@ -1,32 +1,43 @@
import * as sarif from 'sarif';
import { AnalysisResults } from '../remote-queries/shared/analysis-result';
import { AnalysisSummary, RemoteQueryResult } from '../remote-queries/shared/remote-query-result';
import { RawResultSet, ResultRow, ResultSetSchema, Column, ResolvableLocationValue } from './bqrs-cli-types';
import * as sarif from "sarif";
import { AnalysisResults } from "../remote-queries/shared/analysis-result";
import {
AnalysisSummary,
RemoteQueryResult,
} from "../remote-queries/shared/remote-query-result";
import {
RawResultSet,
ResultRow,
ResultSetSchema,
Column,
ResolvableLocationValue,
} from "./bqrs-cli-types";
import {
VariantAnalysis,
VariantAnalysisScannedRepositoryResult,
VariantAnalysisScannedRepositoryState,
} from '../remote-queries/shared/variant-analysis';
import { RepositoriesFilterSortStateWithIds } from './variant-analysis-filter-sort';
} from "../remote-queries/shared/variant-analysis";
import { RepositoriesFilterSortStateWithIds } from "./variant-analysis-filter-sort";
/**
* This module contains types and code that are shared between
* the webview and the extension.
*/
export const SELECT_TABLE_NAME = '#select';
export const ALERTS_TABLE_NAME = 'alerts';
export const GRAPH_TABLE_NAME = 'graph';
export const SELECT_TABLE_NAME = "#select";
export const ALERTS_TABLE_NAME = "alerts";
export const GRAPH_TABLE_NAME = "graph";
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type RawTableResultSet = { t: "RawResultSet" } & RawResultSet;
export type InterpretedResultSet<T> = {
t: 'InterpretedResultSet';
t: "InterpretedResultSet";
readonly schema: ResultSetSchema;
name: string;
interpretation: InterpretationT<T>;
};
export type ResultSet = RawTableResultSet | InterpretedResultSet<InterpretationData>;
export type ResultSet =
| RawTableResultSet
| InterpretedResultSet<InterpretationData>;
/**
* Only ever show this many rows in a raw result table.
@@ -55,7 +66,7 @@ export interface PreviousExecution {
}
export type SarifInterpretationData = {
t: 'SarifInterpretationData';
t: "SarifInterpretationData";
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
@@ -64,11 +75,13 @@ export type SarifInterpretationData = {
} & sarif.Log;
export type GraphInterpretationData = {
t: 'GraphInterpretationData';
t: "GraphInterpretationData";
dot: string[];
};
export type InterpretationData = SarifInterpretationData | GraphInterpretationData;
export type InterpretationData =
| SarifInterpretationData
| GraphInterpretationData;
export interface InterpretationT<T> {
sourceLocationPrefix: string;
@@ -97,7 +110,7 @@ export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
* As a result of receiving this message, listeners might want to display a loading indicator.
*/
export interface ResultsUpdatingMsg {
t: 'resultsUpdating';
t: "resultsUpdating";
}
/**
@@ -105,7 +118,7 @@ export interface ResultsUpdatingMsg {
* query.
*/
export interface SetStateMsg {
t: 'setState';
t: "setState";
resultsPath: string;
origResultsPaths: ResultsPaths;
sortedResultsMap: SortedResultsMap;
@@ -134,7 +147,7 @@ export interface SetStateMsg {
* results.
*/
export interface ShowInterpretedPageMsg {
t: 'showInterpretedPage';
t: "showInterpretedPage";
interpretation: Interpretation;
database: DatabaseInfo;
metadata?: QueryMetadata;
@@ -147,15 +160,15 @@ export interface ShowInterpretedPageMsg {
}
export const enum NavigationDirection {
up = 'up',
down = 'down',
left = 'left',
right = 'right',
up = "up",
down = "down",
left = "left",
right = "right",
}
/** Move up, down, left, or right in the result viewer. */
export interface NavigateMsg {
t: 'navigate';
t: "navigate";
direction: NavigationDirection;
}
@@ -164,7 +177,7 @@ export interface NavigateMsg {
* "Show results in Problems view" checkbox.
*/
export interface UntoggleShowProblemsMsg {
t: 'untoggleShowProblems';
t: "untoggleShowProblems";
}
/**
@@ -194,7 +207,7 @@ export type FromResultsViewMsg =
* file at the provided location.
*/
export interface ViewSourceFileMsg {
t: 'viewSourceFile';
t: "viewSourceFile";
loc: ResolvableLocationValue;
databaseUri: string;
}
@@ -203,13 +216,13 @@ export interface ViewSourceFileMsg {
* Message from the results view to open a file in an editor.
*/
export interface OpenFileMsg {
t: 'openFile';
t: "openFile";
/* Full path to the file to open. */
filePath: string;
}
export interface OpenVirtualFileMsg {
t: 'openVirtualFile';
t: "openVirtualFile";
queryText: string;
}
@@ -218,7 +231,7 @@ export interface OpenVirtualFileMsg {
* query diagnostics.
*/
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
t: "toggleDiagnostics";
databaseUri: string;
metadata?: QueryMetadata;
origResultsPaths: ResultsPaths;
@@ -230,7 +243,7 @@ interface ToggleDiagnostics {
* Message from a view signal that loading is complete.
*/
interface ViewLoadedMsg {
t: 'viewLoaded';
t: "viewLoaded";
viewName: string;
}
@@ -239,7 +252,7 @@ interface ViewLoadedMsg {
* page.
*/
interface ChangePage {
t: 'changePage';
t: "changePage";
pageNumber: number; // 0-indexed, displayed to the user as 1-indexed
selectedTable: string;
}
@@ -254,7 +267,7 @@ export interface RawResultsSortState {
sortDirection: SortDirection;
}
export type InterpretedResultsSortColumn = 'alert-message';
export type InterpretedResultsSortColumn = "alert-message";
export interface InterpretedResultsSortState {
sortBy: InterpretedResultsSortColumn;
@@ -265,7 +278,7 @@ export interface InterpretedResultsSortState {
* Message from the results view to request a sorting change.
*/
interface ChangeRawResultsSortMsg {
t: 'changeSort';
t: "changeSort";
resultSetName: string;
/**
* sortState being undefined means don't sort, just present results in the order
@@ -278,7 +291,7 @@ interface ChangeRawResultsSortMsg {
* Message from the results view to request a sorting change in interpreted results.
*/
interface ChangeInterpretedResultsSortMsg {
t: 'changeInterpretedSort';
t: "changeInterpretedSort";
/**
* sortState being undefined means don't sort, just present results in the order
* they appear in the sarif file.
@@ -299,15 +312,15 @@ export type FromCompareViewMessage =
* Message from the compare view to request opening a query.
*/
export interface OpenQueryMessage {
readonly t: 'openQuery';
readonly kind: 'from' | 'to';
readonly t: "openQuery";
readonly kind: "from" | "to";
}
/**
* Message from the compare view to request changing the result set to compare.
*/
interface ChangeCompareMessage {
t: 'changeCompare';
t: "changeCompare";
newResultSetName: string;
}
@@ -317,7 +330,7 @@ export type ToCompareViewMessage = SetComparisonsMessage;
* Message to the compare view that specifies the query results to compare.
*/
export interface SetComparisonsMessage {
readonly t: 'setComparisons';
readonly t: "setComparisons";
readonly stats: {
fromQuery?: {
name: string;
@@ -339,9 +352,9 @@ export interface SetComparisonsMessage {
}
export enum DiffKind {
Add = 'Add',
Remove = 'Remove',
Change = 'Change',
Add = "Add",
Remove = "Remove",
Change = "Change",
}
/**
@@ -371,14 +384,14 @@ export type QueryCompareResult = {
* @param resultSetNames
*/
export function getDefaultResultSetName(
resultSetNames: readonly string[]
resultSetNames: readonly string[],
): string {
// Choose first available result set from the array
return [
ALERTS_TABLE_NAME,
GRAPH_TABLE_NAME,
SELECT_TABLE_NAME,
resultSetNames[0]
resultSetNames[0],
].filter((resultSetName) => resultSetNames.includes(resultSetName))[0];
}
@@ -407,88 +420,88 @@ export type ToRemoteQueriesMessage =
| SetAnalysesResultsMessage;
export interface SetRemoteQueryResultMessage {
t: 'setRemoteQueryResult';
queryResult: RemoteQueryResult
t: "setRemoteQueryResult";
queryResult: RemoteQueryResult;
}
export interface SetAnalysesResultsMessage {
t: 'setAnalysesResults';
t: "setAnalysesResults";
analysesResults: AnalysisResults[];
}
export interface RemoteQueryErrorMessage {
t: 'remoteQueryError';
t: "remoteQueryError";
error: string;
}
export interface RemoteQueryDownloadAnalysisResultsMessage {
t: 'remoteQueryDownloadAnalysisResults';
analysisSummary: AnalysisSummary
t: "remoteQueryDownloadAnalysisResults";
analysisSummary: AnalysisSummary;
}
export interface RemoteQueryDownloadAllAnalysesResultsMessage {
t: 'remoteQueryDownloadAllAnalysesResults';
t: "remoteQueryDownloadAllAnalysesResults";
analysisSummaries: AnalysisSummary[];
}
export interface RemoteQueryExportResultsMessage {
t: 'remoteQueryExportResults';
t: "remoteQueryExportResults";
queryId: string;
}
export interface CopyRepoListMessage {
t: 'copyRepoList';
t: "copyRepoList";
queryId: string;
}
export interface SetVariantAnalysisMessage {
t: 'setVariantAnalysis';
t: "setVariantAnalysis";
variantAnalysis: VariantAnalysis;
}
export type VariantAnalysisState = {
variantAnalysisId: number;
}
};
export interface SetRepoResultsMessage {
t: 'setRepoResults';
t: "setRepoResults";
repoResults: VariantAnalysisScannedRepositoryResult[];
}
export interface SetRepoStatesMessage {
t: 'setRepoStates';
t: "setRepoStates";
repoStates: VariantAnalysisScannedRepositoryState[];
}
export interface RequestRepositoryResultsMessage {
t: 'requestRepositoryResults';
t: "requestRepositoryResults";
repositoryFullName: string;
}
export interface OpenQueryFileMessage {
t: 'openQueryFile';
t: "openQueryFile";
}
export interface OpenQueryTextMessage {
t: 'openQueryText';
t: "openQueryText";
}
export interface CopyRepositoryListMessage {
t: 'copyRepositoryList';
t: "copyRepositoryList";
filterSort?: RepositoriesFilterSortStateWithIds;
}
export interface ExportResultsMessage {
t: 'exportResults';
t: "exportResults";
filterSort?: RepositoriesFilterSortStateWithIds;
}
export interface OpenLogsMessage {
t: 'openLogs';
t: "openLogs";
}
export interface CancelVariantAnalysisMessage {
t: 'cancelVariantAnalysis';
t: "cancelVariantAnalysis";
}
export type ToVariantAnalysisMessage =

View File

@@ -14,8 +14,8 @@
* the fact that any unknown QueryResultType value counts as an error.
*/
import * as rpc from 'vscode-jsonrpc';
import * as shared from './messages-shared';
import * as rpc from "vscode-jsonrpc";
import * as shared from "./messages-shared";
/**
* A query that should be checked for any errors or warnings
@@ -48,8 +48,8 @@ export interface CompileQueryParams {
*/
extraOptions?: ExtraOptions;
/**
* The ql program to check.
*/
* The ql program to check.
*/
queryToCheck: QlProgram;
/**
* The way of compiling a query
@@ -66,8 +66,8 @@ export interface CompileQueryParams {
*/
export interface CompileDilParams {
/**
* The options for compilation, if missing then the default options.
*/
* The options for compilation, if missing then the default options.
*/
compilationOptions?: DilCompilationOptions;
/**
* The options for compilation that do not affect the result.
@@ -83,7 +83,6 @@ export interface CompileDilParams {
resultPath?: string;
}
/**
* The options for QL compilation.
*/
@@ -147,7 +146,6 @@ export interface ExtraOptions {
timeoutSecs: number;
}
/**
* The DIL compilation options
*/
@@ -245,7 +243,6 @@ export interface CheckQueryResult {
resultPatterns: ResultPattern[];
}
/**
* A compilation message (either an error or a warning)
*/
@@ -411,7 +408,6 @@ export interface CheckUpgradeResult {
upgradeError?: string;
}
/**
* The result of compiling an upgrade
*/
@@ -437,7 +433,6 @@ export interface CompileUpgradeSequenceResult {
error?: string;
}
/**
* A description of a upgrade process
*/
@@ -475,16 +470,17 @@ export interface UpgradeDescription {
newSha: string;
}
export type CompiledUpgrades = MultiFileCompiledUpgrades | SingleFileCompiledUpgrades
export type CompiledUpgrades =
| MultiFileCompiledUpgrades
| SingleFileCompiledUpgrades;
/**
* The parts shared by all compiled upgrades
*/
interface CompiledUpgradesBase {
/**
* The initial sha of the dbscheme to upgrade from
*/
* The initial sha of the dbscheme to upgrade from
*/
initialSha: string;
/**
* The path to the new dataset statistics
@@ -496,7 +492,6 @@ interface CompiledUpgradesBase {
targetSha: string;
}
/**
* A compiled upgrade.
* The upgrade is spread among multiple files.
@@ -516,7 +511,6 @@ interface MultiFileCompiledUpgrades extends CompiledUpgradesBase {
compiledUpgradeFile?: never;
}
/**
* A compiled upgrade.
* The upgrade is in a single file.
@@ -636,7 +630,6 @@ export interface TrimCacheParams {
db: Dataset;
}
/**
* A ql dataset
*/
@@ -709,7 +702,7 @@ export interface EvaluateQueriesParams {
useSequenceHint: boolean;
}
export type TemplateDefinitions = { [key: string]: TemplateSource }
export type TemplateDefinitions = { [key: string]: TemplateSource };
export interface MlModel {
/** A URI pointing to the root directory of the model. */
@@ -890,9 +883,9 @@ export namespace QueryResultType {
*/
export const OTHER_ERROR = 1;
/**
* The query failed due to running out of
* memory
*/
* The query failed due to running out of
* memory
*/
export const OOM = 2;
/**
* The query failed due to exceeding the timeout
@@ -977,77 +970,141 @@ export type ProgressMessage = shared.ProgressMessage;
/**
* Check a Ql query for errors without compiling it
*/
export const checkQuery = new rpc.RequestType<WithProgressId<CheckQueryParams>, CheckQueryResult, void, void>('compilation/checkQuery');
export const checkQuery = new rpc.RequestType<
WithProgressId<CheckQueryParams>,
CheckQueryResult,
void,
void
>("compilation/checkQuery");
/**
* Compile a Ql query into a qlo
*/
export const compileQuery = new rpc.RequestType<WithProgressId<CompileQueryParams>, CheckQueryResult, void, void>('compilation/compileQuery');
export const compileQuery = new rpc.RequestType<
WithProgressId<CompileQueryParams>,
CheckQueryResult,
void,
void
>("compilation/compileQuery");
/**
* Compile a dil query into a qlo
*/
export const compileDilQuery = new rpc.RequestType<WithProgressId<CompileDilParams>, CheckQueryResult, void, void>('compilation/compileDilQuery');
export const compileDilQuery = new rpc.RequestType<
WithProgressId<CompileDilParams>,
CheckQueryResult,
void,
void
>("compilation/compileDilQuery");
/**
* Check if there is a valid upgrade path between two dbschemes.
*/
export const checkUpgrade = new rpc.RequestType<WithProgressId<UpgradeParams>, CheckUpgradeResult, void, void>('compilation/checkUpgrade');
export const checkUpgrade = new rpc.RequestType<
WithProgressId<UpgradeParams>,
CheckUpgradeResult,
void,
void
>("compilation/checkUpgrade");
/**
* Compile an upgrade script to upgrade a dataset.
*/
export const compileUpgrade = new rpc.RequestType<WithProgressId<CompileUpgradeParams>, CompileUpgradeResult, void, void>('compilation/compileUpgrade');
export const compileUpgrade = new rpc.RequestType<
WithProgressId<CompileUpgradeParams>,
CompileUpgradeResult,
void,
void
>("compilation/compileUpgrade");
/**
* Compile an upgrade script to upgrade a dataset.
*/
export const compileUpgradeSequence = new rpc.RequestType<WithProgressId<CompileUpgradeSequenceParams>, CompileUpgradeSequenceResult, void, void>('compilation/compileUpgradeSequence');
export const compileUpgradeSequence = new rpc.RequestType<
WithProgressId<CompileUpgradeSequenceParams>,
CompileUpgradeSequenceResult,
void,
void
>("compilation/compileUpgradeSequence");
/**
* Start a new structured log in the evaluator, terminating the previous one if it exists
*/
export const startLog = new rpc.RequestType<WithProgressId<StartLogParams>, StartLogResult, void, void>('evaluation/startLog');
export const startLog = new rpc.RequestType<
WithProgressId<StartLogParams>,
StartLogResult,
void,
void
>("evaluation/startLog");
/**
* Terminate a structured log in the evaluator. Is a no-op if we aren't logging to the given location
*/
export const endLog = new rpc.RequestType<WithProgressId<EndLogParams>, EndLogResult, void, void>('evaluation/endLog');
export const endLog = new rpc.RequestType<
WithProgressId<EndLogParams>,
EndLogResult,
void,
void
>("evaluation/endLog");
/**
* Clear the cache of a dataset
*/
export const clearCache = new rpc.RequestType<WithProgressId<ClearCacheParams>, ClearCacheResult, void, void>('evaluation/clearCache');
export const clearCache = new rpc.RequestType<
WithProgressId<ClearCacheParams>,
ClearCacheResult,
void,
void
>("evaluation/clearCache");
/**
* Trim the cache of a dataset
*/
export const trimCache = new rpc.RequestType<WithProgressId<TrimCacheParams>, ClearCacheResult, void, void>('evaluation/trimCache');
export const trimCache = new rpc.RequestType<
WithProgressId<TrimCacheParams>,
ClearCacheResult,
void,
void
>("evaluation/trimCache");
/**
* Run some queries on a dataset
*/
export const runQueries = new rpc.RequestType<WithProgressId<EvaluateQueriesParams>, EvaluationComplete, void, void>('evaluation/runQueries');
export const runQueries = new rpc.RequestType<
WithProgressId<EvaluateQueriesParams>,
EvaluationComplete,
void,
void
>("evaluation/runQueries");
/**
* Run upgrades on a dataset
*/
export const runUpgrade = new rpc.RequestType<WithProgressId<RunUpgradeParams>, RunUpgradeResult, void, void>('evaluation/runUpgrade');
export const runUpgrade = new rpc.RequestType<
WithProgressId<RunUpgradeParams>,
RunUpgradeResult,
void,
void
>("evaluation/runUpgrade");
export const registerDatabases = new rpc.RequestType<
WithProgressId<RegisterDatabasesParams>,
RegisterDatabasesResult,
void,
void
>('evaluation/registerDatabases');
>("evaluation/registerDatabases");
export const deregisterDatabases = new rpc.RequestType<
WithProgressId<DeregisterDatabasesParams>,
DeregisterDatabasesResult,
void,
void
>('evaluation/deregisterDatabases');
>("evaluation/deregisterDatabases");
/**
* Request returned to the client to notify completion of a query.
* The full runQueries job is completed when all queries are acknowledged.
*/
export const completeQuery = new rpc.RequestType<EvaluationResult, Record<string, any>, void, void>('evaluation/queryCompleted');
export const completeQuery = new rpc.RequestType<
EvaluationResult,
Record<string, any>,
void,
void
>("evaluation/queryCompleted");
export const progress = shared.progress;

View File

@@ -1,9 +1,9 @@
import { FileLink } from '../remote-queries/shared/analysis-result';
import { FileLink } from "../remote-queries/shared/analysis-result";
export function createRemoteFileRef(
fileLink: FileLink,
startLine?: number,
endLine?: number
endLine?: number,
): string {
if (startLine && endLine) {
return `${fileLink.fileLinkPrefix}/${fileLink.filePath}#L${startLine}-L${endLine}`;

View File

@@ -1,4 +1,4 @@
import { readJsonlFile } from '../log-insights/jsonl-reader';
import { readJsonlFile } from "../log-insights/jsonl-reader";
// TODO(angelapwen): Only load in necessary information and
// location in bytes for this log to save memory.
@@ -14,17 +14,19 @@ export interface EvalLogData {
* A pure method that parses a string of evaluator log summaries into
* an array of EvalLogData objects.
*/
export async function parseViewerData(jsonSummaryPath: string): Promise<EvalLogData[]> {
export async function parseViewerData(
jsonSummaryPath: string,
): Promise<EvalLogData[]> {
const viewerData: EvalLogData[] = [];
await readJsonlFile(jsonSummaryPath, async jsonObj => {
await readJsonlFile(jsonSummaryPath, async (jsonObj) => {
// Only convert log items that have an RA and millis field
if (jsonObj.ra !== undefined && jsonObj.millis !== undefined) {
const newLogData: EvalLogData = {
predicateName: jsonObj.predicateName,
millis: jsonObj.millis,
resultSize: jsonObj.resultSize,
ra: jsonObj.ra
ra: jsonObj.ra,
};
viewerData.push(newLogData);
}

View File

@@ -14,7 +14,7 @@
* the fact that any unknown QueryResultType value counts as an error.
*/
import * as rpc from 'vscode-jsonrpc';
import * as rpc from "vscode-jsonrpc";
/**
* A position within a QL file.
@@ -103,8 +103,9 @@ export interface ProgressMessage {
message: string;
}
/**
* A notification that the progress has been changed.
*/
export const progress = new rpc.NotificationType<ProgressMessage, void>('ql/progressUpdated');
export const progress = new rpc.NotificationType<ProgressMessage, void>(
"ql/progressUpdated",
);

View File

@@ -14,10 +14,8 @@
* the fact that any unknown QueryResultType value counts as an error.
*/
import * as rpc from 'vscode-jsonrpc';
import * as shared from './messages-shared';
import * as rpc from "vscode-jsonrpc";
import * as shared from "./messages-shared";
/**
* Parameters to clear the cache
@@ -54,7 +52,6 @@ export interface ClearCacheResult {
deletionMessage: string;
}
export type QueryResultType = number;
/**
* The result of running a query. This namespace is intentionally not
@@ -76,9 +73,9 @@ export namespace QueryResultType {
*/
export const COMPILATION_ERROR = 2;
/**
* The query failed due to running out of
* memory
*/
* The query failed due to running out of
* memory
*/
export const OOM = 3;
/**
* The query failed because it was cancelled.
@@ -94,7 +91,6 @@ export namespace QueryResultType {
export const DBSCHEME_NO_UPGRADE = 6;
}
export interface RegisterDatabasesParams {
databases: string[];
}
@@ -111,40 +107,37 @@ export type DeregisterDatabasesResult = {
registeredDatabases: string[];
};
export interface RunQueryParams {
/**
* The path of the query
*/
queryPath: string,
queryPath: string;
/**
* The output path
*/
outputPath: string,
outputPath: string;
/**
* The database path
*/
db: string,
additionalPacks: string[],
target: CompilationTarget,
externalInputs: Record<string, string>,
singletonExternalInputs: Record<string, string>,
dilPath?: string,
logPath?: string
db: string;
additionalPacks: string[];
target: CompilationTarget;
externalInputs: Record<string, string>;
singletonExternalInputs: Record<string, string>;
dilPath?: string;
logPath?: string;
}
export interface RunQueryResult {
resultType: QueryResultType,
message?: string,
expectedDbschemeName?: string,
resultType: QueryResultType;
message?: string;
expectedDbschemeName?: string;
evaluationTime: number;
}
export interface UpgradeParams {
db: string,
additionalPacks: string[],
db: string;
additionalPacks: string[];
}
export type UpgradeResult = Record<string, unknown>;
@@ -171,43 +164,62 @@ export type ProgressMessage = shared.ProgressMessage;
/**
* Clear the cache of a dataset
*/
export const clearCache = new rpc.RequestType<WithProgressId<ClearCacheParams>, ClearCacheResult, void, void>('evaluation/clearCache');
export const clearCache = new rpc.RequestType<
WithProgressId<ClearCacheParams>,
ClearCacheResult,
void,
void
>("evaluation/clearCache");
/**
* Trim the cache of a dataset
*/
export const trimCache = new rpc.RequestType<WithProgressId<TrimCacheParams>, ClearCacheResult, void, void>('evaluation/trimCache');
export const trimCache = new rpc.RequestType<
WithProgressId<TrimCacheParams>,
ClearCacheResult,
void,
void
>("evaluation/trimCache");
/**
* Clear the pack cache
*/
export const clearPackCache = new rpc.RequestType<WithProgressId<ClearPackCacheParams>, ClearPackCacheResult, void, void>('evaluation/clearPackCache');
export const clearPackCache = new rpc.RequestType<
WithProgressId<ClearPackCacheParams>,
ClearPackCacheResult,
void,
void
>("evaluation/clearPackCache");
/**
* Run a query on a database
*/
export const runQuery = new rpc.RequestType<WithProgressId<RunQueryParams>, RunQueryResult, void, void>('evaluation/runQuery');
export const runQuery = new rpc.RequestType<
WithProgressId<RunQueryParams>,
RunQueryResult,
void,
void
>("evaluation/runQuery");
export const registerDatabases = new rpc.RequestType<
WithProgressId<RegisterDatabasesParams>,
RegisterDatabasesResult,
void,
void
>('evaluation/registerDatabases');
>("evaluation/registerDatabases");
export const deregisterDatabases = new rpc.RequestType<
WithProgressId<DeregisterDatabasesParams>,
DeregisterDatabasesResult,
void,
void
>('evaluation/deregisterDatabases');
>("evaluation/deregisterDatabases");
export const upgradeDatabase = new rpc.RequestType<
WithProgressId<UpgradeParams>,
UpgradeResult,
void,
void
>('evaluation/runUpgrade');
>("evaluation/runUpgrade");
/**
* A notification that the progress has been changed.

View File

@@ -2,7 +2,7 @@
* Contains an assortment of helper constants and functions for working with numbers.
*/
const numberFormatter = new Intl.NumberFormat('en-US');
const numberFormatter = new Intl.NumberFormat("en-US");
/**
* Formats a number to be human-readable with decimal places and thousands separators.

Some files were not shown because too many files have changed in this diff Show More