Skip to content

Commit 5c81671

Browse files
committed
Retrieve external API usage snippets using SARIF
1 parent 5a66d6f commit 5c81671

File tree

15 files changed

+333
-58
lines changed

15 files changed

+333
-58
lines changed

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,13 +1050,15 @@ export class CodeQLCliServer implements Disposable {
10501050
resultsPath: string,
10511051
interpretedResultsPath: string,
10521052
sourceInfo?: SourceInfo,
1053+
args?: string[],
10531054
): Promise<sarif.Log> {
10541055
const additionalArgs = [
10551056
// TODO: This flag means that we don't group interpreted results
10561057
// by primary location. We may want to revisit whether we call
10571058
// interpretation with and without this flag, or do some
10581059
// grouping client-side.
10591060
"--no-group-results",
1061+
...(args ?? []),
10601062
];
10611063

10621064
await this.runInterpretCommand(
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { CancellationTokenSource } from "vscode";
2+
import { join } from "path";
3+
import { runQuery } from "./external-api-usage-query";
4+
import { CodeQLCliServer } from "../codeql-cli/cli";
5+
import { QueryRunner } from "../query-server";
6+
import { DatabaseItem } from "../databases/local-databases";
7+
import { interpretResultsSarif } from "../query-results";
8+
import { ProgressCallback } from "../common/vscode/progress";
9+
10+
type Options = {
11+
cliServer: Pick<
12+
CodeQLCliServer,
13+
"resolveDatabase" | "resolveQlpacks" | "interpretBqrsSarif"
14+
>;
15+
queryRunner: Pick<QueryRunner, "createQueryRun" | "logger">;
16+
databaseItem: Pick<
17+
DatabaseItem,
18+
| "contents"
19+
| "databaseUri"
20+
| "language"
21+
| "sourceArchive"
22+
| "getSourceLocationPrefix"
23+
>;
24+
queryStorageDir: string;
25+
26+
progress: ProgressCallback;
27+
};
28+
29+
export async function getAutoModelUsages({
30+
cliServer,
31+
queryRunner,
32+
databaseItem,
33+
queryStorageDir,
34+
progress,
35+
}: Options): Promise<Record<string, string[]>> {
36+
const maxStep = 1500;
37+
38+
const cancellationTokenSource = new CancellationTokenSource();
39+
40+
const queryResult = await runQuery("usagesQuery", {
41+
cliServer,
42+
queryRunner,
43+
queryStorageDir,
44+
databaseItem,
45+
progress: (update) =>
46+
progress({
47+
maxStep,
48+
step: update.step,
49+
message: update.message,
50+
}),
51+
token: cancellationTokenSource.token,
52+
});
53+
if (!queryResult) {
54+
throw new Error("Query failed");
55+
}
56+
57+
progress({
58+
maxStep,
59+
step: 1100,
60+
message: "Retrieving source locatin prefix",
61+
});
62+
63+
const sourceLocationPrefix = await databaseItem.getSourceLocationPrefix(
64+
cliServer,
65+
);
66+
const sourceArchiveUri = databaseItem.sourceArchive;
67+
const sourceInfo =
68+
sourceArchiveUri === undefined
69+
? undefined
70+
: {
71+
sourceArchive: sourceArchiveUri.fsPath,
72+
sourceLocationPrefix,
73+
};
74+
75+
progress({
76+
maxStep,
77+
step: 1200,
78+
message: "Interpreting results",
79+
});
80+
81+
const sarif = await interpretResultsSarif(
82+
cliServer,
83+
{
84+
kind: "problem",
85+
id: "usage",
86+
},
87+
{
88+
resultsPath: queryResult.outputDir.bqrsPath,
89+
interpretedResultsPath: join(
90+
queryStorageDir,
91+
"interpreted-results.sarif",
92+
),
93+
},
94+
sourceInfo,
95+
["--sarif-add-snippets"],
96+
);
97+
98+
progress({
99+
maxStep,
100+
step: 1400,
101+
message: "Parsing results",
102+
});
103+
104+
const snippets: Record<string, string[]> = {};
105+
106+
const results = sarif.runs[0]?.results;
107+
if (!results) {
108+
throw new Error("No results");
109+
}
110+
111+
for (const result of results) {
112+
const signature = result.message.text;
113+
114+
const snippet =
115+
result.locations?.[0]?.physicalLocation?.contextRegion?.snippet?.text;
116+
117+
if (!signature || !snippet) {
118+
continue;
119+
}
120+
121+
if (!(signature in snippets)) {
122+
snippets[signature] = [];
123+
}
124+
125+
snippets[signature].push(snippet);
126+
}
127+
128+
return snippets;
129+
}

extensions/ql-vscode/src/data-extensions-editor/auto-model.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function createAutoModelRequest(
1111
language: string,
1212
externalApiUsages: ExternalApiUsage[],
1313
modeledMethods: Record<string, ModeledMethod>,
14+
usages: Record<string, string[]>,
1415
): ModelRequest {
1516
const request: ModelRequest = {
1617
language,
@@ -29,6 +30,10 @@ export function createAutoModelRequest(
2930
type: "none",
3031
};
3132

33+
const usagesForMethod =
34+
usages[externalApiUsage.signature] ??
35+
externalApiUsage.usages.map((usage) => usage.label);
36+
3237
const numberOfArguments =
3338
externalApiUsage.methodParameters === "()"
3439
? 0
@@ -48,9 +53,7 @@ export function createAutoModelRequest(
4853
modeledMethod.type === "none"
4954
? undefined
5055
: toMethodClassification(modeledMethod),
51-
usages: externalApiUsage.usages
52-
.slice(0, 10)
53-
.map((usage) => usage.label),
56+
usages: usagesForMethod.slice(0, 10),
5457
input: `Argument[${argumentIndex}]`,
5558
};
5659

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
parsePredictedClassifications,
4646
} from "./auto-model";
4747
import { showLlmGeneration } from "../config";
48+
import { getAutoModelUsages } from "./auto-model-usages-query";
4849

4950
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
5051
const workspaceFolder = workspace.workspaceFolders?.find(
@@ -242,7 +243,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
242243
const cancellationTokenSource = new CancellationTokenSource();
243244

244245
try {
245-
const queryResult = await runQuery({
246+
const queryResult = await runQuery("mainQuery", {
246247
cliServer: this.cliServer,
247248
queryRunner: this.queryRunner,
248249
databaseItem: this.databaseItem,
@@ -385,23 +386,66 @@ export class DataExtensionsEditorView extends AbstractWebview<
385386
externalApiUsages: ExternalApiUsage[],
386387
modeledMethods: Record<string, ModeledMethod>,
387388
): Promise<void> {
389+
const maxStep = 3000;
390+
391+
await this.showProgress({
392+
step: 0,
393+
maxStep,
394+
message: "Retrieving usages",
395+
});
396+
397+
const usages = await getAutoModelUsages({
398+
cliServer: this.cliServer,
399+
queryRunner: this.queryRunner,
400+
queryStorageDir: this.queryStorageDir,
401+
databaseItem: this.databaseItem,
402+
progress: (update) => this.showProgress(update, maxStep),
403+
});
404+
405+
await this.showProgress({
406+
step: 1800,
407+
maxStep,
408+
message: "Creating request",
409+
});
410+
388411
const request = createAutoModelRequest(
389412
this.databaseItem.language,
390413
externalApiUsages,
391414
modeledMethods,
415+
usages,
392416
);
393417

418+
await this.showProgress({
419+
step: 2000,
420+
maxStep,
421+
message: "Sending request",
422+
});
423+
394424
const response = await autoModel(this.app.credentials, request);
395425

426+
await this.showProgress({
427+
step: 2500,
428+
maxStep,
429+
message: "Parsing response",
430+
});
431+
396432
const predictedModeledMethods = parsePredictedClassifications(
397433
response.predicted,
398434
);
399435

436+
await this.showProgress({
437+
step: 2800,
438+
maxStep,
439+
message: "Applying results",
440+
});
441+
400442
await this.postMessage({
401443
t: "addModeledMethods",
402444
modeledMethods: predictedModeledMethods,
403445
overrideNone: true,
404446
});
447+
448+
await this.clearProgress();
405449
}
406450

407451
/*

extensions/ql-vscode/src/data-extensions-editor/external-api-usage-query.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { QueryResultType } from "../pure/new-messages";
1616
import { join } from "path";
1717
import { redactableError } from "../pure/errors";
1818
import { QueryLanguage } from "../common/query-language";
19+
import { Query } from "./queries/query";
1920

2021
export type RunQueryOptions = {
2122
cliServer: Pick<CodeQLCliServer, "resolveQlpacks">;
@@ -27,14 +28,17 @@ export type RunQueryOptions = {
2728
token: CancellationToken;
2829
};
2930

30-
export async function runQuery({
31-
cliServer,
32-
queryRunner,
33-
databaseItem,
34-
queryStorageDir,
35-
progress,
36-
token,
37-
}: RunQueryOptions): Promise<CoreCompletedQuery | undefined> {
31+
export async function runQuery(
32+
queryName: keyof Omit<Query, "dependencies">,
33+
{
34+
cliServer,
35+
queryRunner,
36+
databaseItem,
37+
queryStorageDir,
38+
progress,
39+
token,
40+
}: RunQueryOptions,
41+
): Promise<CoreCompletedQuery | undefined> {
3842
// The below code is temporary to allow for rapid prototyping of the queries. Once the queries are stabilized, we will
3943
// move these queries into the `github/codeql` repository and use them like any other contextual (e.g. AST) queries.
4044
// This is intentionally not pretty code, as it will be removed soon.
@@ -51,7 +55,7 @@ export async function runQuery({
5155

5256
const queryDir = (await dir({ unsafeCleanup: true })).path;
5357
const queryFile = join(queryDir, "FetchExternalApis.ql");
54-
await writeFile(queryFile, query.mainQuery, "utf8");
58+
await writeFile(queryFile, query[queryName], "utf8");
5559

5660
if (query.dependencies) {
5761
for (const [filename, contents] of Object.entries(query.dependencies)) {

extensions/ql-vscode/src/data-extensions-editor/queries/csharp.ts

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,52 @@ import { Query } from "./query";
22

33
export const fetchExternalApisQuery: Query = {
44
mainQuery: `/**
5-
* @name Usage of APIs coming from external libraries
6-
* @description A list of 3rd party APIs used in the codebase.
7-
* @tags telemetry
8-
* @id cs/telemetry/fetch-external-apis
9-
*/
5+
* @name Usage of APIs coming from external libraries
6+
* @description A list of 3rd party APIs used in the codebase.
7+
* @tags telemetry
8+
* @id cs/telemetry/fetch-external-apis
9+
*/
10+
11+
import csharp
12+
import ExternalApi
13+
14+
private Call aUsage(ExternalApi api) {
15+
result.getTarget().getUnboundDeclaration() = api
16+
}
17+
18+
private boolean isSupported(ExternalApi api) {
19+
api.isSupported() and result = true
20+
or
21+
not api.isSupported() and
22+
result = false
23+
}
24+
25+
from ExternalApi api, string apiName, boolean supported, Call usage
26+
where
27+
apiName = api.getApiName() and
28+
supported = isSupported(api) and
29+
usage = aUsage(api)
30+
select apiName, supported, usage
31+
`,
32+
usagesQuery: `/**
33+
* @name Usage of APIs coming from external libraries
34+
* @description A list of 3rd party APIs used in the codebase.
35+
* @kind problem
36+
* @id cs/telemetry/fetch-external-api-usages
37+
*/
38+
39+
import csharp
40+
import ExternalApi
41+
42+
private Call aUsage(ExternalApi api) {
43+
result.getTarget().getUnboundDeclaration() = api
44+
}
1045
11-
import csharp
12-
import semmle.code.csharp.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
13-
import ExternalApi
14-
15-
private Call aUsage(ExternalApi api) {
16-
result.getTarget().getUnboundDeclaration() = api
17-
}
18-
19-
private boolean isSupported(ExternalApi api) {
20-
api.isSupported() and result = true
21-
or
22-
not api.isSupported() and
23-
result = false
24-
}
25-
26-
from ExternalApi api, string apiName, boolean supported, Call usage
27-
where
28-
apiName = api.getApiName() and
29-
supported = isSupported(api) and
30-
usage = aUsage(api)
31-
select apiName, supported, usage
46+
from ExternalApi api, string apiName, Call usage
47+
where
48+
apiName = api.getApiName() and
49+
usage = aUsage(api)
50+
select usage, apiName
3251
`,
3352
dependencies: {
3453
"ExternalApi.qll": `/** Provides classes and predicates related to handling APIs from external libraries. */

0 commit comments

Comments
 (0)