Merge remote-tracking branch 'origin/main' into koesie10/add-database-source

This commit is contained in:
Koen Vlaswinkel
2023-11-14 10:06:27 +01:00
22 changed files with 154 additions and 62 deletions

View File

@@ -2,6 +2,10 @@
## [UNRELEASED]
- When adding a CodeQL database, we no longer add the database source folder to the workspace by default (since this caused bugs in single-folder workspaces). [#3047](https://github.com/github/vscode-codeql/pull/3047)
- You can manually add individual database source folders to the workspace with the "Add Database Source to Workspace" right-click command in the databases view.
- To restore the old behavior of adding all database source folders by default, set the `codeQL.addingDatabases.addDatabaseSourceToWorkspace` setting to `true`.
- Rename the `codeQL.databaseDownload.allowHttp` setting to `codeQL.addingDatabases.allowHttp`, so that database-related settings are grouped together in the Settings UI. [#3047](https://github.com/github/vscode-codeql/pull/3047)
- The "Sort by Language" action in the databases view now sorts by name within each language. [#3055](https://github.com/github/vscode-codeql/pull/3055)
## 1.9.4 - 6 November 2023

View File

@@ -372,13 +372,23 @@
},
{
"type": "object",
"title": "Downloading databases",
"title": "Adding databases",
"order": 6,
"properties": {
"codeQL.databaseDownload.allowHttp": {
"codeQL.addingDatabases.allowHttp": {
"type": "boolean",
"default": false,
"description": "Allow database to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
"description": "Allow databases to be downloaded via HTTP. Warning: enabling this option will allow downloading from insecure servers."
},
"codeQL.databaseDownload.allowHttp": {
"type": "boolean",
"markdownDeprecationMessage": "**Deprecated**: Please use `#codeQL.addingDatabases.allowHttp#` instead.",
"deprecationMessage": "Deprecated: Please use codeQL.addingDatabases.allowHttp instead."
},
"codeQL.addingDatabases.addDatabaseSourceToWorkspace": {
"type": "boolean",
"default": false,
"markdownDescription": "When adding a CodeQL database, automatically add the database's source folder as a workspace folder. Warning: enabling this option in a single-folder workspace will cause the workspace to reload as a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces). This may cause query history and database lists to be reset."
}
}
},

View File

@@ -43,7 +43,9 @@ export function getFirstWorkspaceFolder() {
const workspaceFolders = getOnDiskWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error("No workspace folders found");
throw new Error(
"No workspace folders found. Please open a folder or workspace in VS Code.",
);
}
const firstFolderFsPath = workspaceFolders[0];

View File

@@ -641,14 +641,23 @@ export function isCodespacesTemplate() {
return !!CODESPACES_TEMPLATE.getValue<boolean>();
}
const DATABASE_DOWNLOAD_SETTING = new Setting("databaseDownload", ROOT_SETTING);
const ADDING_DATABASES_SETTING = new Setting("addingDatabases", ROOT_SETTING);
const ALLOW_HTTP_SETTING = new Setting("allowHttp", DATABASE_DOWNLOAD_SETTING);
const ALLOW_HTTP_SETTING = new Setting("allowHttp", ADDING_DATABASES_SETTING);
export function allowHttp(): boolean {
return ALLOW_HTTP_SETTING.getValue<boolean>() || false;
}
const ADD_DATABASE_SOURCE_TO_WORKSPACE_SETTING = new Setting(
"addDatabaseSourceToWorkspace",
ADDING_DATABASES_SETTING,
);
export function addDatabaseSourceToWorkspace(): boolean {
return ADD_DATABASE_SOURCE_TO_WORKSPACE_SETTING.getValue<boolean>() || false;
}
/**
* Parent setting for all settings related to the "Create Query" command.
*/

View File

@@ -29,7 +29,7 @@ import {
} from "../common/github-url-identifier-helper";
import { Credentials } from "../common/authentication";
import { AppCommandManager } from "../common/commands";
import { allowHttp } from "../config";
import { addDatabaseSourceToWorkspace, allowHttp } from "../config";
import { showAndLogInformationMessage } from "../common/logging";
import { AppOctokit } from "../common/octokit";
import { getLanguageDisplayName } from "../common/query-language";
@@ -104,7 +104,7 @@ export async function promptImportGithubDatabase(
cli?: CodeQLCliServer,
language?: string,
makeSelected = true,
addSourceArchiveFolder = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
): Promise<DatabaseItem | undefined> {
const githubRepo = await askForGitHubRepo(progress);
if (!githubRepo) {
@@ -183,7 +183,7 @@ export async function downloadGitHubDatabase(
cli?: CodeQLCliServer,
language?: string,
makeSelected = true,
addSourceArchiveFolder = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
): Promise<DatabaseItem | undefined> {
const nwo = getNwoFromGitHubUrl(githubRepo) || githubRepo;
if (!isValidGitHubNwo(nwo)) {
@@ -311,7 +311,7 @@ async function databaseArchiveFetcher(
progress: ProgressCallback,
cli?: CodeQLCliServer,
makeSelected = true,
addSourceArchiveFolder = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
): Promise<DatabaseItem> {
progress({
message: "Getting database",
@@ -493,7 +493,7 @@ async function checkForFailingResponse(
return response;
}
// An error downloading the database. Attempt to extract the resaon behind it.
// An error downloading the database. Attempt to extract the reason behind it.
const text = await response.text();
let msg: string;
try {

View File

@@ -7,6 +7,7 @@ import { QueryRunner } from "../../query-server";
import * as cli from "../../codeql-cli/cli";
import { ProgressCallback, withProgress } from "../../common/vscode/progress";
import {
addDatabaseSourceToWorkspace,
getAutogenerateQlPacks,
isCodespacesTemplate,
setAutogenerateQlPacks,
@@ -137,7 +138,7 @@ export class DatabaseManager extends DisposableObject {
displayName?: string,
{
isTutorialDatabase = false,
addSourceArchiveFolder = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
}: OpenDatabaseOptions = {},
): Promise<DatabaseItem> {
const databaseItem = await this.createDatabaseItem(
@@ -164,7 +165,7 @@ export class DatabaseManager extends DisposableObject {
databaseItem: DatabaseItemImpl,
makeSelected: boolean,
isTutorialDatabase?: boolean,
addSourceArchiveFolder = true,
addSourceArchiveFolder = addDatabaseSourceToWorkspace(),
): Promise<DatabaseItem> {
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
if (existingItem !== undefined) {
@@ -274,7 +275,7 @@ export class DatabaseManager extends DisposableObject {
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
);
if (answer === "No") {
if (answer === "No" || answer === undefined) {
return;
}

View File

@@ -126,3 +126,38 @@ export function isModelAccepted(
modeledMethod.provenance !== "ai-generated"
);
}
/**
* Calculates the new provenance for a modeled method based on the current provenance.
* @param modeledMethod The modeled method if there is one.
* @returns The new provenance.
*/
export function calculateNewProvenance(
modeledMethod: ModeledMethod | undefined,
) {
if (!modeledMethod || !modeledMethodSupportsProvenance(modeledMethod)) {
// If nothing has been modeled or the modeled method does not support
// provenance, we assume that the user has entered it manually.
return "manual";
}
switch (modeledMethod.provenance) {
case "df-generated":
// If the method has been generated and there has been a change, we assume
// that the user has manually edited it.
return "df-manual";
case "df-manual":
// If the method has had manual edits, we want the provenance to stay the same.
return "df-manual";
case "ai-generated":
// If the method has been generated and there has been a change, we assume
// that the user has manually edited it.
return "ai-manual";
case "ai-manual":
// If the method has had manual edits, we want the provenance to stay the same.
return "ai-manual";
default:
// The method has been modeled manually.
return "manual";
}
}

View File

@@ -3,7 +3,6 @@ import { styled } from "styled-components";
type Props = {
percent: number;
label?: string;
};
const Circle = styled.div`
@@ -33,10 +32,7 @@ const progressSegments = 44;
// See https://github.com/microsoft/fast/blob/21c210f2164c5cf285cade1a328460c67e4b97e6/packages/web-components/fast-foundation/src/progress-ring/progress-ring.template.ts
// Once the determinate progress ring is available in the VSCode webview UI toolkit, we should use that instead
export const DeterminateProgressRing = ({
percent,
label = "Loading...",
}: Props) => (
export const DeterminateProgressRing = ({ percent }: Props) => (
<Circle
role="progressbar"
aria-valuemin={0}

View File

@@ -226,7 +226,6 @@ export const LibraryRow = ({
<>
<SectionDivider />
<ModeledMethodDataGrid
packageName={title}
methods={methods}
modeledMethodsMap={modeledMethodsMap}
modifiedSignatures={modifiedSignatures}

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import { ChangeEvent, useCallback, useMemo } from "react";
import {
ModeledMethod,
calculateNewProvenance,
isModelAccepted,
modeledMethodSupportsInput,
} from "../../model-editor/modeled-method";
@@ -53,6 +54,7 @@ export const ModelInputDropdown = ({
onChange({
...modeledMethod,
provenance: calculateNewProvenance(modeledMethod),
input: target.value,
});
},

View File

@@ -5,6 +5,7 @@ import {
ModeledMethodKind,
modeledMethodSupportsKind,
isModelAccepted,
calculateNewProvenance,
} from "../../model-editor/modeled-method";
import { getModelsAsDataLanguage } from "../../model-editor/languages";
import { QueryLanguage } from "../../common/query-language";
@@ -52,6 +53,7 @@ export const ModelKindDropdown = ({
onChange({
...modeledMethod,
provenance: calculateNewProvenance(modeledMethod),
kind,
});
},

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import { ChangeEvent, useCallback, useMemo } from "react";
import {
ModeledMethod,
calculateNewProvenance,
isModelAccepted,
modeledMethodSupportsOutput,
} from "../../model-editor/modeled-method";
@@ -54,6 +55,7 @@ export const ModelOutputDropdown = ({
onChange({
...modeledMethod,
provenance: calculateNewProvenance(modeledMethod),
output: target.value,
});
},

View File

@@ -1,11 +1,10 @@
import * as React from "react";
import { ChangeEvent, useCallback } from "react";
import {
calculateNewProvenance,
isModelAccepted,
ModeledMethod,
modeledMethodSupportsProvenance,
ModeledMethodType,
Provenance,
} from "../../model-editor/modeled-method";
import { Method } from "../../model-editor/method";
import { createEmptyModeledMethod } from "../../model-editor/modeled-method-empty";
@@ -43,15 +42,6 @@ export const ModelTypeDropdown = ({
(e: ChangeEvent<HTMLSelectElement>) => {
const modelsAsDataLanguage = getModelsAsDataLanguage(language);
let newProvenance: Provenance = "manual";
if (modeledMethod && modeledMethodSupportsProvenance(modeledMethod)) {
if (modeledMethod.provenance === "df-generated") {
newProvenance = "df-manual";
} else if (modeledMethod.provenance === "ai-generated") {
newProvenance = "ai-manual";
}
}
const emptyModeledMethod = createEmptyModeledMethod(
e.target.value as ModeledMethodType,
method,
@@ -67,7 +57,7 @@ export const ModelTypeDropdown = ({
updatedModeledMethod.output = "ReturnValue";
}
if ("provenance" in updatedModeledMethod) {
updatedModeledMethod.provenance = newProvenance;
updatedModeledMethod.provenance = calculateNewProvenance(modeledMethod);
}
if ("kind" in updatedModeledMethod) {
updatedModeledMethod.kind = "value";

View File

@@ -15,7 +15,6 @@ export const MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS =
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr max-content";
export type ModeledMethodDataGridProps = {
packageName: string;
methods: Method[];
modeledMethodsMap: Record<string, ModeledMethod[]>;
modifiedSignatures: Set<string>;
@@ -27,7 +26,6 @@ export type ModeledMethodDataGridProps = {
};
export const ModeledMethodDataGrid = ({
packageName,
methods,
modeledMethodsMap,
modifiedSignatures,
@@ -81,24 +79,22 @@ export const ModeledMethodDataGrid = ({
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
</DataGridCell>
)}
{methodsWithModelability.map(
({ method, methodCanBeModeled }, index) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
return (
<MethodRow
key={method.signature}
method={method}
methodCanBeModeled={methodCanBeModeled}
modeledMethods={modeledMethods}
methodIsUnsaved={modifiedSignatures.has(method.signature)}
modelingInProgress={inProgressMethods.has(method.signature)}
viewState={viewState}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
);
},
)}
{methodsWithModelability.map(({ method, methodCanBeModeled }) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
return (
<MethodRow
key={method.signature}
method={method}
methodCanBeModeled={methodCanBeModeled}
modeledMethods={modeledMethods}
methodIsUnsaved={modifiedSignatures.has(method.signature)}
modelingInProgress={inProgressMethods.has(method.signature)}
viewState={viewState}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
);
})}
</>
)}
<HiddenMethodsRow

View File

@@ -27,7 +27,7 @@ describe(MethodRow.name, () => {
input: "Argument[0]",
output: "ReturnValue",
kind: "taint",
provenance: "df-generated",
provenance: "manual",
};
const onChange = jest.fn();
@@ -111,6 +111,32 @@ describe(MethodRow.name, () => {
]);
});
it("changes the provenance when the kind is changed", async () => {
const modeledMethodWithGeneratedProvenance: ModeledMethod = {
...modeledMethod,
provenance: "df-generated",
};
render({ modeledMethods: [modeledMethodWithGeneratedProvenance] });
onChange.mockReset();
expect(screen.getByRole("combobox", { name: "Kind" })).toHaveValue("taint");
await userEvent.selectOptions(
screen.getByRole("combobox", { name: "Kind" }),
"value",
);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(method.signature, [
{
...modeledMethod,
kind: "value",
provenance: "df-manual",
},
]);
});
it("has the correct input options", () => {
render();

View File

@@ -45,7 +45,6 @@ describe(ModeledMethodDataGrid.name, () => {
const render = (props: Partial<ModeledMethodDataGridProps> = {}) =>
reactRender(
<ModeledMethodDataGrid
packageName="sql2o"
methods={[method1, method2, method3]}
modeledMethodsMap={{
[method1.signature]: [

View File

@@ -47,9 +47,7 @@ export function Graph({ graphData, databaseUri }: GraphProps) {
d.attributes["xlink:href"] = "#";
d.attributes["href"] = "#";
loc.uri = `file://${loc.uri}`;
select(this).on("click", function (e) {
jumpToLocation(loc, databaseUri);
});
select(this).on("click", () => jumpToLocation(loc, databaseUri));
}
}
if ("fill" in d.attributes) {

View File

@@ -9,7 +9,6 @@ interface Props {
loc: ResolvableLocationValue;
label: string;
databaseUri: string;
title?: string;
onClick?: () => void;
}
@@ -24,7 +23,6 @@ export function ClickableLocation({
loc,
label,
databaseUri,
title,
onClick: onClick,
}: Props): JSX.Element {
const handleClick = useCallback(

View File

@@ -48,7 +48,6 @@ export function Location({
loc={resolvableLoc}
label={displayLabel}
databaseUri={databaseUri}
title={title}
onClick={onClick}
/>
);

View File

@@ -10,6 +10,7 @@
"rootDir": "../..",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,

View File

@@ -1,5 +1,5 @@
[
"v2.15.1",
"v2.15.2",
"v2.14.6",
"v2.13.5",
"v2.12.7",

View File

@@ -625,6 +625,16 @@ describe("local databases", () => {
expect(generateSpy).not.toBeCalled();
});
it("should return early if the user escapes out of the dialog", async () => {
showNeverAskAgainDialogSpy = jest
.spyOn(dialog, "showNeverAskAgainDialog")
.mockResolvedValue(undefined);
await (databaseManager as any).createSkeletonPacks(mockDbItem);
expect(generateSpy).not.toBeCalled();
});
it("should return early and write choice to settings if user wants to never be asked again", async () => {
showNeverAskAgainDialogSpy = jest
.spyOn(dialog, "showNeverAskAgainDialog")
@@ -738,7 +748,20 @@ describe("local databases", () => {
expect(setCurrentDatabaseItemSpy).toBeCalledTimes(1);
});
it("should add database source archive folder", async () => {
it("should not add database source archive folder when `codeQL.addingDatabases.addDatabaseSourceToWorkspace` is `false`", async () => {
jest.spyOn(config, "addDatabaseSourceToWorkspace").mockReturnValue(false);
await databaseManager.openDatabase(
mockDbItem.databaseUri,
mockDbItem.origin,
);
expect(addDatabaseSourceArchiveFolderSpy).toBeCalledTimes(0);
});
it("should add database source archive folder when `codeQL.addingDatabases.addDatabaseSourceToWorkspace` is `true`", async () => {
jest.spyOn(config, "addDatabaseSourceToWorkspace").mockReturnValue(true);
await databaseManager.openDatabase(
mockDbItem.databaseUri,
mockDbItem.origin,