Merge branch 'main' into robertbrignull/automodel-sort-order

This commit is contained in:
Robert
2024-02-21 17:38:50 +00:00
10 changed files with 311 additions and 270 deletions

View File

@@ -5,57 +5,6 @@ import type { AutoModelQueriesResult } from "./auto-model-codeml-queries";
import { assertNever } from "../common/helpers-pure";
import type { Log } from "sarif";
import { gzipEncode } from "../common/zlib";
import type { Method, MethodSignature } from "./method";
import type { ModeledMethod } from "./modeled-method";
import { groupMethods, sortGroupNames, sortMethods } from "./shared/sorting";
/**
* Return the candidates that the model should be run on. This includes limiting the number of
* candidates to the candidate limit and filtering out anything that is already modeled and respecting
* the order in the UI.
* @param mode Whether it is application or framework mode.
* @param methods all methods.
* @param modeledMethodsBySignature the currently modeled methods.
* @returns list of modeled methods that are candidates for modeling.
*/
export function getCandidates(
mode: Mode,
methods: readonly Method[],
modeledMethodsBySignature: Record<string, readonly ModeledMethod[]>,
processedByAutoModelMethods: Set<string>,
): MethodSignature[] {
const candidateMethods = methods.filter((method) => {
// Filter out any methods already processed by auto-model
if (processedByAutoModelMethods.has(method.signature)) {
return false;
}
const modeledMethods: ModeledMethod[] = [
...(modeledMethodsBySignature[method.signature] ?? []),
];
// Anything that is modeled is not a candidate
if (modeledMethods.some((m) => m.type !== "none")) {
return false;
}
// A method that is supported is modeled outside of the model file, so it is not a candidate.
if (method.supported) {
return false;
}
return true;
});
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(candidateMethods, mode);
const sortedGroupNames = sortGroupNames(grouped);
return sortedGroupNames.flatMap((name) =>
// We can safely pass empty sets for `modifiedSignatures` and `processedByAutoModelMethods`
// because we've filtered out all methods that are already modeled or have already been processed by auto-model.
sortMethods(grouped[name], modeledMethodsBySignature, new Set(), new Set()),
);
}
/**
* Encode a SARIF log to the format expected by the server: JSON, GZIP-compressed, base64-encoded

View File

@@ -3,7 +3,8 @@ import type { ModeledMethod } from "./modeled-method";
import { load as loadYaml } from "js-yaml";
import type { ProgressCallback } from "../common/vscode/progress";
import { withProgress } from "../common/vscode/progress";
import { createAutoModelRequest, getCandidates } from "./auto-model";
import { createAutoModelRequest } from "./auto-model";
import { getCandidates } from "./shared/auto-model-candidates";
import { runAutoModelQueries } from "./auto-model-codeml-queries";
import { loadDataExtensionYaml } from "./yaml";
import type { ModelRequest, ModelResponse } from "./auto-model-api";

View File

@@ -0,0 +1,53 @@
import type { Method, MethodSignature } from "../method";
import type { ModeledMethod } from "../modeled-method";
import type { Mode } from "./mode";
import { groupMethods, sortGroupNames, sortMethods } from "./sorting";
/**
* Return the candidates that the model should be run on. This includes limiting the number of
* candidates to the candidate limit and filtering out anything that is already modeled and respecting
* the order in the UI.
* @param mode Whether it is application or framework mode.
* @param methods all methods.
* @param modeledMethodsBySignature the currently modeled methods.
* @returns list of modeled methods that are candidates for modeling.
*/
export function getCandidates(
mode: Mode,
methods: readonly Method[],
modeledMethodsBySignature: Record<string, readonly ModeledMethod[]>,
processedByAutoModelMethods: Set<string>,
): MethodSignature[] {
const candidateMethods = methods.filter((method) => {
// Filter out any methods already processed by auto-model
if (processedByAutoModelMethods.has(method.signature)) {
return false;
}
const modeledMethods: ModeledMethod[] = [
...(modeledMethodsBySignature[method.signature] ?? []),
];
// Anything that is modeled is not a candidate
if (modeledMethods.some((m) => m.type !== "none")) {
return false;
}
// A method that is supported is modeled outside of the model file, so it is not a candidate.
if (method.supported) {
return false;
}
return true;
});
// Sort the same way as the UI so we send the first ones listed in the UI first
const grouped = groupMethods(candidateMethods, mode);
const sortedGroupNames = sortGroupNames(grouped);
return sortedGroupNames.flatMap((name) =>
// We can safely pass empty sets for `modifiedSignatures` and `processedByAutoModelMethods`
// because we've filtered out all methods that are already modeled or have already been processed by auto-model.
sortMethods(grouped[name], modeledMethodsBySignature, new Set(), new Set()),
);
}

