Skip to content

Commit 143443b

Browse files
authored
Merge pull request #4215 from d10c/d10c/mrva-go-autofix
MRVA: use Go Autofix instead of Cocofix
2 parents deaaab9 + e36e25a commit 143443b

File tree

3 files changed

+162
-42
lines changed

3 files changed

+162
-42
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,23 +1158,22 @@ export class CodeQLCliServer implements Disposable {
11581158
/**
11591159
* Uses a .qhelp file to generate Query Help documentation in a specified format.
11601160
* @param pathToQhelp The path to the .qhelp file
1161-
* @param format The format in which the query help should be generated {@link https://codeql.github.com/docs/codeql-cli/manual/generate-query-help/#cmdoption-codeql-generate-query-help-format}
1162-
* @param outputDirectory The output directory for the generated file
1161+
* @param outputFileOrDirectory The output directory for the generated file
11631162
*/
11641163
async generateQueryHelp(
11651164
pathToQhelp: string,
1166-
outputDirectory?: string,
1165+
outputFileOrDirectory?: string,
11671166
): Promise<string> {
11681167
const subcommandArgs = ["--format=markdown"];
1169-
if (outputDirectory) {
1170-
subcommandArgs.push("--output", outputDirectory);
1168+
if (outputFileOrDirectory) {
1169+
subcommandArgs.push("--output", outputFileOrDirectory);
11711170
}
11721171
subcommandArgs.push(pathToQhelp);
11731172

11741173
return await this.runCodeQlCliCommand(
11751174
["generate", "query-help"],
11761175
subcommandArgs,
1177-
`Generating qhelp in markdown format at ${outputDirectory}`,
1176+
`Generating qhelp in markdown format at ${outputFileOrDirectory}`,
11781177
);
11791178
}
11801179

extensions/ql-vscode/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,3 +961,9 @@ export const AUTOFIX_MODEL = new Setting("model", AUTOFIX_SETTING);
961961
export function getAutofixModel(): string | undefined {
962962
return AUTOFIX_MODEL.getValue<string>() || undefined;
963963
}
964+
965+
export const AUTOFIX_CAPI_DEV_KEY = new Setting("capiDevKey", AUTOFIX_SETTING);
966+
967+
export function getAutofixCapiDevKey(): string | undefined {
968+
return AUTOFIX_CAPI_DEV_KEY.getValue<string>() || undefined;
969+
}

extensions/ql-vscode/src/variant-analysis/view-autofixes.ts

Lines changed: 151 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ import type { VariantAnalysisResultsManager } from "./variant-analysis-results-m
3838
import {
3939
getAutofixPath,
4040
getAutofixModel,
41+
getAutofixCapiDevKey,
4142
downloadTimeout,
4243
AUTOFIX_PATH,
4344
AUTOFIX_MODEL,
45+
AUTOFIX_CAPI_DEV_KEY,
4446
} from "../config";
4547
import { asError, getErrorMessage } from "../common/helpers-pure";
4648
import { createTimeoutSignal } from "../common/fetch-stream";
@@ -155,6 +157,39 @@ async function findLocalAutofix(): Promise<string> {
155157
return localAutofixPath;
156158
}
157159

160+
/**
161+
* Finds and resolves the Copilot API dev key from the `codeQL.autofix.capiDevKey` setting.
162+
* The key can be specified as an environment variable reference (e.g., `env:MY_ENV_VAR`)
163+
* or a 1Password secret reference (e.g., `op://vault/item/field`). By default, it uses
164+
* the environment variable `CAPI_DEV_KEY`.
165+
*
166+
* @returns The resolved Copilot API dev key.
167+
* @throws Error if the Copilot API dev key is not found or invalid.
168+
*/
169+
async function findCapiDevKey(): Promise<string> {
170+
let capiDevKey = getAutofixCapiDevKey() || "env:CAPI_DEV_KEY";
171+
172+
if (!capiDevKey.startsWith("env:") && !capiDevKey.startsWith("op://")) {
173+
// Don't allow literal keys in config.json for security reasons
174+
throw new Error(
175+
`Invalid CAPI dev key format. Use 'env:<ENV_VAR_NAME>' or 'op://<1PASSWORD_SECRET_REFERENCE>'.`,
176+
);
177+
}
178+
if (capiDevKey.startsWith("env:")) {
179+
const envVarName = capiDevKey.substring("env:".length);
180+
capiDevKey = process.env[envVarName] || "";
181+
}
182+
if (capiDevKey.startsWith("op://")) {
183+
capiDevKey = await opRead(capiDevKey);
184+
}
185+
if (!capiDevKey) {
186+
throw new Error(
187+
`Copilot API dev key not found. Make sure ${AUTOFIX_CAPI_DEV_KEY.qualifiedName} is set correctly.`,
188+
);
189+
}
190+
return capiDevKey;
191+
}
192+
158193
/**
159194
* Overrides the query help from a given variant analysis
160195
* at a location within the `localAutofixPath` directory .
@@ -210,19 +245,37 @@ async function overrideQueryHelp(
210245
// Use `replaceAll` since some query IDs have multiple slashes.
211246
const queryIdWithDash = queryId.replaceAll("/", "-");
212247

213-
// Get the path to the output directory for overriding the query help.
214-
// Note: the path to this directory may change in the future.
215-
const queryHelpOverrideDirectory = join(
248+
// Get the path to the output file for overriding the query help.
249+
// Note: the path to this file may change in the future.
250+
const queryHelpOverrideFile = join(
216251
localAutofixPath,
217-
"prompt-templates",
252+
"pkg",
253+
"autofix",
254+
"prompt",
218255
"qhelps",
219256
`${queryIdWithDash}.md`,
220257
);
221258

222-
await cliServer.generateQueryHelp(
223-
queryHelpFilePath,
224-
queryHelpOverrideDirectory,
225-
);
259+
// If the file already exists, slurp it so that we can check if it has changed.
260+
let existingContents: string | null = null;
261+
if (await pathExists(queryHelpOverrideFile)) {
262+
existingContents = await readFile(queryHelpOverrideFile, "utf8");
263+
}
264+
265+
// Generate the query help and output it to the override directory.
266+
await cliServer.generateQueryHelp(queryHelpFilePath, queryHelpOverrideFile);
267+
268+
// If the contents of `queryHelpOverrideFile` have changed, recompile autofix
269+
// to include the new query help.
270+
if (existingContents !== null) {
271+
const newContents = await readFile(queryHelpOverrideFile, "utf8");
272+
if (existingContents !== newContents) {
273+
void extLogger.log(
274+
`Query help for query ID ${queryId} has changed. Recompiling autofix...`,
275+
);
276+
await recompileAutofix(localAutofixPath);
277+
}
278+
}
226279
}
227280

228281
/**
@@ -607,9 +660,9 @@ async function runAutofixForRepository(
607660
} = await getRepoStoragePaths(autofixOutputStoragePath, nwo);
608661

609662
// Get autofix binary.
610-
// Switch to Go binary in the future and have user pass full path
663+
// In the future, have user pass full path
611664
// in an environment variable instead of hardcoding part here.
612-
const cocofixBin = join(process.cwd(), localAutofixPath, "bin", "cocofix.js");
665+
const autofixBin = join(process.cwd(), localAutofixPath, "bin", "autofix");
613666

614667
// Limit number of fixes generated.
615668
const limitFixesBoolean: boolean = resultCount > MAX_NUM_FIXES;
@@ -642,7 +695,7 @@ async function runAutofixForRepository(
642695
transcriptFiles.push(tempTranscriptFilePath);
643696

644697
await runAutofixOnResults(
645-
cocofixBin,
698+
autofixBin,
646699
sarifFile,
647700
srcRootPath,
648701
tempOutputTextFilePath,
@@ -661,7 +714,7 @@ async function runAutofixForRepository(
661714
} else {
662715
// Run autofix once for all alerts.
663716
await runAutofixOnResults(
664-
cocofixBin,
717+
autofixBin,
665718
sarifFile,
666719
srcRootPath,
667720
outputTextFilePath,
@@ -707,7 +760,7 @@ async function getRepoStoragePaths(
707760
* Runs autofix on the results in the given SARIF file.
708761
*/
709762
async function runAutofixOnResults(
710-
cocofixBin: string,
763+
autofixBin: string,
711764
sarifFile: string,
712765
srcRootPath: string,
713766
outputTextFilePath: string,
@@ -736,7 +789,7 @@ async function runAutofixOnResults(
736789
"--format",
737790
"text",
738791
"--diff-style",
739-
"diff", // could do "text" instead if want line of "=" between fixes
792+
"git", // auto|color|plain|git|unified
740793
"--output",
741794
outputTextFilePath,
742795
"--fix-description",
@@ -751,12 +804,12 @@ async function runAutofixOnResults(
751804
}
752805

753806
await execAutofix(
754-
cocofixBin,
807+
autofixBin,
755808
autofixArgs,
756809
{
757810
cwd: workDir,
758811
env: {
759-
CAPI_DEV_KEY: process.env.CAPI_DEV_KEY,
812+
CAPI_DEV_KEY: await findCapiDevKey(),
760813
PATH: process.env.PATH,
761814
},
762815
},
@@ -765,14 +818,14 @@ async function runAutofixOnResults(
765818
}
766819

767820
/**
768-
* Executes the autofix command.
821+
* Spawns an external process and collects its output.
769822
*/
770-
function execAutofix(
823+
function execCommand(
771824
bin: string,
772825
args: string[],
773826
options: Parameters<typeof execFileSync>[2],
774827
showCommand?: boolean,
775-
): Promise<void> {
828+
): Promise<{ code: number | null; stdout: string; stderr: string }> {
776829
return new Promise((resolve, reject) => {
777830
try {
778831
const cwd = options?.cwd || process.cwd();
@@ -805,27 +858,87 @@ function execAutofix(
805858

806859
// Listen for process exit
807860
p.on("exit", (code) => {
808-
// Log collected output
809-
if (stdoutBuffer.trim()) {
810-
void extLogger.log(`Autofix stdout:\n${stdoutBuffer.trim()}`);
811-
}
812-
813-
if (stderrBuffer.trim()) {
814-
void extLogger.log(`Autofix stderr:\n${stderrBuffer.trim()}`);
815-
}
816-
817-
if (code === 0) {
818-
resolve();
819-
} else {
820-
reject(new Error(`Autofix process exited with code ${code}.`));
821-
}
861+
resolve({
862+
code,
863+
stdout: stdoutBuffer.trim(),
864+
stderr: stderrBuffer.trim(),
865+
});
822866
});
823867
} catch (e) {
824868
reject(asError(e));
825869
}
826870
});
827871
}
828872

873+
/**
874+
* Executes the autofix command.
875+
*/
876+
async function execAutofix(
877+
bin: string,
878+
args: string[],
879+
options: Parameters<typeof execFileSync>[2],
880+
showCommand?: boolean,
881+
): Promise<void> {
882+
const { code, stdout, stderr } = await execCommand(
883+
bin,
884+
args,
885+
options,
886+
showCommand,
887+
);
888+
889+
if (code !== 0) throw new Error(`Autofix process exited with code ${code}.`);
890+
891+
// Log collected output
892+
if (stdout) {
893+
void extLogger.log(`Autofix stdout:\n${stdout}`);
894+
}
895+
if (stderr) {
896+
void extLogger.log(`Autofix stderr:\n${stderr}`);
897+
}
898+
}
899+
900+
/** Execute the 1Password CLI command `op read <secretReference>`, if the `op` command exists on the PATH. */
901+
async function opRead(secretReference: string): Promise<string> {
902+
try {
903+
const { code, stdout, stderr } = await execCommand(
904+
"op",
905+
["read", secretReference],
906+
{},
907+
false,
908+
);
909+
910+
if (code === 0) {
911+
return stdout;
912+
} else {
913+
throw new Error(
914+
`1Password CLI exited with code ${code}. Stderr: ${stderr}`,
915+
);
916+
}
917+
} catch (e) {
918+
const error = asError(e);
919+
if ("code" in error && error.code === "ENOENT") {
920+
throw new Error("1Password CLI (op) not found in PATH");
921+
}
922+
throw e;
923+
}
924+
}
925+
926+
/** Recompile the Autofix binary. */
927+
async function recompileAutofix(localAutofixPath: string): Promise<void> {
928+
const { code, stderr } = await execCommand(
929+
"make",
930+
["build"],
931+
{ cwd: localAutofixPath },
932+
false,
933+
);
934+
935+
if (code !== 0) {
936+
throw new Error(
937+
`Failed to recompile autofix after query help change. Exit code: ${code}. Stderr: ${stderr}`,
938+
);
939+
}
940+
}
941+
829942
/**
830943
* Creates a new file path by appending the given suffix.
831944
* @param filePath The original file path.
@@ -902,9 +1015,11 @@ async function formatWithMarkdown(
9021015
const backFormatting: string =
9031016
"```\n\n</details>\n\n ### Notes\n - notes placeholder\n\n";
9041017

905-
// Format the content with Markdown
906-
// Replace ``` in the content with \``` to avoid breaking the Markdown code block
907-
const formattedContent = `## ${header}\n\n${frontFormatting}${content.replaceAll("```", "\\```")}${backFormatting}`;
1018+
// Format the content with Markdown:
1019+
// - Replace ``` in the content with \``` to avoid breaking the Markdown code block
1020+
// - Remove raw terminal escape sequences if any (workaround until `--diff-style plain` is handled by autofix)
1021+
// eslint-disable-next-line no-control-regex
1022+
const formattedContent = `## ${header}\n\n${frontFormatting}${content.replaceAll("```", "\\```").replaceAll(/\x1b\[[0-9;]*m/g, "")}${backFormatting}`;
9081023

9091024
// Write the formatted content back to the file
9101025
await writeFile(inputFile, formattedContent);

0 commit comments

Comments
 (0)