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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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")];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withTheme } from './withTheme';
|
||||
import { VSCodeTheme } from './theme';
|
||||
import { withTheme } from "./withTheme";
|
||||
import { VSCodeTheme } from "./theme";
|
||||
|
||||
export const decorators = [withTheme];
|
||||
|
||||
|
||||
@@ -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+",
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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/"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
[]);
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
})
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
'%': '%',
|
||||
"%": "%",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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! };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */|
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// Avoid explicitly referencing Disposable type in vscode.
|
||||
// This file cannot have dependencies on the vscode API.
|
||||
export interface Disposable {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user