View File

@@ -0,0 +1,77 @@
import { join } from "path";
import type { BaseLogger } from "../common/logging";
import type { QueryLanguage } from "../common/query-language";
import type { CodeQLCliServer } from "../codeql-cli/cli";
import type { QlPackDetails } from "./ql-pack-details";
import { getQlPackFilePath } from "../common/ql";
export async function resolveCodeScanningQueryPack(
logger: BaseLogger,
cliServer: CodeQLCliServer,
language: QueryLanguage,
): Promise<QlPackDetails> {
// Get pack
void logger.log(`Downloading pack for language: ${language}`);
const packName = `codeql/${language}-queries`;
const packDownloadResult = await cliServer.packDownload([packName]);
const downloadedPack = packDownloadResult.packs[0];
const packDir = join(
packDownloadResult.packDir,
downloadedPack.name,
downloadedPack.version,
);
// Resolve queries
void logger.log(`Resolving queries for pack: ${packName}`);
const suitePath = join(
packDir,
"codeql-suites",
`${language}-code-scanning.qls`,
);
const resolvedQueries = await cliServer.resolveQueries(suitePath);
const problemQueries = await filterToOnlyProblemQueries(
logger,
cliServer,
resolvedQueries,
);
if (problemQueries.length === 0) {
throw Error(
`No problem queries found in published query pack: ${packName}.`,
);
}
// Return pack details
const qlPackFilePath = await getQlPackFilePath(packDir);
const qlPackDetails: QlPackDetails = {
queryFiles: problemQueries,
qlPackRootPath: packDir,
qlPackFilePath,
language,
};
return qlPackDetails;
}
async function filterToOnlyProblemQueries(
logger: BaseLogger,
cliServer: CodeQLCliServer,
queries: string[],
): Promise<string[]> {
const problemQueries: string[] = [];
for (const query of queries) {
const queryMetadata = await cliServer.resolveMetadata(query);
if (
queryMetadata.kind === "problem" ||
queryMetadata.kind === "path-problem"
) {
problemQueries.push(query);
} else {
void logger.log(`Skipping non-problem query ${query}`);
}
}
return problemQueries;
}

View File

@@ -94,6 +94,7 @@ import { getQlPackFilePath } from "../common/ql";
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
import { findVariantAnalysisQlPackRoot } from "./ql";
import { resolveCodeScanningQueryPack } from "./code-scanning-pack";
const maxRetryCount = 3;
@@ -219,7 +220,7 @@ export class VariantAnalysisManager
public async runVariantAnalysisFromPublishedPack(): Promise<void> {
return withProgress(async (progress, token) => {
progress({
maxStep: 8,
maxStep: 7,
step: 0,
message: "Determining query language",
});
@@ -230,53 +231,17 @@ export class VariantAnalysisManager
}
progress({
maxStep: 8,
step: 1,
message: "Downloading query pack",
});
const packName = `codeql/${language}-queries`;
const packDownloadResult = await this.cliServer.packDownload([packName]);
const downloadedPack = packDownloadResult.packs[0];
const packDir = join(
packDownloadResult.packDir,
downloadedPack.name,
downloadedPack.version,
);
progress({
maxStep: 8,
maxStep: 7,
step: 2,
message: "Resolving queries in pack",
message: "Downloading query pack and resolving queries",
});
const suitePath = join(
packDir,
"codeql-suites",
`${language}-code-scanning.qls`,
);
const resolvedQueries = await this.cliServer.resolveQueries(suitePath);
const problemQueries =
await this.filterToOnlyProblemQueries(resolvedQueries);
if (problemQueries.length === 0) {
void this.app.logger.showErrorMessage(
`Unable to trigger variant analysis. No problem queries found in published query pack: ${packName}.`,
);
return;
}
const qlPackFilePath = await getQlPackFilePath(packDir);
// Build up details to pass to the functions that run the variant analysis.
const qlPackDetails: QlPackDetails = {
queryFiles: problemQueries,
qlPackRootPath: packDir,
qlPackFilePath,
const qlPackDetails = await resolveCodeScanningQueryPack(
this.app.logger,
this.cliServer,
language,
};
);
await this.runVariantAnalysis(
qlPackDetails,
@@ -291,24 +256,6 @@ export class VariantAnalysisManager
});
}
private async filterToOnlyProblemQueries(
queries: string[],
): Promise<string[]> {
const problemQueries: string[] = [];
for (const query of queries) {
const queryMetadata = await this.cliServer.resolveMetadata(query);
if (
queryMetadata.kind === "problem" ||
queryMetadata.kind === "path-problem"
) {
problemQueries.push(query);
} else {
void this.app.logger.log(`Skipping non-problem query ${query}`);
}
}
return problemQueries;
}
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {
if (queryFiles.length === 0) {
throw new Error("Please select a .ql file to run as a variant analysis");

View File

@@ -14,6 +14,7 @@ import {
} from "@vscode/webview-ui-toolkit/react";
import type { ModelEditorViewState } from "../../model-editor/shared/view-state";
import type { AccessPathSuggestionOptions } from "../../model-editor/suggestions";
import { getCandidates } from "../../model-editor/shared/auto-model-candidates";
const LibraryContainer = styled.div`
background-color: var(--vscode-peekViewResult-background);
@@ -186,6 +187,17 @@ export const LibraryRow = ({
return methods.some((method) => inProgressMethods.has(method.signature));
}, [methods, inProgressMethods]);
const modelWithAIDisabled = useMemo(() => {
return (
getCandidates(
viewState.mode,
methods,
modeledMethodsMap,
processedByAutoModelMethods,
).length === 0
);
}, [methods, modeledMethodsMap, processedByAutoModelMethods, viewState.mode]);
return (
<LibraryContainer>
<TitleContainer onClick={toggleExpanded} aria-expanded={isExpanded}>
@@ -205,7 +217,11 @@ export const LibraryRow = ({
{hasUnsavedChanges ? <VSCodeTag>UNSAVED</VSCodeTag> : null}
</NameContainer>
{viewState.showLlmButton && !canStopAutoModeling && (
<VSCodeButton appearance="icon" onClick={handleModelWithAI}>
<VSCodeButton
appearance="icon"
disabled={modelWithAIDisabled}
onClick={handleModelWithAI}
>
<Codicon name="lightbulb-autofix" label="Model with AI" />
&nbsp;Model with AI
</VSCodeButton>

View File

@@ -1,16 +1,12 @@
import {
createAutoModelRequest,
encodeSarif,
getCandidates,
} from "../../../src/model-editor/auto-model";
import { Mode } from "../../../src/model-editor/shared/mode";
import { AutomodelMode } from "../../../src/model-editor/auto-model-api";
import type { AutoModelQueriesResult } from "../../../src/model-editor/auto-model-codeml-queries";
import type { Log } from "sarif";
import { gzipDecode } from "../../../src/common/zlib";
import type { Method } from "../../../src/model-editor/method";
import { EndpointType } from "../../../src/model-editor/method";
import type { ModeledMethod } from "../../../src/model-editor/modeled-method";
describe("createAutoModelRequest", () => {
const createSarifLog = (queryId: string): Log => {
@@ -84,118 +80,3 @@ describe("createAutoModelRequest", () => {
expect(parsed).toEqual(result.candidates);
});
});
describe("getCandidates", () => {
it("doesn't return methods that are already modelled", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
},
];
const modeledMethods: Record<string, ModeledMethod[]> = {
"org.my.A#x()": [
{
type: "neutral",
kind: "sink",
provenance: "manual",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
},
],
};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(0);
});
it("doesn't return methods that are supported from other sources", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: true,
supportedType: "none",
usages: [],
},
];
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(0);
});
it("doesn't return methods that are already processed by auto model", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
},
];
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(["org.my.A#x()"]),
);
expect(candidates.length).toEqual(0);
});
it("returns methods that are neither modeled nor supported from other sources", () => {
const methods: Method[] = [];
methods.push({
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
});
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(1);
});
});

View File

@@ -0,0 +1,120 @@
import type { Method } from "../../../../src/model-editor/method";
import { EndpointType } from "../../../../src/model-editor/method";
import type { ModeledMethod } from "../../../../src/model-editor/modeled-method";
import { getCandidates } from "../../../../src/model-editor/shared/auto-model-candidates";
import { Mode } from "../../../../src/model-editor/shared/mode";
describe("getCandidates", () => {
it("doesn't return methods that are already modelled", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
},
];
const modeledMethods: Record<string, ModeledMethod[]> = {
"org.my.A#x()": [
{
type: "neutral",
kind: "sink",
provenance: "manual",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
},
],
};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(0);
});
it("doesn't return methods that are supported from other sources", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: true,
supportedType: "none",
usages: [],
},
];
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(0);
});
it("doesn't return methods that are already processed by auto model", () => {
const methods: Method[] = [
{
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
},
];
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(["org.my.A#x()"]),
);
expect(candidates.length).toEqual(0);
});
it("returns methods that are neither modeled nor supported from other sources", () => {
const methods: Method[] = [];
methods.push({
library: "my.jar",
signature: "org.my.A#x()",
endpointType: EndpointType.Method,
packageName: "org.my",
typeName: "A",
methodName: "x",
methodParameters: "()",
supported: false,
supportedType: "none",
usages: [],
});
const modeledMethods = {};
const candidates = getCandidates(
Mode.Application,
methods,
modeledMethods,
new Set(),
);
expect(candidates.length).toEqual(1);
});
});

View File

@@ -0,0 +1,34 @@
import type { CodeQLCliServer } from "../../../../src/codeql-cli/cli";
import type { App } from "../../../../src/common/app";
import { QueryLanguage } from "../../../../src/common/query-language";
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
import { resolveCodeScanningQueryPack } from "../../../../src/variant-analysis/code-scanning-pack";
import { getActivatedExtension } from "../../global.helper";
describe("Code Scanning pack", () => {
let cli: CodeQLCliServer;
let app: App;
beforeEach(async () => {
const extension = await getActivatedExtension();
cli = extension.cliServer;
app = new ExtensionApp(extension.ctx);
});
it("should download pack for correct language and identify problem queries", async () => {
const pack = await resolveCodeScanningQueryPack(
app.logger,
cli,
QueryLanguage.Javascript,
);
// Should include queries. Just check that at least one known query exists.
// It doesn't particularly matter which query we check for.
expect(
pack.queryFiles.some((q) => q.includes("PostMessageStar.ql")),
).toBeTruthy();
// Should not include non-problem queries.
expect(
pack.queryFiles.some((q) => q.includes("LinesOfCode.ql")),
).toBeFalsy();
});
});

View File

@@ -477,41 +477,4 @@ describe("Variant Analysis Manager", () => {
}
}
});
describe("runVariantAnalysisFromPublishedPack", () => {
// Temporarily disabling this until we add a way to receive multiple queries in the
// runVariantAnalysis function.
it("should download pack for correct language and identify problem queries", async () => {
const showQuickPickSpy = jest
.spyOn(window, "showQuickPick")
.mockResolvedValue(
mockedQuickPickItem({
label: "JavaScript",
description: "javascript",
language: "javascript",
}),
);
const runVariantAnalysisMock = jest.fn();
variantAnalysisManager.runVariantAnalysis = runVariantAnalysisMock;
await variantAnalysisManager.runVariantAnalysisFromPublishedPack();
expect(showQuickPickSpy).toHaveBeenCalledTimes(1);
expect(runVariantAnalysisMock).toHaveBeenCalledTimes(1);
console.log(runVariantAnalysisMock.mock.calls[0][0]);
const queries: string[] =
runVariantAnalysisMock.mock.calls[0][0].queryFiles;
// Should include queries. Just check that at least one known query exists.
// It doesn't particularly matter which query we check for.
expect(
queries.find((q) => q.includes("PostMessageStar.ql")),
).toBeDefined();
// Should not include non-problem queries.
expect(
queries.find((q) => q.includes("LinesOfCode.ql")),
).not.toBeDefined();
});
});
});